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,106 @@
|
||||
import type {
|
||||
ExternalIdentity,
|
||||
ExternalIdentityCreate,
|
||||
IdentityResolveOutput,
|
||||
IdentityStatus,
|
||||
MediaEntity,
|
||||
ResolutionStatus,
|
||||
} from "@umt/contracts";
|
||||
import type { IdentityCandidate, IdentityResolution, RankedCandidate } from "@umt/domain";
|
||||
import { toContractEvidenceLevel, toDomainMediaType } from "./evidence.ts";
|
||||
|
||||
// 身份解析:domain.resolveIdentity(auto / ambiguous / no_match)→ contracts 的能力输出状态
|
||||
// 与 source_release.resolution_status。
|
||||
|
||||
export type DomainResolutionStatus = IdentityResolution["status"];
|
||||
export type ContractResolveStatus = IdentityResolveOutput["status"];
|
||||
|
||||
export interface ResolveStatusOptions {
|
||||
/**
|
||||
* auto 时是否可直接接受(automation 总开关/授权/人工锁都允许)。
|
||||
* false 时产出 proposed:证据足够但需要确认,进待识别池。
|
||||
*/
|
||||
autoAccept: boolean;
|
||||
}
|
||||
|
||||
const RESOLVE_STATUS: Record<DomainResolutionStatus, (o: ResolveStatusOptions) => ContractResolveStatus> = {
|
||||
auto: (o) => (o.autoAccept ? "resolved" : "proposed"),
|
||||
ambiguous: () => "ambiguous",
|
||||
no_match: () => "unidentifiable",
|
||||
};
|
||||
|
||||
export const DOMAIN_RESOLUTION_STATUSES = Object.keys(RESOLVE_STATUS) as DomainResolutionStatus[];
|
||||
|
||||
export function toContractResolveStatus(
|
||||
status: DomainResolutionStatus,
|
||||
opts: ResolveStatusOptions,
|
||||
): ContractResolveStatus {
|
||||
return RESOLVE_STATUS[status](opts);
|
||||
}
|
||||
|
||||
const RELEASE_RESOLUTION: Record<ContractResolveStatus, ResolutionStatus> = {
|
||||
resolved: "resolved",
|
||||
// 待确认的提议与歧义一样进待识别池
|
||||
proposed: "ambiguous",
|
||||
ambiguous: "ambiguous",
|
||||
unidentifiable: "unidentifiable",
|
||||
};
|
||||
|
||||
/** 能力输出状态 → source_release.resolution_status */
|
||||
export function toReleaseResolutionStatus(status: ContractResolveStatus): ResolutionStatus {
|
||||
return RELEASE_RESOLUTION[status];
|
||||
}
|
||||
|
||||
/**
|
||||
* 排名候选 → external_identity 新建输入。本地候选(provider=local)不是外部身份,返回 null;
|
||||
* 被否决的候选同样返回 null。
|
||||
*/
|
||||
export function rankedToIdentityCreate(
|
||||
r: RankedCandidate,
|
||||
target: { mediaEntityId: string } | { contentUnitId: string },
|
||||
status: IdentityStatus = "proposed",
|
||||
): ExternalIdentityCreate | null {
|
||||
const level = toContractEvidenceLevel(r.level);
|
||||
if (!level || r.candidate.provider === "local") return null;
|
||||
return {
|
||||
...target,
|
||||
provider: r.candidate.provider,
|
||||
entityType: r.candidate.entityType,
|
||||
externalId: r.candidate.externalId,
|
||||
status,
|
||||
evidenceLevel: level,
|
||||
evidence: {
|
||||
titleMatch: r.titleMatch,
|
||||
yearMatch: r.yearMatch,
|
||||
typeMatch: r.typeMatch,
|
||||
rules: r.evidence,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地作品 → domain 身份候选(本地优先召回)。externalId 用本地 UUID;
|
||||
* 已接受的外部身份作为 linkedExternalIds,用于与远端候选判同。
|
||||
* 媒体类型无法映射到 movie/tv(合集/其他)时返回 null。
|
||||
*/
|
||||
export function localIdentityCandidate(
|
||||
entity: Pick<MediaEntity, "id" | "kind" | "title" | "originalTitle" | "year">,
|
||||
aliases: readonly string[],
|
||||
identities: readonly Pick<ExternalIdentity, "provider" | "externalId" | "status">[] = [],
|
||||
): IdentityCandidate | null {
|
||||
const entityType = toDomainMediaType(entity.kind);
|
||||
if (!entityType) return null;
|
||||
const titles = [entity.title, ...(entity.originalTitle ? [entity.originalTitle] : [])];
|
||||
return {
|
||||
provider: "local",
|
||||
entityType,
|
||||
externalId: entity.id,
|
||||
titles,
|
||||
...(entity.year !== null ? { year: entity.year } : {}),
|
||||
aliases: [...aliases],
|
||||
localEntityId: entity.id,
|
||||
linkedExternalIds: identities
|
||||
.filter((i) => i.status === "accepted")
|
||||
.map((i) => ({ provider: i.provider, id: i.externalId })),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user