/** * Candidate cards are keyed only on scoreable facts + birth profile + inference. * An occupation_note that does not score must not stale the snapshot. */ export type CandidateSnapshotSource = Readonly<{ birthProfileFingerprint: string; scoreableEvidenceFingerprint: string; inferenceRevision: number; candidateSetVersion: string; scoringPolicyVersion: string; }>; export type SnapshotStaleReason = | "birth_profile_changed" | "scoreable_evidence_changed" | "candidate_set_superseded" | "inference_revision_changed" | "fingerprint_missing"; export const SNAPSHOT_STALE_COPY: Readonly> = { birth_profile_changed: "出生资料已变化,候选已失效", scoreable_evidence_changed: "可评分证据已变化,请重新比较候选", candidate_set_superseded: "已有更新的候选结果", inference_revision_changed: "候选后验已更新,请使用当前结果", fingerprint_missing: "候选快照缺少证据指纹,请重新比较候选", }; export function classifySnapshotStaleReason( snapshot: CandidateSnapshotSource | null | undefined, current: CandidateSnapshotSource, ): SnapshotStaleReason | null { if (!snapshot) return "candidate_set_superseded"; if (!snapshot.scoreableEvidenceFingerprint || !current.scoreableEvidenceFingerprint) { return "fingerprint_missing"; } if ( snapshot.birthProfileFingerprint && current.birthProfileFingerprint && snapshot.birthProfileFingerprint !== current.birthProfileFingerprint ) { return "birth_profile_changed"; } if (snapshot.scoreableEvidenceFingerprint !== current.scoreableEvidenceFingerprint) { return "scoreable_evidence_changed"; } if (snapshot.candidateSetVersion !== current.candidateSetVersion) { return "candidate_set_superseded"; } if (snapshot.inferenceRevision !== current.inferenceRevision) { return "inference_revision_changed"; } return null; } /** Cards stay current when only non-scoreable notes (occupation_note) change. */ export function scoreableSnapshotIsCurrent( snapshot: CandidateSnapshotSource | null | undefined, current: CandidateSnapshotSource, ): boolean { if (!snapshot) return false; if (!snapshot.scoreableEvidenceFingerprint || !current.scoreableEvidenceFingerprint) return false; return snapshot.scoreableEvidenceFingerprint === current.scoreableEvidenceFingerprint && snapshot.candidateSetVersion === current.candidateSetVersion && snapshot.inferenceRevision === current.inferenceRevision; } /** No stored snapshot is not stale; a stored snapshot with a missing fingerprint is. */ export function storedSnapshotIsCurrent( snapshot: CandidateSnapshotSource | null | undefined, current: CandidateSnapshotSource, ): boolean { if (!snapshot) return true; return scoreableSnapshotIsCurrent(snapshot, current); } export function snapshotIsCurrent( snapshot: CandidateSnapshotSource | null | undefined, current: CandidateSnapshotSource, ): boolean { return classifySnapshotStaleReason(snapshot, current) === null; } export function candidateSnapshotSource(input: CandidateSnapshotSource): CandidateSnapshotSource { return { birthProfileFingerprint: input.birthProfileFingerprint, scoreableEvidenceFingerprint: input.scoreableEvidenceFingerprint, inferenceRevision: input.inferenceRevision, candidateSetVersion: input.candidateSetVersion, scoringPolicyVersion: input.scoringPolicyVersion, }; }