Files
Jyotisha/frontend/tests/rectification-inference-machine.test.ts
T
Jesse_Chen 909de8b884
Independent Staging Quality Gate / validate (push) Failing after 4m0s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(web): persist rectification C/D answers on an append-only inference ledger
Engine result rows stay immutable. Choice answers append transitions, and reads overlay the latest revision instead of patching the cached receipt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 08:22:01 +08:00

790 lines
28 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("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";
let 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,
);
});