30d8c78d19
Missing evidence fingerprints are stale, not current. Stale snapshots keep asking discriminators when a probe exists, and offer-candidates refuses until the ledger matches. Co-authored-by: Cursor <cursoragent@cursor.com>
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
/**
|
|
* 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<Record<SnapshotStaleReason, string>> = {
|
|
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,
|
|
};
|
|
}
|