fix(rectification): stop a stale candidate-set prefix from hiding the choice card
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:
@@ -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
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user