Files
Jyotisha/frontend/tests/rectification-inference-machine.test.ts
T
Jesse_Chen f5e73ef326
Independent Staging Quality Gate / validate (push) Failing after 22m30s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(web): rank remaining-minute probes by split and match choice kind
Choice cards used a hardcoded domain menu and always asked existence.
Rank scoring layers by remaining-minute entropy, keep finance and health
volunteer-only, and ask D9/D10 style or exam quality so taps match outcomes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 20:44:55 +08:00

972 lines
35 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("discriminating result does not stay in event_collection", () => {
const state = buildInferenceState({
range_start: "04:50",
range_end: "05:10",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 34 },
{ id: "05:03", time: "05:03", relative_support: 33 },
{ id: "05:04", time: "05:04", relative_support: 33 },
],
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" },
{ id: "e5", domain: "relocation", year: 2017, precision: "year" },
],
probes: [probe({ id: "open", gain: 0.3, yesSupports: ["05:00"], yesConflicts: ["05:03"] })],
phase: "event_collection",
});
assert.equal(state.result_status, "discriminating");
assert.equal(state.phase, "discrimination");
});
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");
const quality = {
...probe({ id: "p-quality", domain: "education", year: 2016, gain: 0, yesSupports: ["05:00"], yesConflicts: ["05:10"] }),
source: "known_event_quality",
};
const skippedQuality = answersFromEvidence(
[quality],
[{ id: "enroll", domain: "education", year: 2016, precision: "year" }],
);
assert.equal(skippedQuality.length, 0);
});
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("A/B/C/D on a remaining-minute contrast probe moves the posterior", () => {
const contrast: ConflictProbe = {
id: "contrast:varga.d24.05:00/05:06|05:07",
semantic_key: "varga.d24.05:00/05:06|05:07",
candidate_split_hash: "varga.d24.05:00/05:06|05:07",
domain: "education",
year: 0,
question: "学业盘还分得开",
candidate_ids: ["05:00", "05:06", "05:07"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:06", "05:07"] },
{ answer_class: "no", supports: ["05:06", "05:07"], conflicts: ["05:00"] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: 0.16,
source: "varga_contrast",
};
const state = buildInferenceState({
range_start: "04:45",
range_end: "05:15",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 34 },
{ id: "05:06", time: "05:06", relative_support: 33 },
{ id: "05:07", time: "05:07", relative_support: 33 },
],
events: [{ id: "e1", domain: "education", year: 2016, precision: "year" }],
probes: [contrast],
});
const before = posteriorMap(state.candidates);
const applied = applyChoiceWithoutEvidence(state, {
choiceKey: "C",
schema: {
choice: { prompt: "那次考试有没有发挥失常?" },
probe_id: contrast.id,
semantic_key: contrast.semantic_key,
},
});
assert.equal(applied.applied, true);
assert.equal(applied.answerClass, "no");
assert.notDeepEqual(posteriorMap(applied.state.candidates), before);
assert.ok((applied.state.candidates.find((item) => item.time === "05:06")?.posterior_score ?? 0)
> (applied.state.candidates.find((item) => item.time === "05:00")?.posterior_score ?? 0));
});
test("two-way style C 都不像 does not promote the other minute group", () => {
const contrast = {
id: "contrast:varga.d9.05:00/05:04",
semantic_key: "varga.d9.05:00/05:04",
candidate_split_hash: "varga.d9.05:00/05:04",
domain: "relationship",
year: 0,
question: "这段关系更接近哪一种相处?",
candidate_ids: ["05:00", "05:04"],
expected_outcomes: [
{ answer_class: "yes" as const, supports: ["05:00"], conflicts: ["05:04"] },
{ answer_class: "weak_yes" as const, supports: ["05:04"], conflicts: ["05:00"] },
{ answer_class: "unsure" as const, supports: [] as string[], conflicts: [] as string[] },
],
information_gain: 1,
source: "varga_contrast",
};
const state = buildInferenceState({
range_start: "04:45",
range_end: "05:15",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 34 },
{ id: "05:04", time: "05:04", relative_support: 33 },
],
events: [{ id: "e1", domain: "relationship", year: 2024, precision: "year" }],
probes: [contrast],
});
const before = posteriorMap(state.candidates);
const applied = applyChoiceWithoutEvidence(state, {
choiceKey: "C",
schema: {
choice: {
prompt: "这段关系更接近哪一种相处?",
option_c: "两边都不像",
},
choice_kind: "varga_style",
probe_id: contrast.id,
semantic_key: contrast.semantic_key,
},
});
assert.equal(applied.applied, true);
assert.equal(applied.answerClass, "unsure");
assert.deepEqual(posteriorMap(applied.state.candidates), before);
});
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,
);
});