feat(domain): pure business logic for parsing, identity, slots, acquisition, paths, organize, download state (W1-2)

- release-parser: title/file/dir parsing with MP parity + deviations (airDate/issue,
  cut, release group suffix fallback, dir+file merge, safe episode offset)
- identity: title normalization (NFKC, trad->simp map, season suffix) and
  evidence-level candidate ranking (year tolerance ±1, no probabilities)
- slot: hard/soft/upgrade evaluation (missing vs unmet fields), state machine,
  movie/unit completion (season pack must be covered by actual file list)
- acquisition: new_title/catch_up/upgrade modes, priority order, budget,
  cache volume hard threshold with resume watermark (no double counting)
- path: immutable path bindings, restricted naming templates, sanitize,
  conflict detection, downloader<->host path mapping
- organize: planOrganize with sidecar pairing, sample exclusion, existing
  target policy, crossDevice marking, H&R -> conflict for move
- download-state: qB state mapping, completion, no-progress evidence

230 tests, ~97% statement coverage.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-27 18:11:29 +08:00
1 parent a6d049db81
commit 7ba712ea55
40 files changed
+5885 -2

No files matched your search

@@ -0,0 +1,37 @@
import type { AcquisitionMode } from "./mode";
/** 默认:用户明确立即处理 > 追更补缺 > 新作品 > 已有版本升级(REQ §6.8) */
export type PriorityClass = "immediate" | "catch_up" | "new_title" | "upgrade";
export const DEFAULT_PRIORITY_ORDER: readonly PriorityClass[] = ["immediate", "catch_up", "new_title", "upgrade"];
export interface QueueItem {
id: string;
priorityClass: PriorityClass;
/** 入队时间 ISO;同优先级先进先出,避免饥饿 */
enqueuedAt: string;
}
export function priorityOf(mode: AcquisitionMode, immediate = false): PriorityClass {
return immediate ? "immediate" : mode;
}
export function validatePriorityOrder(order: readonly PriorityClass[]): void {
const all = new Set<PriorityClass>(DEFAULT_PRIORITY_ORDER);
if (order.length !== all.size || new Set(order).size !== all.size || !order.every((o) => all.has(o)))
throw new Error(`优先级配置必须恰好包含 ${[...all].join("/")} 各一次`);
}
/** 按优先级排序(稳定、纯函数)。优先级只影响资源分配,不越过阈值/HR/权限 */
export function sortByPriority<T extends QueueItem>(
items: readonly T[],
order: readonly PriorityClass[] = DEFAULT_PRIORITY_ORDER,
): T[] {
validatePriorityOrder(order);
const rank = new Map(order.map((c, i) => [c, i]));
return [...items].sort(
(a, b) =>
rank.get(a.priorityClass)! - rank.get(b.priorityClass)! ||
a.enqueuedAt.localeCompare(b.enqueuedAt) ||
a.id.localeCompare(b.id),
);
}