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:
@@ -1,11 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import {
|
||||
buildCandidateContrastPacket,
|
||||
isSameCandidateSplit,
|
||||
mentionedVargaKeysFromLedgerEvidence,
|
||||
remainingVargaSplits,
|
||||
selectDiscriminatorProbe,
|
||||
splitHashForCandidateSet,
|
||||
volunteeredDomainsFromEvidence,
|
||||
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||||
|
||||
@@ -167,3 +171,62 @@ test("occupation mention does not skip remaining D10; answered varga.d10 still d
|
||||
});
|
||||
assert.equal(answeredPacket.probes.some((item) => item.semanticKey.startsWith("varga.d10.")), false);
|
||||
});
|
||||
|
||||
test("a stale candidate-set prefix is re-minted instead of stacked", () => {
|
||||
const current = "05:00-05:07:05:00,05:06,05:07";
|
||||
const stale = "05:00-05:07:05:00,05:05,05:07";
|
||||
const key = "varga.d9.05:00|05:06/05:07";
|
||||
assert.equal(splitHashForCandidateSet(`${stale}:${key}`, key, current), `${current}:${key}`);
|
||||
assert.equal(splitHashForCandidateSet(`${current}:${key}`, key, current), `${current}:${key}`);
|
||||
assert.equal(splitHashForCandidateSet(null, key, current), `${current}:${key}`);
|
||||
// opaque engine hashes stay scoped exactly once
|
||||
assert.equal(splitHashForCandidateSet("f4cc4975", "career.2023", current), `${current}:f4cc4975`);
|
||||
assert.equal(
|
||||
splitHashForCandidateSet(`${current}:f4cc4975`, "career.2023", current),
|
||||
`${current}:f4cc4975`,
|
||||
);
|
||||
});
|
||||
|
||||
test("engine probes keep one candidate-set prefix after the set changes", () => {
|
||||
const current = "05:00-05:07:05:00,05:06,05:07";
|
||||
const stale = "05:00-05:07:05:00,05:05,05:07";
|
||||
const key = "varga.d9.05:00|05:06/05:07";
|
||||
const packet = buildCandidateContrastPacket({
|
||||
candidateSetVersion: current,
|
||||
candidateTimes: ["05:00", "05:06", "05:07"],
|
||||
engineProbes: [{
|
||||
semantic_key: key,
|
||||
candidate_split_hash: `${stale}:${key}`,
|
||||
domain: "relationship",
|
||||
user_meaning: "亲密关系里更接近下面哪一种相处方式?",
|
||||
information_gain: 1.53,
|
||||
choice_kind: "varga_style",
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:06", "05:07"] },
|
||||
{ answer_class: "no", supports: ["05:07"], conflicts: ["05:00", "05:06"] },
|
||||
],
|
||||
}],
|
||||
});
|
||||
const probe = packet.probes.find((item) => item.semanticKey === key);
|
||||
assert.ok(probe);
|
||||
assert.equal(probe.candidateSplitHash, `${current}:${key}`);
|
||||
assert.doesNotMatch(probe.candidateSplitHash, /05:05/);
|
||||
});
|
||||
|
||||
test("split identity ignores the candidate-set prefix but not the probe", () => {
|
||||
const key = "varga.d9.05:00|05:06/05:07";
|
||||
assert.equal(isSameCandidateSplit(`a:${key}`, `b:${key}`, key), true);
|
||||
assert.equal(isSameCandidateSplit(`a:${key}`, `a:varga.d10.05:00|05:06/05:07`, key), false);
|
||||
assert.equal(isSameCandidateSplit("f4cc4975", "a:f4cc4975", "career.2023"), false);
|
||||
assert.equal(isSameCandidateSplit("f4cc4975", "f4cc4975", "career.2023"), true);
|
||||
});
|
||||
|
||||
test("scoring scopes the contrast packet to the candidate set it just produced", () => {
|
||||
const source = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
|
||||
const packetCall = source.slice(
|
||||
source.indexOf("const scoredCandidateSetId = candidateSetId("),
|
||||
source.indexOf("const inference = buildCaseInferenceState("),
|
||||
);
|
||||
assert.ok(packetCall.includes("candidateSetVersion: scoredCandidateSetId"));
|
||||
assert.doesNotMatch(packetCall, /candidateSetVersion:\s*previousInferenceFromReceipt/);
|
||||
});
|
||||
|
||||
@@ -1376,3 +1376,136 @@ test("dasha existence cards lock the event and use supplied dynamic labels", ()
|
||||
assert.equal(copy.option_a, DYNAMIC_STYLE_OPTIONS[0].label);
|
||||
assert.equal(copy.option_c, DYNAMIC_STYLE_OPTIONS[2].label);
|
||||
});
|
||||
|
||||
const D9_STALE_KEY = "varga.d9.05:00|05:06/05:07";
|
||||
const CURRENT_SET_ID = "05:00-05:07:05:00,05:06,05:07";
|
||||
// The probe was minted while the candidate set still held 05:05 instead of 05:06.
|
||||
const STALE_SET_ID = "05:00-05:07:05:00,05:05,05:07";
|
||||
const D9_STYLE_OPTIONS = [
|
||||
{ sign: "天秤座", label: "和谐、美感、合作", answer_class: "yes" },
|
||||
{ sign: "天蝎座", label: "深刻、占有欲、转化", answer_class: "weak_yes" },
|
||||
{ sign: "射手座", label: "自由、哲学、冒险", answer_class: "no" },
|
||||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||||
] as const;
|
||||
|
||||
function staleSplitDossier(schemaHash: string) {
|
||||
return {
|
||||
evidence: [
|
||||
{ id: "e-career-a", status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2020-04-01", occurredTo: "2020-04-01", eventKind: "career_entry" },
|
||||
{ id: "e-career-b", status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2020-10-01", occurredTo: "2020-10-01", eventKind: "career_exit" },
|
||||
{ id: "e-rel-end", status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: "2024-08-08", eventKind: "relationship_end" },
|
||||
{ id: "e-rel-start", status: "confirmed", domain: "relationship", datePrecision: "month", occurredFrom: "2024-05-01", occurredTo: "2024-05-31", eventKind: "relationship_start" },
|
||||
],
|
||||
conversationSummary: {
|
||||
activeFocus: {
|
||||
id: FOCUS_ID,
|
||||
questionId: `probe:${D9_STALE_KEY}`,
|
||||
intent: "distinguish_candidates",
|
||||
targetDomain: "relationship",
|
||||
targetKind: null,
|
||||
expectedAnswerSchema: {
|
||||
choice: {
|
||||
prompt: "2024 年前后,相处方式更接近其中一种?",
|
||||
option_a: D9_STYLE_OPTIONS[0].label,
|
||||
option_b: D9_STYLE_OPTIONS[1].label,
|
||||
option_c: D9_STYLE_OPTIONS[2].label,
|
||||
option_d: D9_STYLE_OPTIONS[3].label,
|
||||
options: [
|
||||
{ key: "A", label: D9_STYLE_OPTIONS[0].label, answer_class: "yes" },
|
||||
{ key: "B", label: D9_STYLE_OPTIONS[1].label, answer_class: "weak_yes" },
|
||||
{ key: "C", label: D9_STYLE_OPTIONS[2].label, answer_class: "no" },
|
||||
{ key: "D", label: D9_STYLE_OPTIONS[3].label, answer_class: "unsure" },
|
||||
],
|
||||
},
|
||||
probe_id: `contrast:${D9_STALE_KEY}`,
|
||||
semantic_key: D9_STALE_KEY,
|
||||
candidate_split_hash: schemaHash,
|
||||
choice_kind: "varga_style",
|
||||
},
|
||||
},
|
||||
declinedSkippedTopics: [],
|
||||
},
|
||||
latestResult: {
|
||||
resultId: "66666666-6666-4666-8666-666666666666",
|
||||
selectionAllowed: false,
|
||||
candidates: [
|
||||
{ time: "05:00", relativeSupport: 34 },
|
||||
{ time: "05:06", relativeSupport: 33 },
|
||||
{ time: "05:07", relativeSupport: 33 },
|
||||
],
|
||||
decisionReceipt: {
|
||||
window_scan: {
|
||||
scanned: true,
|
||||
d9_candidates_differ: true,
|
||||
d9_sign_names: ["天秤座", "天蝎座", "射手座"],
|
||||
transitions: [
|
||||
{ layer: "d9", at: "05:06", from_sign: "天秤座", to_sign: "天蝎座" },
|
||||
{ layer: "d9", at: "05:07", from_sign: "天蝎座", to_sign: "射手座" },
|
||||
],
|
||||
},
|
||||
inference_state: {
|
||||
algorithm_version: "rectification-inference-v1",
|
||||
candidate_set_id: CURRENT_SET_ID,
|
||||
revision: 1,
|
||||
phase: "discrimination",
|
||||
result_status: "discriminating",
|
||||
range_start: "05:00",
|
||||
range_end: "05:07",
|
||||
candidates: [
|
||||
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 34, posterior_score: 34, probability: 0.34, status: "active", rank: 1, strong_conflict_count: 0 },
|
||||
{ id: "05:06", time: "05:06", cluster_range: ["05:06", "05:06"], prior_score: 33, posterior_score: 33, probability: 0.33, status: "active", rank: 2, strong_conflict_count: 0 },
|
||||
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 33, posterior_score: 33, probability: 0.33, status: "active", rank: 3, strong_conflict_count: 0 },
|
||||
],
|
||||
events: [
|
||||
{ id: "e-career-a", domain: "career", year: 2020, precision: "month", usage: "training" },
|
||||
{ id: "e-career-b", domain: "career", year: 2020, precision: "month", usage: "training" },
|
||||
{ id: "e-rel-end", domain: "relationship", year: 2024, precision: "day", usage: "training" },
|
||||
{ id: "e-rel-start", domain: "relationship", year: 2024, precision: "month", usage: "holdout" },
|
||||
],
|
||||
answered_probes: [],
|
||||
probes: [{
|
||||
id: `contrast:${D9_STALE_KEY}`,
|
||||
semantic_key: D9_STALE_KEY,
|
||||
// minted one scoring round earlier, so the prefix names the old set
|
||||
candidate_split_hash: `${STALE_SET_ID}:${D9_STALE_KEY}`,
|
||||
domain: "relationship",
|
||||
year: 0,
|
||||
question: "亲密关系里更接近下面哪一种相处方式?",
|
||||
candidate_ids: ["05:00", "05:06", "05:07"],
|
||||
information_gain: 1.53,
|
||||
source: "varga_contrast",
|
||||
choice_kind: "varga_style",
|
||||
style_options: D9_STYLE_OPTIONS,
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:06", "05:07"] },
|
||||
{ answer_class: "weak_yes", supports: ["05:06"], conflicts: ["05:00", "05:07"] },
|
||||
{ answer_class: "no", supports: ["05:07"], conflicts: ["05:00", "05:06"] },
|
||||
],
|
||||
}],
|
||||
rounds: [],
|
||||
entropy: 1.5,
|
||||
representative_time: "05:00",
|
||||
credible_range: ["05:00", "05:07"],
|
||||
},
|
||||
},
|
||||
},
|
||||
case: { acceptedTime: null },
|
||||
turns: [],
|
||||
};
|
||||
}
|
||||
|
||||
test("GET still renders the card when the probe hash carries an older candidate set prefix", () => {
|
||||
const card = choiceCardFromCaseDossier(
|
||||
staleSplitDossier(`${STALE_SET_ID}:${D9_STALE_KEY}`) as never,
|
||||
);
|
||||
assert.ok(card, "a stale candidate-set prefix must not silence the persisted card");
|
||||
assert.equal(card.probe_id, `contrast:${D9_STALE_KEY}`);
|
||||
assert.equal(card.question_id, `probe:${D9_STALE_KEY}`);
|
||||
});
|
||||
|
||||
test("GET keeps hiding the card when the persisted split belongs to another probe", () => {
|
||||
const card = choiceCardFromCaseDossier(
|
||||
staleSplitDossier(`${CURRENT_SET_ID}:varga.d10.05:00|05:06/05:07`) as never,
|
||||
);
|
||||
assert.equal(card, null);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user