refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries

This commit is contained in:
root committed 2026-10-13 14:37:03 +08:00
1 parent 3f1c6d02f6
commit 27c0f27434
268 files changed
+7468 -7852

No files matched your search

+3 -3
View File
@@ -3,8 +3,8 @@
纯业务逻辑(无 IO):发布标题解析、身份匹配、Slot 满足度、收录模式/预算/磁盘阈值、路径绑定与命名、整理计划、下载状态映射。
- 禁止 import 任何网络/文件/数据库库,也不读全局配置与时钟;时间(`now`)、设备号、文件清单、已存在目标等事实一律由调用方传入。
- 不依赖 `@umt/contracts`:这里的类型是领域类型,W2 在 server/tasks 层映射到 zod 合同。
- 唯一入口 `src/index.ts`(`export *` 各模块)。测试:`cd packages/domain && deno task test`。
- 同一语义的类型和纯规则可以依赖 `@umt/contracts`,例如 `ContentType`;含义不同的领域类型通过 `@umt/mapping` 转换。保持无 IO,不依赖 server / db / adapters。
- 唯一入口 `src/index.ts`(`export *` 各模块)。测试:`cd packages/domain && bun run test`。
## 模块地图
@@ -49,7 +49,7 @@
| move 跨盘 | EXDEV 隐式降级复制,删源失败仍成功 | 只标记 `crossDevice`,执行层显式三步 |
| Move 删种 | 不看 H&R | H&R 未满足/未知 → 阻塞冲突 |
## 给 W2 的接入提示
## 接入提示
- 解析:下载完成后用 `parseReleasePath({ file, parent, grandParent })` 或 `planOrganize({ sourceRoot })`(只在种子根内取目录名);RSS/站点标题用 `parseRelease(title, { subtitle })`。`ParsedRelease.sizeBytes` 可用站点字段覆盖(`{ ...parsed, sizeBytes }`)后再交给 Slot 评估。
- 身份:本地别名召回(pg_trgm)与 TMDB 结果都转成 `IdentityCandidate`(本地用 `provider: "local"` + `localEntityId` + `linkedExternalIds`);`resolveIdentity` 为 `auto` 才自动绑定,其余进待识别池。
-9
View File
@@ -1,9 +0,0 @@
{
"name": "@umt/domain",
"version": "0.3.44",
"exports": { ".": "./src/index.ts" },
"tasks": {
"test": "deno run -A npm:vitest run",
"typecheck": "deno run -A npm:typescript/tsc --noEmit -p tsconfig.json"
}
}
+8
View File
@@ -4,5 +4,13 @@
"type": "module",
"exports": {
".": "./src/index.ts"
},
"version": "0.3.44",
"scripts": {
"test": "bun x --bun vitest run",
"typecheck": "bun x --bun tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@umt/contracts": "workspace:*"
}
}
+1
View File
@@ -25,6 +25,7 @@ export {
resolveAutoFill,
} from "./fill.ts";
export { type AcquisitionMode, determineMode, type ModeDecision, type ModeInput } from "./mode.ts";
export { type ScopedAcquisitionPolicy, selectAcquisitionPolicy } from "./policy.ts";
export {
DEFAULT_PRIORITY_ORDER,
type PriorityClass,
+30
View File
@@ -0,0 +1,30 @@
import type { AcquisitionPolicyScope } from "@umt/contracts";
export interface ScopedAcquisitionPolicy {
id: string;
scope: AcquisitionPolicyScope;
}
/** 输入已按发布时间排序;分类明确命中优先于类型默认,显式选择不回退到别的策略。 */
export function selectAcquisitionPolicy<T extends ScopedAcquisitionPolicy>(
policies: readonly T[],
input: { explicitId?: string | null; mediaKind: "movie" | "series" | "variety"; category: string | null },
): T | undefined {
if (input.explicitId) return policies.find((p) => p.id === input.explicitId);
const type = input.mediaKind === "movie" ? "movie" : "tv";
let selected: T | undefined;
let specificity = -1;
for (const p of policies) {
if (p.scope.mediaKinds?.length && !p.scope.mediaKinds.includes(input.mediaKind)) continue;
const categories = p.scope.categories;
if (categories && Object.values(categories).some((v) => v?.length)) {
if (!input.category || !categories[type]?.includes(input.category)) continue;
}
const score = categories?.[type]?.length ? 2 : p.scope.mediaKinds?.length ? 1 : 0;
if (score > specificity) {
selected = p;
specificity = score;
}
}
return selected;
}
+3 -9
View File
@@ -1,3 +1,4 @@
import type { ContentType } from "@umt/contracts";
import type { CompiledGlobalWord } from "../identify-words/apply.ts";
/** 解析器版本:规则变化时递增,缓存/识别结果按此失效 */
@@ -35,15 +36,8 @@ export type Cut =
| "Ultimate"
| "Special Edition";
/** 综艺内容类型(REQ §5.1);v0.1 只识别标记,不做分段建模 */
export type ContentType =
| "unknown"
| "main"
| "extra"
| "behind_the_scenes"
| "performance_only"
| "preview"
| "reaction";
// 内容类型在 contracts 定义一次;领域与接口使用相同语义。
export type { ContentType };
export type Segment = "full" | "upper" | "middle" | "lower";
+77
View File
@@ -0,0 +1,77 @@
import type { ParsedRelease } from "../release-parser/types.ts";
import type { HardField } from "../slot/types.ts";
import { type AtomCatalog, testAtom } from "./evaluate.ts";
import type { RuleContext, RuleExpr, RuleGroupSpec } from "./types.ts";
// 只对有明确事实字段的条件判断未知;正则文本缺词仍是已知不命中。
const fields: Record<string, HardField> = {
"4K": "resolution",
"1080P": "resolution",
"720P": "resolution",
BLU: "source",
BLURAY: "source",
REMUX: "source",
WEBDL: "source",
H265: "codec",
H264: "codec",
FREE: "promotion",
};
type PartialResult = { hit: boolean | null; missing: HardField[] };
function partial(e: RuleExpr, r: ParsedRelease, ctx: RuleContext, catalog: AtomCatalog): PartialResult {
if (e.op === "atom") {
const id = e.id.toUpperCase();
const f = fields[id];
const value =
f === "resolution"
? r.resolution
: f === "source"
? r.source
: f === "codec"
? r.videoCodec
: ctx.downloadVolumeFactor;
if (f && value === undefined) return { hit: null, missing: [f] };
const custom = catalog.get(id);
const missing: HardField[] = [];
if (custom?.minSeeders !== undefined && ctx.seeders === undefined) missing.push("seeders");
if (custom?.free && ctx.downloadVolumeFactor === undefined) missing.push("promotion");
if (custom?.unitSizeMB && r.sizeBytes === undefined) missing.push("size");
const input = { r, ctx, text: [r.raw ?? "", ctx.subtitle ?? "", ...(ctx.labels ?? [])].join(" ") };
// 已知的正则条件失败时,其他缺失字段不会改变结论。
if (custom && missing.length) {
const known = { ...custom, minSeeders: undefined, free: undefined, unitSizeMB: undefined };
const knownCatalog = new Map(catalog).set(id, known);
if (!testAtom(id, input, knownCatalog).hit) return { hit: false, missing: [] };
return { hit: null, missing };
}
return { hit: testAtom(id, input, catalog).hit, missing: [] };
}
if (e.op === "not") {
const result = partial(e.arg, r, ctx, catalog);
return { ...result, hit: result.hit === null ? null : !result.hit };
}
const results = e.args.map((a) => partial(a, r, ctx, catalog));
const decisive = e.op !== "and";
if (results.some((x) => x.hit === decisive)) return { hit: decisive, missing: [] };
const unknown = results.filter((x) => x.hit === null);
return unknown.length ? { hit: null, missing: unknown.flatMap((x) => x.missing) } : { hit: !decisive, missing: [] };
}
/** 未知的高档不能被当成不命中而落入低档,NOT 也不能把未知当成事实。 */
export function unknownRuleFields(
groups: readonly RuleGroupSpec[],
r: ParsedRelease,
ctx: RuleContext,
catalog: AtomCatalog,
): HardField[] {
const missing: HardField[] = [];
for (const group of groups) {
for (const expr of group.hardExcludes ?? []) missing.push(...partial(expr, r, ctx, catalog).missing);
for (const layer of group.layers) {
const result = partial(layer.expr, r, ctx, catalog);
missing.push(...result.missing);
if (result.hit === true) break;
}
}
return [...new Set(missing)];
}
+95 -11
View File
@@ -1,6 +1,8 @@
import type { ParsedRelease, ReleaseSource, Resolution } from "../release-parser/types.ts";
import { catalogOf, evaluateRuleChain } from "../rules/evaluate.ts";
import type { RuleChainResult } from "../rules/types.ts";
import { unknownRuleFields } from "../rules/unknown.ts";
import { seasonReleaseKey, upgradeSpec } from "./season-release.ts";
import type {
EvaluationContext,
HardField,
@@ -310,6 +312,8 @@ export function describeViolation(v: Violation): string {
return `优先级没有提高(现有 ${JSON.stringify(e)},这个资源 ${JSON.stringify(a)})`;
case "upgrade.not_full_pack":
return "全集洗版只接受整季包";
case "upgrade.different_season_release":
return "与当季选定的成套版本不同,等待同套资源";
case "upgrade.no_improvable_units":
return "这个资源包含的季集都没有可升级的";
default:
@@ -348,6 +352,37 @@ function tierFailures(rule: SlotTierRule, r: ParsedRelease, ctx: EvaluationConte
return out;
}
function unknownTierFields(spec: SlotSpec, r: ParsedRelease, ctx: EvaluationContext): HardField[] {
for (const tier of spec.soft?.tiers ?? []) {
if (!tierFailures(tier, r, ctx).length) break;
const missing: HardField[] = [];
const filled = { ...r };
if (!r.resolution && tier.resolutions?.length) {
missing.push("resolution");
filled.resolution = tier.resolutions[0];
}
if (!r.source && (tier.sources?.length || tier.excludeSources?.length || tier.requireEncode)) {
missing.push("source");
filled.source =
tier.sources?.[0] ??
(tier.requireEncode
? "BluRay"
: (Object.keys(DEFAULT_SOURCE_TIERS) as ReleaseSource[]).find((s) => !tier.excludeSources?.includes(s)));
filled.isEncode = tier.requireEncode ?? filled.source === "BluRay";
}
if (!r.videoCodec && tier.codecs?.length) {
missing.push("codec");
filled.videoCodec = tier.codecs[0];
}
if (!r.releaseGroup && tier.groups?.length) {
missing.push("group");
filled.releaseGroup = { name: tier.groups.join("@"), source: "known" };
}
if (missing.length && !tierFailures(tier, filled, ctx).length) return missing;
}
return [];
}
/**
* 优先级阶梯(对应 MP 规则组 `A > B > C`):逐层匹配,第 k 层(0 起)命中 → pri = 100 - k;
* 未配置阶梯 → pri = 100、level = null(不参与排序差异);配置了但都不命中 → pri = 0。
@@ -465,9 +500,21 @@ export function evaluateCandidate(
current?: QualityFacts | null,
ctx: EvaluationContext = {},
): SlotEvaluation {
if (current) spec = upgradeSpec(spec);
const hard = checkHardConditions(spec, r, ctx);
const { missing, accepted } = hard;
const chain = ruleChainOf(spec, r, ctx);
if (hard.violations.length === 0) {
const unknown = spec.rules ? unknownRuleFields(spec.rules.groups, r, ctx, catalogOf(spec.rules.atoms)) : [];
if (chain?.decidingGroup === undefined || chain.decidingGroup === null)
unknown.push(...unknownTierFields(spec, r, ctx));
if (unknown.length)
return {
decision: "unknown_field",
missing: [...new Set([...hard.missing, ...unknown])],
trace: unknown.map((f) => `组合规则需要补充:${f}`),
};
}
const { missing, accepted } = hard;
const withRules = chain ? { rules: chain } : {};
const violations = [...hard.violations, ...chainViolations(chain, spec)];
const ladder = effectiveLadder(spec, r, ctx, chain);
@@ -524,6 +571,53 @@ export function evaluateCandidate(
const up = spec.upgrade;
if (!up?.enabled) return reject({ field: "upgrade", code: "upgrade_disabled" });
if (up.mode === "full_pack" && (r.episodes || r.airDate)) return reject({ field: "upgrade", code: "not_full_pack" });
if (r.isEncode && up.encodeGroupAllowlist?.length) {
if (!r.releaseGroup) return { decision: "unknown_field", missing: ["group"], trace: ["合格压制组需要查证发布组"] };
const groups = r.releaseGroup ? r.releaseGroup.name.split("@").map(lc) : [];
if (!up.encodeGroupAllowlist.some((g) => groups.includes(lc(g))))
return reject({
field: "upgrade",
code: "encode_not_qualified",
expected: up.encodeGroupAllowlist,
actual: r.releaseGroup?.name,
});
}
if (up.consistency === "season_release") {
const key = seasonReleaseKey(r);
const targetKey = ctx.seasonReleaseTarget ? seasonReleaseKey(ctx.seasonReleaseTarget) : null;
if (key && !targetKey)
return { decision: "unknown_field", missing: ["current_asset"], trace: ["当季成套目标的规格未知"] };
if (!key)
return {
decision: "unknown_field",
missing: [
...(!r.resolution ? ["resolution" as const] : []),
...(!r.source ? ["source" as const] : []),
...(!r.videoCodec ? ["codec" as const] : []),
...(!r.releaseGroup?.name ? ["group" as const] : []),
],
trace: ["成套洗版需要完整版本规格"],
};
if (key !== targetKey) return reject({ field: "upgrade", code: "different_season_release" });
const fromPri = currentPri(spec, current);
if (key !== seasonReleaseKey(current) && ladder.pri === fromPri) {
const from = qualityTier(current, spec);
if (tier.source < from.source || tier.resolution < from.resolution)
return reject({ field: "upgrade", code: "lower_tier", expected: from, actual: tier });
return {
decision: "upgrade",
tier,
fromTier: from,
softRank: rank,
reasons: [...reasons, "season_release_aligned", ...priReasons],
pri: ladder.pri,
fromPri,
ladder,
trace: ["保持优先级,向当季同套版本对齐", ...ladder.trace],
...withRules,
};
}
}
const basis = up.basis ?? (tiered ? "priority" : "tier");
if (basis === "priority") {
const fromPri = currentPri(spec, current);
@@ -559,16 +653,6 @@ export function evaluateCandidate(
dSource === 0 && dRes === 0 ? "same_tier" : dSource < 0 || dRes < 0 ? "lower_tier" : "below_min_improvement";
return reject({ field: "upgrade", code, expected: { minTierSteps: minSteps, from }, actual: tier });
}
if (r.isEncode && up.encodeGroupAllowlist?.length) {
const groups = r.releaseGroup ? r.releaseGroup.name.split("@").map(lc) : [];
if (!up.encodeGroupAllowlist.some((g) => groups.includes(lc(g))))
return reject({
field: "upgrade",
code: "encode_not_qualified",
expected: up.encodeGroupAllowlist,
actual: r.releaseGroup?.name,
});
}
return {
decision: "upgrade",
tier,
+1
View File
@@ -59,6 +59,7 @@ export {
type SubscriptionGroupResult,
subscriptionGroup,
} from "./overview.ts";
export { seasonReleaseKey, selectSeasonRelease, upgradeSpec } from "./season-release.ts";
export {
canTransition,
type SlotEvent,
@@ -0,0 +1,49 @@
import type { ParsedRelease } from "../release-parser/types.ts";
import { compareRank, currentPri, qualityTier, softRank } from "./evaluate.ts";
import type { QualityFacts, SlotSpec } from "./types.ts";
/** 缺少关键规格时不能声称同套;集号、文件名、种子 ID 不属于版本身份。 */
export function seasonReleaseKey(q: QualityFacts): string | null {
if (!q.resolution || !q.source || !q.videoCodec || !q.releaseGroup?.name) return null;
return JSON.stringify([
q.resolution,
q.source,
q.videoCodec,
q.releaseGroup.name
.toLowerCase()
.split("@")
.map((g) => g.trim())
.sort(),
[...(q.hdr ?? [])].sort(),
q.cut ?? "default",
]);
}
export function upgradeSpec(spec: SlotSpec): SlotSpec {
return {
...spec,
...(spec.upgradeRules ? { rules: spec.upgradeRules } : {}),
...(spec.upgradeHard ? { hard: spec.upgradeHard } : {}),
};
}
/** 优先级、软偏好、规范化规格形成稳定顺序;同档对齐不允许质量降档。 */
export function selectSeasonRelease(spec: SlotSpec, versions: readonly QualityFacts[]): QualityFacts | undefined {
let target: QualityFacts | undefined;
const rank = (q: QualityFacts) => {
const tier = qualityTier(q, spec);
return [
currentPri(spec, q),
tier.source,
tier.resolution,
...softRank(spec, { raw: "", hdr: [], audio: [], ...q } as unknown as ParsedRelease),
];
};
for (const q of versions) {
const key = seasonReleaseKey(q);
if (!key) continue;
const compared = target ? compareRank(rank(q), rank(target)) : 1;
if (compared > 0 || (compared === 0 && key < seasonReleaseKey(target!)!)) target = q;
}
return target;
}
+9
View File
@@ -91,6 +91,10 @@ export interface SlotSoftPreferences {
export interface SlotUpgradePolicy {
enabled: boolean;
/** 洗版阶段显式规则组;未设时沿用既有洗版默认链。 */
ruleGroupIds?: string[];
/** 追更可混合,洗版按季向同组、同来源和编码的版本收敛。 */
consistency?: "season_release";
/** 最小改进:来源等级至少提升几级,默认 1(同等级不反复更换) */
minTierSteps?: number;
/** 来源等级表;默认 REMUX/原盘/蓝光二压=3,WEB-DL=2,WEBRip/HDTV/HDRip=1,DVD=0 */
@@ -128,6 +132,9 @@ export interface SlotSpec {
hard: SlotHardConditions;
/** 规则组:硬排除 → 拒绝;各组须命中某层(ladderRequired);pri 取最后一个带层的组(覆盖 soft.tiers 的 pri) */
rules?: SlotRuleChain;
/** 服务入口解析的洗版规则,不参与首次补齐。 */
upgradeRules?: SlotRuleChain;
upgradeHard?: SlotHardConditions;
unknownPolicy?: UnknownPolicy | Partial<Record<HardField, UnknownPolicy>>;
soft?: SlotSoftPreferences;
upgrade?: SlotUpgradePolicy;
@@ -151,6 +158,8 @@ export type QualityFacts = Pick<
/** 评估上下文:站点字段与折算口径(不在标题解析里的事实由调用方给) */
export interface EvaluationContext {
/** 季级成套目标,由同一收录版本的有效资产及在途计划确定。 */
seasonReleaseTarget?: QualityFacts;
siteId?: string;
/** 副标题/标签等参与关键词匹配的文本 */
subtitle?: string;
@@ -0,0 +1,356 @@
import { describe, expect, it } from "vitest";
import {
evaluateCandidate,
type ParsedRelease,
parseRelease,
parseRuleString,
type QualityFacts,
type RuleGroupSpec,
rankForSlot,
type SlotSpec,
seasonReleaseKey,
selectAcquisitionPolicy,
selectSeasonRelease,
} from "../src/index.ts";
const release = (title: string, facts: Partial<ParsedRelease> = {}): ParsedRelease => ({
...parseRelease(title),
...facts,
});
const group = (name: string, rule: string): RuleGroupSpec => ({ name, layers: parseRuleString(rule).layers });
const current: QualityFacts = release("Show.S01E01.1080p.WEB-DL.H265-OTHER");
const target = release("Show.S01E02.1080p.WEB-DL.H265-CHD");
const consistent: SlotSpec = {
name: "正片",
hard: { resolutions: ["1080p", "2160p"] },
rules: { groups: [group("允许收录", "1080P | 4K")] },
upgrade: { enabled: true, consistency: "season_release" },
};
describe("按分类选择默认收录策略", () => {
const policies = [
{ id: "global", scope: {} },
{ id: "series", scope: { mediaKinds: ["series" as const] } },
{ id: "animation", scope: { categories: { tv: ["动漫剧集"] } } },
];
it("订阅显式指定的策略覆盖类型和分类范围", () => {
expect(
selectAcquisitionPolicy(policies, { explicitId: "series", mediaKind: "movie", category: "动画电影" })?.id,
).toBe("series");
});
it("显式指定的策略不存在时不悄悄回退", () => {
expect(
selectAcquisitionPolicy(policies, { explicitId: "missing", mediaKind: "series", category: "动漫剧集" }),
).toBeUndefined();
});
it("分类匹配优先于类型默认,类型默认优先于全局默认", () => {
expect(selectAcquisitionPolicy(policies, { mediaKind: "series", category: "动漫剧集" })?.id).toBe("animation");
expect(selectAcquisitionPolicy(policies, { mediaKind: "series", category: "华语剧集" })?.id).toBe("series");
expect(selectAcquisitionPolicy(policies, { mediaKind: "movie", category: "动画电影" })?.id).toBe("global");
});
it.each(["movie", "series", "variety"] as const)("%s 不串用另一类型的同名分类策略", (mediaKind) => {
const scoped = [
{ id: "variety", scope: { mediaKinds: ["variety" as const], categories: { tv: ["同名分类"] } } },
{ id: "movie", scope: { categories: { movie: ["同名分类"] } } },
{ id: "series", scope: { mediaKinds: ["series" as const], categories: { tv: ["同名分类"] } } },
];
expect(selectAcquisitionPolicy(scoped, { mediaKind, category: "同名分类" })?.id).toBe(mediaKind);
});
it("同一范围按传入的最新发布顺序取第一个", () => {
const latest = { id: "latest", scope: { categories: { tv: ["动漫剧集"] } } };
const old = { id: "old", scope: { categories: { tv: ["动漫剧集"] } } };
expect(selectAcquisitionPolicy([latest, old], { mediaKind: "series", category: "动漫剧集" })).toBe(latest);
expect(selectAcquisitionPolicy([old, latest], { mediaKind: "series", category: "动漫剧集" })).toBe(old);
});
it("分类未知时跳过分类限定策略,仍可使用类型默认", () => {
expect(selectAcquisitionPolicy(policies, { mediaKind: "series", category: null })?.id).toBe("series");
});
it("类型或分类没有匹配且无默认时返回 undefined", () => {
const scoped = policies.slice(1);
expect(selectAcquisitionPolicy(scoped, { mediaKind: "movie", category: "动漫剧集" })).toBeUndefined();
expect(selectAcquisitionPolicy([policies[2]!], { mediaKind: "series", category: null })).toBeUndefined();
expect(selectAcquisitionPolicy([], { mediaKind: "variety", category: "综艺" })).toBeUndefined();
});
it("空的分类列表仍按类型默认处理", () => {
const typeDefault = { id: "type", scope: { mediaKinds: ["series" as const], categories: { movie: [], tv: [] } } };
expect(selectAcquisitionPolicy([policies[0]!, typeDefault], { mediaKind: "series", category: null })).toBe(
typeDefault,
);
});
});
describe("整季同套的规格与目标选择", () => {
it("发布组大小写、联合组顺序、HDR 顺序不产生新的版本身份", () => {
const first = {
...target,
releaseGroup: { name: "CHD@FLUX", source: "suffix" as const },
hdr: ["DV", "HDR10"] as const,
};
const second = {
...target,
releaseGroup: { name: "flux@chd", source: "known" as const },
hdr: ["HDR10", "DV"] as const,
};
expect(seasonReleaseKey({ ...first, hdr: [...first.hdr] })).toBe(
seasonReleaseKey({ ...second, hdr: [...second.hdr] }),
);
});
it.each(["resolution", "source", "videoCodec", "releaseGroup"] as const)(
"缺少 %s 时不声称属于完整成套版本",
(field) => {
expect(seasonReleaseKey({ ...target, [field]: undefined })).toBeNull();
},
);
it("来源、分辨率、编码和剪辑不同分别是不同的成套版本", () => {
for (const change of [
{ source: "BluRay" as const },
{ resolution: "2160p" as const },
{ videoCodec: "H.264" as const },
{ cut: "Extended" as const },
{ hdr: ["DV" as const] },
]) {
expect(seasonReleaseKey({ ...target, ...change })).not.toBe(seasonReleaseKey(target));
}
});
const unrestricted: SlotSpec = { name: "主版本", hard: {} };
it.each([
{
name: "来源等级优先于更高分辨率和软偏好",
spec: { ...unrestricted, soft: { groups: ["CHD"] } },
weaker: release("Show.S01E01.2160p.WEB-DL.H265-CHD"),
better: release("Show.S01E02.1080p.BluRay.H264-OTHER"),
},
{
name: "同来源时分辨率优先于软偏好",
spec: { ...unrestricted, soft: { groups: ["CHD"] } },
weaker: target,
better: release("Show.S01E02.2160p.WEB-DL.H265-OTHER"),
},
{
name: "同档规格按发布组软偏好排序",
spec: { ...unrestricted, soft: { groups: ["CHD", "OTHER"] } },
weaker: release("Show.S01E01.1080p.WEB-DL.H265-OTHER"),
better: target,
},
{
name: "同档规格按 HDR 软偏好排序",
spec: { ...unrestricted, soft: { hdr: ["DV" as const, "HDR10" as const] } },
weaker: release("Show.S01E01.1080p.WEB-DL.HDR.H265-CHD"),
better: release("Show.S01E02.1080p.WEB-DL.DV.H265-CHD"),
},
{
name: "同档规格按编码软偏好排序",
spec: { ...unrestricted, soft: { codecs: ["H.265" as const, "H.264" as const] } },
weaker: release("Show.S01E01.1080p.WEB-DL.H264-CHD"),
better: target,
},
])("$name,成套选择与候选排序一致", ({ spec, weaker, better }) => {
expect(rankForSlot(spec, [weaker, better])[0]?.candidate).toBe(better);
expect(selectSeasonRelease(spec, [weaker, better])).toBe(better);
expect(selectSeasonRelease(spec, [better, weaker])).toBe(better);
});
it("组合优先级先于来源和分辨率等级", () => {
const spec: SlotSpec = { ...unrestricted, rules: { groups: [group("来源偏好", "WEBDL > BLURAY")] } };
const blue = release("Show.S01E01.2160p.BluRay.H265-OTHER");
expect(rankForSlot(spec, [blue, target])[0]?.candidate).toBe(target);
expect(selectSeasonRelease(spec, [blue, target])).toBe(target);
});
it("规格不完整的版本不参与成套选择", () => {
const incomplete = { ...target, videoCodec: undefined, pri: 1000 };
expect(selectSeasonRelease(unrestricted, [incomplete, target])).toBe(target);
expect(selectSeasonRelease(unrestricted, [incomplete])).toBeUndefined();
});
it("同档没有偏好时目标不随数据库返回顺序反复变化", () => {
const other = release("Show.S01E02.1080p.WEB-DL.H265-OTHER");
expect(selectSeasonRelease(unrestricted, [target, other])).toBe(selectSeasonRelease(unrestricted, [other, target]));
});
});
describe("追更和洗版分别使用规则", () => {
const phased: SlotSpec = {
name: "主版本",
hard: { resolutions: ["1080p", "2160p"] },
rules: { groups: [group("追更", "WEBDL")] },
upgradeRules: { groups: [group("洗版", "BLURAY > WEBDL")] },
upgrade: { enabled: true },
};
const blue = release("Show.S01E02.1080p.BluRay.H265-CHD");
it("补齐缺集按追更规则,洗版规则不提前过滤", () => {
expect(evaluateCandidate(phased, target)).toMatchObject({ decision: "accept", pri: 100 });
expect(evaluateCandidate(phased, blue)).toMatchObject({
decision: "reject",
violations: [expect.objectContaining({ field: "rule", expected: "追更" })],
});
});
it("已有当前资产时改用洗版规则,并按洗版规则重算当前档位", () => {
expect(evaluateCandidate(phased, blue, current)).toMatchObject({ decision: "upgrade", pri: 100, fromPri: 99 });
expect(evaluateCandidate(phased, target, current)).toMatchObject({
decision: "reject",
violations: [expect.objectContaining({ code: "not_higher_priority" })],
});
});
it("洗版专用硬条件不阻止追更,但在已有资产时生效", () => {
const spec: SlotSpec = { ...phased, upgradeHard: { resolutions: ["2160p"] } };
expect(evaluateCandidate(spec, target).decision).toBe("accept");
expect(evaluateCandidate(spec, blue, current)).toMatchObject({
decision: "reject",
violations: [expect.objectContaining({ field: "resolution", code: "not_allowed" })],
});
});
it.each(["1080p", "2160p"] as const)("允许分辨率 %s 在追更和洗版阶段均可使用", (resolution) => {
const spec: SlotSpec = { name: "主版本", hard: phased.hard, upgrade: { enabled: true } };
const candidate = release(`Show.S01E02.${resolution}.BluRay.H265-CHD`);
expect(evaluateCandidate(spec, candidate).decision).toBe("accept");
expect(evaluateCandidate(spec, candidate, current).decision).toBe("upgrade");
});
it.each(["720p", "4320p"] as const)("范围外的分辨率 %s 不因更优来源或整季对齐而放行", (resolution) => {
const candidate = release(`Show.S01E02.${resolution}.BluRay.H265-CHD`);
expect(evaluateCandidate(consistent, candidate).decision).toBe("reject");
expect(evaluateCandidate(consistent, candidate, current, { seasonReleaseTarget: candidate })).toMatchObject({
decision: "reject",
violations: expect.arrayContaining([expect.objectContaining({ field: "resolution", code: "not_allowed" })]),
});
});
it("分辨率未知时保持待补充,不把未知当作允许范围", () => {
expect(evaluateCandidate(phased, { ...target, resolution: undefined })).toMatchObject({
decision: "unknown_field",
missing: ["resolution"],
});
});
it("组合条件的编码未知时,不能因 NOT 自动命中低档", () => {
const spec: SlotSpec = { name: "主版本", hard: {}, rules: { groups: [group("编码", "H265 > !H265")] } };
expect(evaluateCandidate(spec, release("Show.S01E02.1080p.WEB-DL-CHD"))).toMatchObject({
decision: "unknown_field",
missing: ["codec"],
});
});
it("高档分辨率已明确不满足时,无关的未知编码不阻止已匹配的低档", () => {
const spec: SlotSpec = {
name: "主版本",
hard: {},
rules: { groups: [group("组合档位", "4K & H265 > 1080P")] },
};
expect(evaluateCandidate(spec, release("Show.S01E02.1080p.WEB-DL-CHD"))).toMatchObject({
decision: "accept",
pri: 99,
});
});
});
describe("整季同套的同档对齐不绕过升级限制", () => {
it("同档的不同发布组可以逐集对齐到选定的整季版本", () => {
expect(evaluateCandidate(consistent, target, current, { seasonReleaseTarget: target })).toMatchObject({
decision: "upgrade",
pri: 100,
fromPri: 100,
reasons: expect.arrayContaining(["season_release_aligned"]),
});
});
it.each([
{ name: "来源", source: "BluRay" as const, resolution: "1080p" as const },
{ name: "分辨率", source: "WEB-DL" as const, resolution: "2160p" as const },
])("同档对齐不能降低当前文件的$name", ({ source, resolution }) => {
expect(
evaluateCandidate(consistent, target, { ...current, source, resolution }, { seasonReleaseTarget: target }),
).toMatchObject({ decision: "reject", violations: [expect.objectContaining({ code: "lower_tier" })] });
});
it("当前文件已经同套时不重复整理或下载", () => {
expect(evaluateCandidate(consistent, target, target, { seasonReleaseTarget: target }).decision).toBe("reject");
const normalizedCurrent = { ...target, releaseGroup: { name: "chd", source: "known" as const } };
expect(evaluateCandidate(consistent, target, normalizedCurrent, { seasonReleaseTarget: target }).decision).toBe(
"reject",
);
});
it("另一套同档资源不能替换已选定的当季目标", () => {
const other = release("Show.S01E02.1080p.WEB-DL.H265-FLUX");
expect(evaluateCandidate(consistent, other, current, { seasonReleaseTarget: target })).toMatchObject({
decision: "reject",
violations: [expect.objectContaining({ code: "different_season_release" })],
});
});
it("关闭升级时,同档对齐也不能发起替换", () => {
const spec = { ...consistent, upgrade: { ...consistent.upgrade!, enabled: false } };
expect(evaluateCandidate(spec, target, current, { seasonReleaseTarget: target })).toMatchObject({
decision: "reject",
violations: [expect.objectContaining({ code: "upgrade_disabled" })],
});
});
it("全集洗版不能因同档对齐而接受单集或播出日期资源", () => {
const spec: SlotSpec = { ...consistent, upgrade: { ...consistent.upgrade!, mode: "full_pack" } };
for (const candidate of [target, { ...target, episodes: undefined, airDate: "20261013" }]) {
expect(evaluateCandidate(spec, candidate, current, { seasonReleaseTarget: candidate })).toMatchObject({
decision: "reject",
violations: [expect.objectContaining({ code: "not_full_pack" })],
});
}
const pack = release("Show.S01.1080p.WEB-DL.H265-CHD");
expect(pack.episodes).toBeUndefined();
expect(evaluateCandidate(spec, pack, current, { seasonReleaseTarget: pack }).decision).toBe("upgrade");
});
it("不合格蓝光压制组不能因同档对齐绕过名单", () => {
const spec: SlotSpec = {
...consistent,
upgrade: { ...consistent.upgrade!, encodeGroupAllowlist: ["CHD"] },
};
const old = release("Show.S01E01.1080p.BluRay.x265-OTHER");
const unqualified = release("Show.S01E02.1080p.BluRay.x265-BADGRP");
expect(unqualified.isEncode).toBe(true);
expect(evaluateCandidate(spec, unqualified, old, { seasonReleaseTarget: unqualified })).toMatchObject({
decision: "reject",
violations: [expect.objectContaining({ code: "encode_not_qualified" })],
});
const qualified = release("Show.S01E02.1080p.BluRay.x265-chd");
expect(evaluateCandidate(spec, qualified, old, { seasonReleaseTarget: qualified }).decision).toBe("upgrade");
});
it.each([
{ field: "resolution", missing: "resolution" },
{ field: "source", missing: "source" },
{ field: "videoCodec", missing: "codec" },
{ field: "releaseGroup", missing: "group" },
] as const)("候选缺少 $field 时不执行成套替换", ({ field, missing }) => {
const candidate = { ...target, [field]: undefined };
expect(evaluateCandidate(consistent, candidate, current, { seasonReleaseTarget: target })).toMatchObject({
decision: "unknown_field",
missing: expect.arrayContaining([missing]),
});
});
it("季目标缺失或规格不完整时等待,不自行认定当前候选是目标", () => {
expect(evaluateCandidate(consistent, target, current)).toMatchObject({
decision: "unknown_field",
missing: ["current_asset"],
});
expect(
evaluateCandidate(consistent, target, current, { seasonReleaseTarget: { ...target, videoCodec: undefined } }),
).toMatchObject({ decision: "unknown_field", missing: ["current_asset"] });
});
});
@@ -194,7 +194,7 @@ describe("MP 过滤规则差分(filter-cases.json)", () => {
});
});
/** 生成/更新登记时用:UMT_FILTER_DUMP=1 deno task test 打印当前不一致 */
/** 生成/更新登记时用:UMT_FILTER_DUMP=1 bun run test 打印当前不一致 */
if (process.env.UMT_FILTER_DUMP) {
const rows: Record<string, unknown> = {};
for (const c of fx.cases) {