Files
Jyotisha/frontend/src/lib/rectification-agentic/core/snapshot-source.ts
T
Jesse_Chen 30d8c78d19
Independent Staging Quality Gate / validate (push) Successful in 11m24s
Independent Staging Quality Gate / publish (push) Successful in 15m48s
fix(rectification): fail-close snapshot fingerprints without rewinding discrimination
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>
2026-08-28 18:42:18 +08:00

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,
};
}