Files
UHD_Media_Tool/packages/mapping/src/slot.ts
T
rootandClaude Opus 5.5 42ecc93fa5 feat(mapping): centralize domain <-> contracts mappings (B5)
New @umt/mapping (depends on domain + contracts only): download status + pause reason,
evidence level / media kind, identity resolution status, slot status / spec columns /
evaluation, ParsedRelease <-> source_release.parsed, SiteRelease <-> source_release,
PathBinding, OrganizePlan -> organization_record / file results / organize.plan output.
Tables are Record<union, ...> so adding a value on either side fails typecheck or tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:20:30 +08:00

125 lines
4.4 KiB
TypeScript

import type {
AcquisitionSlot,
AcquisitionSlotCreate,
SlotStatus as ContractSlotStatus,
JsonObject,
} from "@umt/contracts";
import type {
SlotStatus as DomainSlotStatus,
SlotCompletionSpec,
SlotEvaluation,
SlotHardConditions,
SlotSoftPreferences,
SlotSpec,
SlotUpgradePolicy,
} from "@umt/domain";
// Slot:状态两侧取值相同(REQ §6.1),映射表只为让任一侧增删值时编译/测试失败。
// 规则 JSON 列(hard_conditions / soft_preferences / upgrade_threshold / completion / fallback)↔ domain SlotSpec。
export type { DomainSlotStatus };
const TO_CONTRACT: Record<DomainSlotStatus, ContractSlotStatus> = {
empty: "empty",
reserved: "reserved",
downloading: "downloading",
verifying: "verifying",
satisfied: "satisfied",
attention: "attention",
upgrade_pending: "upgrade_pending",
};
const TO_DOMAIN: Record<ContractSlotStatus, DomainSlotStatus> = {
empty: "empty",
reserved: "reserved",
downloading: "downloading",
verifying: "verifying",
satisfied: "satisfied",
upgrade_pending: "upgrade_pending",
attention: "attention",
};
export const DOMAIN_SLOT_STATUSES = Object.keys(TO_CONTRACT) as DomainSlotStatus[];
export const toContractSlotStatus = (s: DomainSlotStatus): ContractSlotStatus => TO_CONTRACT[s];
export const toDomainSlotStatus = (s: ContractSlotStatus): DomainSlotStatus => TO_DOMAIN[s];
type SlotRuleRow = Pick<
AcquisitionSlot,
"id" | "name" | "hardConditions" | "softPreferences" | "upgradeThreshold" | "completion" | "fallback"
>;
const isEmpty = (o: JsonObject | null | undefined) => !o || Object.keys(o).length === 0;
/**
* acquisition_slot 行 → domain SlotSpec。JSON 列按 domain 类型原样解释(写入时由 slotSpecToColumns 保证形状);
* fallback.unknownPolicy 对应 SlotSpec.unknownPolicy。
*/
export function slotSpecFromRow(row: SlotRuleRow): SlotSpec {
const unknownPolicy = row.fallback.unknownPolicy as SlotSpec["unknownPolicy"] | undefined;
return {
id: row.id,
name: row.name,
hard: row.hardConditions as SlotHardConditions,
...(unknownPolicy !== undefined ? { unknownPolicy } : {}),
...(isEmpty(row.softPreferences) ? {} : { soft: row.softPreferences as SlotSoftPreferences }),
...(isEmpty(row.upgradeThreshold) ? {} : { upgrade: row.upgradeThreshold as unknown as SlotUpgradePolicy }),
...(isEmpty(row.completion) ? {} : { completion: row.completion as unknown as SlotCompletionSpec }),
};
}
/** domain SlotSpec → acquisition_slot 规则列(Create/Update 可直接展开) */
export function slotSpecToColumns(
spec: SlotSpec,
): Pick<
AcquisitionSlotCreate,
"name" | "hardConditions" | "softPreferences" | "upgradeThreshold" | "completion" | "fallback"
> {
const json = (v: unknown): JsonObject => (v ? (JSON.parse(JSON.stringify(v)) as JsonObject) : {});
return {
name: spec.name,
hardConditions: json(spec.hard),
softPreferences: json(spec.soft),
upgradeThreshold: json(spec.upgrade),
completion: json(spec.completion),
fallback: spec.unknownPolicy === undefined ? {} : json({ unknownPolicy: spec.unknownPolicy }),
};
}
export interface ContractSlotEvaluation {
decision: SlotEvaluation["decision"];
/** 可以用这个候选生成计划(accept 或 upgrade) */
acceptable: boolean;
/** 与 DownloadPlanOutput.rejected 同形:硬条件不满足 / 字段未知的原因 */
rejected: { code: string; message: string }[];
reasons: string[];
}
/** domain 评估结果 → 能力输出/决策记录用的扁平表示 */
export function toContractSlotEvaluation(ev: SlotEvaluation): ContractSlotEvaluation {
switch (ev.decision) {
case "accept":
case "upgrade":
return { decision: ev.decision, acceptable: true, rejected: [], reasons: [...ev.reasons] };
case "reject":
return {
decision: "reject",
acceptable: false,
rejected: ev.violations.map((v) => ({
code: `${v.field}.${v.code}`,
message:
v.expected === undefined
? `${v.field}: ${v.code}`
: `${v.field}: ${v.code}(期望 ${JSON.stringify(v.expected)},实际 ${JSON.stringify(v.actual ?? null)})`,
})),
reasons: [],
};
case "unknown_field":
return {
decision: "unknown_field",
acceptable: false,
rejected: ev.missing.map((m) => ({ code: `unknown_field.${m}`, message: `字段未知,需要补充: ${m}` })),
reasons: [],
};
}
}