Files
Jyotisha/frontend/tests/rectification-inference-machine.test.ts
T
Jesse_Chen d2d7542df1 fix(web): use const for unused-reassign inference ledger receipt
The staging quality gate already passed 2006 frontend tests, then ESLint failed on prefer-const in the BUG-367 test helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 10:12:08 +08:00

851 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts";
import {
answersFromEvidence,
applyAnswerToState,
applySupersedeAnswer,
buildInferenceState,
replayInferenceState,
} from "../src/lib/rectification-agentic/core/build-state.ts";
import {
composeInferenceReceipt,
} from "../src/lib/rectification-agentic/core/compose-receipt.ts";
import {
decisionStateFingerprint,
posteriorMap,
} from "../src/lib/rectification-agentic/core/decision-fingerprint.ts";
import { INFERENCE_ALGORITHM_VERSION } from "../src/lib/rectification-agentic/core/types.ts";
import { applyChoiceWithoutEvidence } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { HOLDOUT_MESSAGE_PREFIX } from "../src/lib/rectification-agentic/v9/choice-card.ts";
import { existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { clusterEquivalentCandidates } from "../src/lib/rectification-agentic/core/cluster-candidates.ts";
import { evaluateConvergence } from "../src/lib/rectification-agentic/core/convergence-evaluator.ts";
import { isDuplicateProbe } from "../src/lib/rectification-agentic/core/duplicate-probes.ts";
import { entropyFromScores } from "../src/lib/rectification-agentic/core/entropy.ts";
import { selectHighestGainProbe } from "../src/lib/rectification-agentic/core/select-probe.ts";
import { holdoutEventIds, splitHoldoutEvents } from "../src/lib/rectification-agentic/core/split-holdout.ts";
import type { ConflictProbe, InferenceCandidate, ProbeAnswer } from "../src/lib/rectification-agentic/core/types.ts";
function probe(input: {
id: string;
domain?: string;
year?: number;
gain: number;
yesSupports: readonly string[];
yesConflicts: readonly string[];
split?: string;
}): ConflictProbe {
return {
id: input.id,
semantic_key: `${input.domain ?? "career"}.${input.year ?? 2019}`,
candidate_split_hash: input.split ?? `${input.yesSupports.join(",")}|${input.yesConflicts.join(",")}`,
domain: input.domain ?? "career",
year: input.year ?? 2019,
question: "是否发生",
candidate_ids: [...input.yesSupports, ...input.yesConflicts],
expected_outcomes: [
{ answer_class: "yes", supports: input.yesSupports, conflicts: input.yesConflicts },
{ answer_class: "no", supports: input.yesConflicts, conflicts: input.yesSupports },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: input.gain,
source: "dasha_boundary",
};
}
function candidates(scores: Readonly<Record<string, number>>): InferenceCandidate[] {
return Object.entries(scores).map(([id, score], index) => ({
id,
time: id,
cluster_range: [id, id] as const,
prior_score: 10,
posterior_score: score,
probability: score,
status: "active" as const,
rank: index + 1,
strong_conflict_count: 0,
}));
}
test("an informative answer lowers entropy and cannot revive an eliminated candidate", () => {
const conflict = probe({
id: "p1",
gain: 0.3,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
});
const before = { "04:50": 10, "05:00": 10, "05:10": 10 };
const first = applyProbeOutcome(before, conflict, "yes");
assert.equal(first.kind, "informative");
assert.ok(entropyFromScores(first.scores) < entropyFromScores(before));
assert.ok(first.eliminated_ids.includes("05:10"));
const next = buildInferenceState({
range_start: "04:50",
range_end: "05:10",
candidates: [
{ id: "04:50", time: "04:50", relative_support: 10 },
{ id: "05:00", time: "05:00", relative_support: 10 },
{ id: "05:10", time: "05:10", relative_support: 10 },
],
events: [
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2019, precision: "year" },
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
{ id: "e4", domain: "family", year: 2023, precision: "year" },
],
probes: [conflict],
answered_probes: [{
probe_id: "p1",
semantic_key: conflict.semantic_key,
candidate_split_hash: conflict.candidate_split_hash,
answer_class: "no",
classified_from: "choice",
}],
});
assert.equal(next.candidates.find((item) => item.id === "05:00")?.status, "eliminated");
const revived = applyAnswerToState(next, "p1", "yes");
assert.equal(revived.candidates.find((item) => item.id === "05:00")?.status, "eliminated");
});
test("an unsure answer is low-information and the next probe cannot reuse the same split", () => {
const first = probe({
id: "p-split",
domain: "relationship",
year: 2019,
gain: 0.4,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
split: "05:00|05:10",
});
const second = probe({
id: "p-repeat",
domain: "relationship",
year: 2019,
gain: 0.5,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
split: "05:00|05:10",
});
const third = probe({
id: "p-other",
domain: "career",
year: 2022,
gain: 0.2,
yesSupports: ["04:50"],
yesConflicts: ["05:10"],
split: "04:50|05:10",
});
const applied = applyProbeOutcome({ "05:00": 10, "05:10": 10 }, first, "unsure");
assert.equal(applied.kind, "low_information");
assert.deepEqual(applied.scores, { "05:00": 10, "05:10": 10 });
const asked: ProbeAnswer[] = [{
probe_id: first.id,
semantic_key: first.semantic_key,
candidate_split_hash: first.candidate_split_hash,
answer_class: "unsure",
classified_from: "choice",
}];
assert.equal(isDuplicateProbe(second, asked), true);
assert.equal(selectHighestGainProbe([first, second, third], asked)?.id, "p-other");
});
test("max rounds is not success and equivalent minutes return a range", () => {
const clustered = clusterEquivalentCandidates([
{ id: "a", time: "04:58", score: 12 },
{ id: "b", time: "05:00", score: 12 },
{ id: "c", time: "05:01", score: 12 },
]);
assert.equal(clustered.length, 1);
assert.equal(clustered[0]?.range_start, "04:58");
assert.equal(clustered[0]?.range_end, "05:01");
const state = buildInferenceState({
range_start: "04:58",
range_end: "05:04",
candidates: [
{ id: "a", time: "04:58", relative_support: 12 },
{ id: "b", time: "05:00", relative_support: 12 },
{ id: "c", time: "05:01", relative_support: 12 },
],
events: [
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2019, precision: "year" },
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
{ id: "e4", domain: "family", year: 2023, precision: "year" },
],
probes: [],
});
assert.equal(state.result_status, "credible_range");
assert.deepEqual(state.credible_range, ["04:58", "05:01"]);
const exhausted = evaluateConvergence({
candidates: candidates({ a: 0.45, b: 0.35, c: 0.2 }).map((item, index) => ({
...item,
probability: [0.45, 0.35, 0.2][index] ?? 0,
})),
events: splitHoldoutEvents([
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2019, precision: "year" },
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
{ id: "e4", domain: "family", year: 2023, precision: "year" },
{ id: "e5", domain: "health", year: 2018, precision: "year" },
]),
probes: [probe({ id: "open", gain: 0.3, yesSupports: ["a"], yesConflicts: ["b"] })],
answered_probes: [],
rounds: [
{
round: 1,
phase: "discrimination",
probe_id: "r1",
scores_before: {},
scores_after: {},
entropy_before: 1,
entropy_after: 0.9,
eliminated_ids: [],
winner_id: "a",
kind: "informative",
},
{
round: 2,
phase: "discrimination",
probe_id: "r2",
scores_before: {},
scores_after: {},
entropy_before: 0.9,
entropy_after: 0.8,
eliminated_ids: [],
winner_id: "a",
kind: "informative",
},
],
credible_range: null,
max_rounds: 2,
});
assert.equal(exhausted.result_status, "max_rounds_reached");
assert.equal(exhausted.converged, false);
});
test("holdout events stay out of training and a winner must stay stable for two rounds", () => {
const events = splitHoldoutEvents([
{ id: "edu", domain: "education", year: 2016, precision: "month" },
{ id: "job", domain: "career", year: 2019, precision: "year" },
{ id: "love", domain: "relationship", year: 2021, precision: "year" },
{ id: "home", domain: "relocation", year: 2023, precision: "day" },
{ id: "health", domain: "health", year: 2018, precision: "year" },
]);
assert.equal(holdoutEventIds(events).size, 1);
assert.equal(events.filter((item) => item.usage === "training").length, 4);
const oneRound = evaluateConvergence({
candidates: [
{ ...candidates({ "05:00": 12 })[0]!, probability: 0.8, posterior_score: 12 },
{ ...candidates({ "05:10": 4 })[0]!, id: "05:10", time: "05:10", probability: 0.2, posterior_score: 4 },
],
events,
probes: [],
answered_probes: [],
rounds: [{
round: 1,
phase: "discrimination",
probe_id: "p",
scores_before: {},
scores_after: {},
entropy_before: 1,
entropy_after: 0.4,
eliminated_ids: [],
winner_id: "05:00",
kind: "informative",
}],
credible_range: null,
});
assert.equal(oneRound.converged, false);
const twoRounds = evaluateConvergence({
candidates: [
{
id: "05:00",
time: "05:00",
cluster_range: ["05:00", "05:00"],
prior_score: 8,
posterior_score: 14,
probability: 0.82,
status: "active",
rank: 1,
strong_conflict_count: 0,
},
{
id: "05:10",
time: "05:10",
cluster_range: ["05:10", "05:10"],
prior_score: 8,
posterior_score: 4,
probability: 0.18,
status: "active",
rank: 2,
strong_conflict_count: 0,
},
],
events,
probes: [],
answered_probes: [],
rounds: [
{
round: 1,
phase: "discrimination",
probe_id: "p1",
scores_before: {},
scores_after: {},
entropy_before: 1,
entropy_after: 0.5,
eliminated_ids: [],
winner_id: "05:00",
kind: "informative",
},
{
round: 2,
phase: "discrimination",
probe_id: "p2",
scores_before: {},
scores_after: {},
entropy_before: 0.5,
entropy_after: 0.3,
eliminated_ids: [],
winner_id: "05:00",
kind: "informative",
},
],
credible_range: null,
});
assert.equal(twoRounds.converged, true);
assert.equal(twoRounds.result_status, "converged");
const matching = answersFromEvidence(
[probe({ id: "p-job", domain: "career", year: 2019, gain: 0.2, yesSupports: ["05:00"], yesConflicts: ["05:10"] })],
[{ id: "job", domain: "career", year: 2019, precision: "year" }],
);
assert.equal(matching[0]?.classified_from, "evidence");
});
test("C without new evidence updates the posterior immediately and D only marks the split asked", () => {
const conflict = probe({
id: "p-cd",
domain: "career",
year: 2019,
gain: 0.4,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
split: "05:00|05:10",
});
const state = buildInferenceState({
range_start: "04:50",
range_end: "05:10",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 10 },
{ id: "05:10", time: "05:10", relative_support: 10 },
],
events: [
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2018, precision: "year" },
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
{ id: "e4", domain: "family", year: 2023, precision: "year" },
],
probes: [conflict],
});
const denied = applyChoiceWithoutEvidence(state, {
choiceKey: "C",
status: "declined",
schema: { choice: { prompt: "2019 年前后有没有入职或职责加重?" }, semantic_key: conflict.semantic_key },
});
assert.equal(denied.applied, true);
assert.equal(denied.answerClass, "no");
assert.equal(denied.state.revision, state.revision + 1);
assert.notDeepEqual(posteriorMap(denied.state.candidates), posteriorMap(state.candidates));
assert.equal(denied.state.candidates.find((item) => item.id === "05:00")?.status, "eliminated");
assert.ok(denied.state.entropy < state.entropy);
assert.equal(denied.state.answered_probes.some((item) => item.semantic_key === conflict.semantic_key), true);
const unsure = applyChoiceWithoutEvidence(state, {
choiceKey: "D",
status: "skipped",
userMessage: "D. 不记得 / 不确定",
schema: { choice: { prompt: "2019 年前后有没有入职或职责加重?" }, semantic_key: conflict.semantic_key },
});
assert.equal(unsure.applied, true);
assert.equal(unsure.answerClass, "unsure");
assert.deepEqual(
unsure.state.candidates.map((item) => item.posterior_score),
state.candidates.map((item) => item.posterior_score),
);
assert.equal(selectHighestGainProbe([conflict, probe({
id: "p-other",
domain: "relationship",
year: 2021,
gain: 0.2,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
split: "05:00|2021",
})], unsure.state.answered_probes)?.id, "p-other");
});
test("holdout and collection declines do not write a probe answer", () => {
const conflict = probe({
id: "p-holdout",
gain: 0.3,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
});
const state = buildInferenceState({
range_start: "04:50",
range_end: "05:10",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 10 },
{ id: "05:10", time: "05:10", relative_support: 10 },
],
events: [
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2019, precision: "year" },
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
{ id: "e4", domain: "family", year: 2023, precision: "year" },
],
probes: [conflict],
});
const holdout = applyChoiceWithoutEvidence(state, {
choiceKey: "C",
userMessage: `${HOLDOUT_MESSAGE_PREFIX}C. 没有明显发生`,
schema: { choice: { prompt: "盘外核对" }, scoring: false },
questionId: "relatives:family_event:holdout",
});
assert.equal(holdout.reason, "holdout");
assert.equal(holdout.state.answered_probes.length, state.answered_probes.length);
const collection = applyChoiceWithoutEvidence(state, {
status: "declined",
schema: { required: ["year"] },
});
assert.equal(collection.reason, "no_choice");
});
test("structured A/yes from a choice card moves the posterior", () => {
const conflict = probe({
id: "p-a",
domain: "education",
year: 2016,
gain: 0.4,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
});
const state = buildInferenceState({
range_start: "04:50",
range_end: "05:10",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 10 },
{ id: "05:10", time: "05:10", relative_support: 10 },
],
events: [
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2018, precision: "year" },
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
{ id: "e4", domain: "family", year: 2023, precision: "year" },
],
probes: [conflict],
});
const after = applyChoiceWithoutEvidence(state, {
choiceKey: "A",
schema: { probe_id: conflict.id, semantic_key: conflict.semantic_key },
});
assert.equal(after.applied, true);
assert.equal(after.answerClass, "yes");
assert.equal(after.state.answered_probes.at(-1)?.classified_from, "choice");
assert.notEqual(
after.state.candidates.find((item) => item.id === "05:00")?.posterior_score,
state.candidates.find((item) => item.id === "05:00")?.posterior_score,
);
});
test("choice action receipts are unique per case_id and action_id and close focus in the same SQL function", () => {
const migration = readFileSync(
fileURLToPath(new URL("../supabase/migrations/20260824020000_rectification_choice_action.sql", import.meta.url)),
"utf8",
);
assert.match(migration, /create table if not exists public\.agentic_rectification_choice_actions \(/);
assert.match(migration, /unique \(case_id, action_id\)/);
assert.match(migration, /apply_agentic_rectification_choice_action\(/);
const applySql = migration.slice(
migration.indexOf("create or replace function public.apply_agentic_rectification_choice_action("),
);
assert.match(applySql, /append_agentic_rectification_inference_transition\(/);
assert.match(applySql, /resolve_agentic_rectification_conversation_focus\(/);
const appendAt = applySql.indexOf("append_agentic_rectification_inference_transition(");
const resolveAt = applySql.indexOf("resolve_agentic_rectification_conversation_focus(");
const returnAt = applySql.indexOf("return v_receipt;");
assert.ok(appendAt >= 0 && resolveAt > appendAt && returnAt > resolveAt);
assert.equal(
existsSync(new URL("../db/migrations/20260824020000_rectification_choice_action.sql", import.meta.url)),
false,
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
);
});
test("choice answers append an inference transition instead of patching the candidate cache", () => {
const migration = readFileSync(
fileURLToPath(new URL("../supabase/migrations/20260824010000_rectification_inference_transition_ledger.sql", import.meta.url)),
"utf8",
);
assert.match(migration, /create table if not exists public\.agentic_rectification_inference_transitions \(/);
assert.match(migration, /create or replace function public\.append_agentic_rectification_inference_transition\(/);
assert.match(migration, /agentic_rectification_revision_conflict/);
assert.match(migration, /agentic_rectification_stale_probe/);
assert.match(migration, /idempotency_key/);
assert.match(migration, /raise exception 'agentic_rectification_inference_patch_retired'/);
const appendSql = migration.slice(
migration.indexOf("create or replace function public.append_agentic_rectification_inference_transition("),
migration.indexOf("create or replace function public.get_agentic_rectification_latest_inference_transition("),
);
assert.doesNotMatch(appendSql, /update public\.agentic_rectification_results/);
assert.match(
migration,
/compose_agentic_rectification_decision_receipt\(p_case_id, v_cached\.id, v_cached\.decision_receipt\)/,
);
assert.match(
migration,
/compose_agentic_rectification_decision_receipt\(p_case_id, v_result_id, v_saved_decision_receipt\)/,
);
assert.match(
migration,
/compose_agentic_rectification_decision_receipt\(v_case\.id, v_result\.id, v_result\.decision_receipt\)/,
);
assert.doesNotMatch(migration, /and decision_state_fingerprint = /);
assert.equal(
existsSync(new URL("../db/migrations/20260824010000_rectification_inference_transition_ledger.sql", import.meta.url)),
false,
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
);
const retired = readFileSync(
fileURLToPath(new URL("../supabase/migrations/20260823020000_rectification_inference_choice_write.sql", import.meta.url)),
"utf8",
);
assert.match(retired, /patch_agentic_rectification_inference_state/);
});
function fingerprintOf(
caseId: string,
evidenceFp: string,
state: { candidate_set_id: string; revision: number; answered_probes: readonly { probe_id: string }[] },
): string {
return decisionStateFingerprint({
caseId,
evidenceLedgerFingerprint: evidenceFp,
candidateSetId: state.candidate_set_id,
inferenceRevision: state.revision,
answeredProbeIds: state.answered_probes.map((item) => item.probe_id),
scoringPolicyVersion: INFERENCE_ALGORITHM_VERSION,
});
}
type LedgerRow = {
revision: number;
probeId: string;
answerClass: string;
idempotencyKey: string;
inferenceState: ReturnType<typeof buildInferenceState>;
fingerprint: string;
};
function createLedger(seed: ReturnType<typeof buildInferenceState>) {
const rows: LedgerRow[] = [];
const evidenceFp = "e".repeat(64);
const caseId = "case-1";
const engineReceipt: Record<string, unknown> = { inference_state: seed };
return {
evidenceFp,
append(input: {
expectedRevision: number;
probeId: string;
openProbeId: string;
answerClass: "yes" | "weak_yes" | "no" | "unsure";
idempotencyKey: string;
apply: () => ReturnType<typeof buildInferenceState>;
}) {
const existing = rows.find((row) => row.idempotencyKey === input.idempotencyKey);
if (existing) {
return { idempotent: true, row: existing, receipt: composeInferenceReceipt(engineReceipt, {
resultId: "result-1",
revision: existing.revision,
probeId: existing.probeId,
reason: "choice",
decisionStateFingerprint: existing.fingerprint,
inferenceState: existing.inferenceState,
posteriorBefore: {},
posteriorAfter: posteriorMap(existing.inferenceState.candidates),
scoreDeltas: {},
}, "result-1") };
}
const current = rows.at(-1)?.revision ?? seed.revision;
if (input.expectedRevision !== current) {
const error = new Error("agentic_rectification_revision_conflict");
throw error;
}
if (input.probeId !== input.openProbeId) {
throw new Error("agentic_rectification_stale_probe");
}
const next = input.apply();
const fingerprint = fingerprintOf(caseId, evidenceFp, next);
const row: LedgerRow = {
revision: next.revision,
probeId: input.probeId,
answerClass: input.answerClass,
idempotencyKey: input.idempotencyKey,
inferenceState: next,
fingerprint,
};
rows.push(row);
return {
idempotent: false,
row,
receipt: composeInferenceReceipt(engineReceipt, {
resultId: "result-1",
revision: row.revision,
probeId: row.probeId,
reason: "choice",
decisionStateFingerprint: fingerprint,
inferenceState: next,
posteriorBefore: {},
posteriorAfter: posteriorMap(next.candidates),
scoreDeltas: {},
}, "result-1"),
};
},
reread() {
const latest = rows.at(-1);
if (!latest) return composeInferenceReceipt(engineReceipt, null, "result-1");
return composeInferenceReceipt(engineReceipt, {
resultId: "result-1",
revision: latest.revision,
probeId: latest.probeId,
reason: "choice",
decisionStateFingerprint: latest.fingerprint,
inferenceState: latest.inferenceState,
posteriorBefore: {},
posteriorAfter: posteriorMap(latest.inferenceState.candidates),
scoreDeltas: {},
}, "result-1");
},
rows,
};
}
test("evidence fingerprint can stay put while decision-state fingerprint and posterior change", () => {
const conflict = probe({
id: "p-cd",
domain: "career",
year: 2019,
gain: 0.4,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
});
const state = buildInferenceState({
range_start: "04:50",
range_end: "05:10",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 10 },
{ id: "05:10", time: "05:10", relative_support: 10 },
],
events: [
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2018, precision: "year" },
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
{ id: "e4", domain: "family", year: 2023, precision: "year" },
],
probes: [conflict],
});
const after = applyChoiceWithoutEvidence(state, {
choiceKey: "C",
schema: { probe_id: conflict.id, semantic_key: conflict.semantic_key },
});
assert.equal(after.applied, true);
assert.equal(after.state.revision, state.revision + 1);
const evidenceFp = "e".repeat(64);
const beforeFp = fingerprintOf("case-1", evidenceFp, state);
const afterFp = fingerprintOf("case-1", evidenceFp, after.state);
assert.equal(evidenceFp, "e".repeat(64));
assert.notEqual(afterFp, beforeFp);
const staleReceipt = { inference_state: state, display_allowed: true };
const composed = composeInferenceReceipt(staleReceipt, {
resultId: "result-1",
revision: after.state.revision,
probeId: conflict.id,
reason: "choice",
decisionStateFingerprint: afterFp,
inferenceState: after.state,
posteriorBefore: posteriorMap(state.candidates),
posteriorAfter: posteriorMap(after.state.candidates),
scoreDeltas: {},
}, "result-1");
assert.notDeepEqual(
(composed.inference_state as { candidates: unknown }).candidates,
(staleReceipt.inference_state as { candidates: unknown }).candidates,
);
assert.equal(composed.decision_state_fingerprint, afterFp);
});
test("duplicate D is idempotent, C then D supersedes, stale probes and stale revisions are rejected, replay matches", () => {
const conflict = probe({
id: "p-cd",
domain: "career",
year: 2019,
gain: 0.4,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
});
const other = probe({
id: "p-old",
domain: "relationship",
year: 2021,
gain: 0.2,
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
split: "05:00|2021",
});
const state = buildInferenceState({
range_start: "04:50",
range_end: "05:10",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 10 },
{ id: "05:10", time: "05:10", relative_support: 10 },
],
events: [
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2018, precision: "year" },
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
{ id: "e4", domain: "family", year: 2023, precision: "year" },
],
probes: [conflict, other],
});
const ledger = createLedger(state);
const firstD = applyChoiceWithoutEvidence(state, {
choiceKey: "D",
schema: { probe_id: conflict.id, semantic_key: conflict.semantic_key },
});
assert.equal(firstD.applied, true);
const stale = applyChoiceWithoutEvidence(state, {
choiceKey: "D",
schema: { probe_id: other.id, semantic_key: other.semantic_key },
});
assert.equal(stale.reason, "stale_probe");
assert.deepEqual(posteriorMap(stale.state.candidates), posteriorMap(state.candidates));
assert.throws(
() => ledger.append({
expectedRevision: state.revision,
probeId: other.id,
openProbeId: conflict.id,
answerClass: "unsure",
idempotencyKey: `choice:${other.id}:unsure`,
apply: () => firstD.state,
}),
/stale_probe/,
);
const persisted = ledger.append({
expectedRevision: state.revision,
probeId: conflict.id,
openProbeId: conflict.id,
answerClass: "unsure",
idempotencyKey: `choice:${conflict.id}:unsure`,
apply: () => firstD.state,
});
assert.equal(persisted.idempotent, false);
assert.equal(persisted.row.revision, state.revision + 1);
const again = ledger.append({
expectedRevision: state.revision,
probeId: conflict.id,
openProbeId: conflict.id,
answerClass: "unsure",
idempotencyKey: `choice:${conflict.id}:unsure`,
apply: () => firstD.state,
});
assert.equal(again.idempotent, true);
assert.equal(again.row.revision, persisted.row.revision);
assert.equal(ledger.rows.length, 1);
const afterC = applyChoiceWithoutEvidence(state, {
choiceKey: "C",
schema: { probe_id: conflict.id, semantic_key: conflict.semantic_key },
});
const superseded = applyChoiceWithoutEvidence(afterC.state, {
choiceKey: "D",
schema: { probe_id: conflict.id, semantic_key: conflict.semantic_key },
});
assert.equal(superseded.reason, "superseded");
assert.equal(superseded.state.revision, afterC.state.revision + 1);
assert.equal(superseded.state.answered_probes.filter((item) => item.probe_id === conflict.id).length, 1);
assert.equal(superseded.state.answered_probes[0]?.answer_class, "unsure");
const keptC = applySupersedeAnswer(afterC.state, conflict.id, "unsure");
assert.equal(keptC.revision, superseded.state.revision);
const corrections = createLedger(state);
const writtenC = corrections.append({
expectedRevision: state.revision,
probeId: conflict.id,
openProbeId: conflict.id,
answerClass: "no",
idempotencyKey: `choice:${conflict.id}:no`,
apply: () => afterC.state,
});
const writtenD = corrections.append({
expectedRevision: afterC.state.revision,
probeId: conflict.id,
openProbeId: conflict.id,
answerClass: "unsure",
idempotencyKey: `supersede:${conflict.id}:unsure`,
apply: () => superseded.state,
});
assert.equal(writtenC.idempotent, false);
assert.equal(writtenD.idempotent, false);
assert.equal(writtenD.row.revision, writtenC.row.revision + 1);
assert.equal(corrections.rows.length, 2);
assert.equal(corrections.rows[0]?.answerClass, "no");
assert.equal(corrections.rows[1]?.answerClass, "unsure");
assert.throws(
() => ledger.append({
expectedRevision: state.revision,
probeId: conflict.id,
openProbeId: conflict.id,
answerClass: "no",
idempotencyKey: `choice:${conflict.id}:no`,
apply: () => afterC.state,
}),
/revision_conflict/,
);
const reread = ledger.reread();
assert.deepEqual(
posteriorMap((reread.inference_state as typeof firstD.state).candidates),
posteriorMap(firstD.state.candidates),
);
const replayed = replayInferenceState(state, firstD.state.answered_probes);
assert.deepEqual(posteriorMap(replayed.candidates), posteriorMap(firstD.state.candidates));
assert.equal(replayed.revision, firstD.state.revision);
const rescored = buildInferenceState({
range_start: "04:50",
range_end: "05:10",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 12 },
{ id: "05:10", time: "05:10", relative_support: 8 },
{ id: "05:04", time: "05:04", relative_support: 9 },
],
events: [
{ id: "e1", domain: "education", year: 2016, precision: "month" },
{ id: "e2", domain: "career", year: 2018, precision: "year" },
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
{ id: "e4", domain: "family", year: 2023, precision: "year" },
],
probes: [conflict, other],
previous: firstD.state,
});
assert.notEqual(rescored.candidate_set_id, firstD.state.candidate_set_id);
assert.equal(rescored.revision, firstD.state.revision);
assert.equal(
rescored.answered_probes.some((item) => item.probe_id === conflict.id && item.answer_class === "unsure"),
true,
);
});