fix(rectification): stop a stale candidate-set prefix from hiding the choice card
Independent Staging Quality Gate / validate (push) Successful in 11m31s
Independent Staging Quality Gate / publish (push) Successful in 14m41s

Scoring scoped the contrast packet to the previous round's candidate set while
minting probes for the new one, so every later read re-prefixed the stored hash
and the persisted focus schema could never match. Mint one prefix per set and
compare splits by probe identity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
This commit is contained in:
Jesse_Chen
2026-08-29 08:51:19 +00:00
parent 0044d4f358
commit 3cecb25f42
6 changed files with 276 additions and 8 deletions
@@ -667,6 +667,46 @@ function inferredContrastChoiceKind(
return "existence";
}
/**
* Split hashes are `<candidate set id>:<semantic key>` for varga contrasts and
* `<candidate set id>:<opaque engine hash>` for dated event probes. A stored
* probe can carry the set id of an earlier scoring round; re-prefixing such a
* hash would stack prefixes and make the persisted focus schema unmatchable, so
* a stale set prefix is re-minted against the current set instead.
*/
export function splitHashForCandidateSet(
rawHash: string | null | undefined,
semanticKey: string,
candidateSetVersion: string,
): string {
const scoped = `${candidateSetVersion}:${semanticKey}`;
const hash = rawHash?.trim() ?? "";
if (!hash) return scoped;
if (hash === scoped) return hash;
if (hash.endsWith(`:${semanticKey}`)) return scoped;
if (hash.startsWith(`${candidateSetVersion}:`)) return hash;
return `${candidateSetVersion}:${hash}`;
}
/**
* Two hashes describe the same split when they are equal, or when both are
* `<some candidate set id>:<the same semantic key>`. The semantic key of a
* varga contrast already encodes its candidate groups, so only the set prefix
* can differ, and a stale prefix must not hide an otherwise valid card.
*/
export function isSameCandidateSplit(
left: string | null | undefined,
right: string | null | undefined,
semanticKey: string | null | undefined,
): boolean {
const a = left?.trim() ?? "";
const b = right?.trim() ?? "";
if (a === b) return true;
const key = semanticKey?.trim() ?? "";
if (!key || !a || !b) return false;
return a.endsWith(`:${key}`) && b.endsWith(`:${key}`);
}
function probeFromEngine(
probe: EngineContrastProbe,
candidateSetVersion: string,
@@ -684,11 +724,11 @@ function probeFromEngine(
if (outcomes.length < 2) return null;
if ((probe.information_gain ?? 0) <= 0) return null;
const semanticKey = probe.semantic_key ?? `${probe.domain ?? "career"}.${probe.year ?? "contrast"}`;
const split = probe.candidate_split_hash
? (probe.candidate_split_hash.includes(candidateSetVersion)
? probe.candidate_split_hash
: `${candidateSetVersion}:${probe.candidate_split_hash}`)
: `${candidateSetVersion}:${semanticKey}`;
const split = splitHashForCandidateSet(
probe.candidate_split_hash,
semanticKey,
candidateSetVersion,
);
const question = probe.question ?? probe.user_meaning ?? "";
if (!question.trim()) return null;
const candidateIds = [...new Set(outcomes.flatMap((row) => [
@@ -63,6 +63,7 @@ import { decideRectification, type HoldoutValidationStatus } from "../core/recti
import {
datedDomainsFromEvidence,
isStructuredDiscriminator,
isSameCandidateSplit,
mentionedVargaKeysFromLedgerEvidence,
vargaLayerCovered,
vargaLayerFromSemanticKey,
@@ -1818,7 +1819,14 @@ export function projectRectificationChoiceCard(
: null;
if (input.activeFocus?.intent !== followup.intent) return null;
if (followup.semantic_key && schemaRow?.semantic_key !== followup.semantic_key) return null;
if (followup.candidate_split_hash && schemaRow?.candidate_split_hash !== followup.candidate_split_hash) return null;
if (
followup.candidate_split_hash
&& !isSameCandidateSplit(
typeof schemaRow?.candidate_split_hash === "string" ? schemaRow.candidate_split_hash : null,
followup.candidate_split_hash,
followup.semantic_key,
)
) return null;
if (
!followup.semantic_key
&& !followup.candidate_split_hash
+10 -2
View File
@@ -105,6 +105,7 @@ import {
inspectDiscriminatorProbes,
mentionedVargaKeysFromLedgerEvidence,
} from "@/lib/rectification-agentic/core/candidate-contrast-packet";
import { candidateSetId } from "@/lib/rectification-agentic/core/build-state";
import { offerSessionKinds } from "@/lib/rectification-agentic/core/decide-next-action";
import {
candidateSnapshotSource,
@@ -882,9 +883,16 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
const refinement = refinementFromDecisionReceipt(receipt);
const windowScan = windowScanFromDecisionReceipt(receipt);
// The packet must be scoped to the candidate set this round produced. Using
// the previous round's set id mints split hashes that no later read can
// match, which silently suppresses the persisted choice card.
const scoredCandidateSetId = candidateSetId(
parsed.case.candidateRange.start_time,
parsed.case.candidateRange.end_time,
score.candidates.map((item) => item.time),
);
const contrastPacket = buildCandidateContrastPacket({
candidateSetVersion: previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null)?.candidate_set_id
?? score.engineResultId,
candidateSetVersion: scoredCandidateSetId,
calculationResultId: score.engineResultId,
engineProbes: refinement.discriminating_event_probes,
vargaDifferences: [