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]>
This commit is contained in:
1 parent
da8e53bbca
commit
42ecc93fa5
16 files changed
+1282
-1
No files matched your search
@@ -0,0 +1,124 @@
|
||||
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: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user