Files
confidence-engine/tests/experimental/report-epistemic-ab-input-control.test.js
T
robbondandClaude Code d387b66c03 test(confidence-engine): clarify controlled report experiment boundary
Retained deterministic input-control test proving that A/B apparatus
correctly holds supportedEvidence, findings, plausibleInterpretations,
scenarioState, and providerConfig constant while only varying unresolved
question visibility. Added buildOverviewSynthesisPrompt identity proof
that full vs filtered graph produce identical prompt text.

Corrected handoff semantics: deterministic input filtering does NOT prove
model output behaviour; live two-call semantic comparison remains required.
Preserved prior imperfect live evidence with correct qualification about
uncontrolled plausibleInterpretations divergence.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 12:30:36 +01:00

176 lines
9.0 KiB
JavaScript

/**
* Deterministic control of report epistemic comparison inputs.
*
* Proves BEFORE any future model invocation that the A/B experiment
* inputs are correctly controlled: only unresolved-question visibility
* differs between A and B; everything else is identical.
*
* No live model calls. No mocking. Pure input-contract verification.
*/
import { describe, it, expect } from "vitest";
import fixture from "../fixtures/progressed-investigation-manufacturing.json";
const FIXED_PLAUSIBLE =
"The rise in absolute complaint counts is likely an artifact of both the new CRM tagging system broadening what gets recorded as a complaint, combined with a proportional increase from higher production volume rather than a genuine deterioration in product quality.";
function buildABInputs(fix, includeUQ) {
const sg = fix.situationGraph;
const se = sg.nodes.filter(n => n.status === "known" || n.status === "supported").map(n => ({id: n.id, kind: n.kind, status: n.status, label: n.label}));
const fi = fix.findings.filter(f2 => f2.status === "provisional" && f2.userDisposition === null).map(f3 => ({id: f3.id, proposition: f3.proposition}));
const pi = [FIXED_PLAUSIBLE];
const uq = sg.nodes.filter(n => n.kind === "unknown" && n.status === "unknown").map(n => ({id: n.id, label: n.label, description: n.description}));
const ss = {centralStatement: sg.centralStatement, activeUnknownNodeId: sg.activeUnknownNodeId, resolvedNodeIds: [...sg.resolvedNodeIds], nodeCount: sg.nodes.length, edgeCount: sg.edges.length};
const pc = {provider: "ollama", model: process.env.OLLAMA_MODEL || "configured-model"};
return {supportedEvidence: se, findings: fi, plausibleInterpretations: pi, unresolvedQuestions: includeUQ ? [...uq] : [], scenarioState: ss, providerConfig: pc};
}
function deepEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== typeof b) return false;
if (Array.isArray(a)) { if (!Array.isArray(b) || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false; return true; }
if (typeof a === "object") { const kA = Object.keys(a).sort(), kB = Object.keys(b).sort(); if (kA.length !== kB.length) return false; for (let i = 0; i < kA.length; i++) if (kA[i] !== kB[i] || !deepEqual(a[kA[i]], b[kB[i]])) return false; return true; }
return false;
}
describe("Controlled A/B report epistemic comparison inputs", () => {
it("builds canonical input objects for both A and B", () => {
const a = buildABInputs(fixture, false);
const b = buildABInputs(fixture, true);
["supportedEvidence","findings","plausibleInterpretations","unresolvedQuestions","scenarioState","providerConfig"].forEach(k => { expect(a).toHaveProperty(k); expect(b).toHaveProperty(k); });
});
it("A and B have identical admitted evidence", () => {
const a = buildABInputs(fixture, false); const b = buildABInputs(fixture, true);
expect(deepEqual(a.supportedEvidence, b.supportedEvidence)).toBe(true);
});
it("A and B have identical eligible Findings", () => {
const a = buildABInputs(fixture, false); const b = buildABInputs(fixture, true);
expect(deepEqual(a.findings, b.findings)).toBe(true);
});
it("A and B have identical fixed plausibleInterpretations", () => {
const a = buildABInputs(fixture, false); const b = buildABInputs(fixture, true);
expect(deepEqual(a.plausibleInterpretations, b.plausibleInterpretations)).toBe(true);
});
it("fixed plausible interpretation matches canonical value exactly", () => {
const a = buildABInputs(fixture, false);
expect(a.plausibleInterpretations).toHaveLength(1);
expect(a.plausibleInterpretations[0]).toBe(FIXED_PLAUSIBLE);
});
it("A and B reference the same progressed investigation state", () => {
const a = buildABInputs(fixture, false); const b = buildABInputs(fixture, true);
expect(deepEqual(a.scenarioState, b.scenarioState)).toBe(true);
});
it("provider/model configuration is identical for A and B", () => {
const a = buildABInputs(fixture, false); const b = buildABInputs(fixture, true);
expect(a.providerConfig.provider).toBe(b.providerConfig.provider);
expect(a.providerConfig.model).toBe(b.providerConfig.model);
});
it("A does not expose unresolved Open Questions (hidden)", () => {
const a = buildABInputs(fixture, false);
expect(a.unresolvedQuestions).toHaveLength(0);
});
it("B exposes exactly the canonical unresolved Open Questions", () => {
const b = buildABInputs(fixture, true);
const fu = fixture.situationGraph.nodes.filter(n => n.kind === "unknown" && n.status === "unknown");
expect(b.unresolvedQuestions).toHaveLength(fu.length);
for (const node of fu) { const found = b.unresolvedQuestions.find(uq => uq.id === node.id); expect(found).toBeDefined(); expect(found.label).toBe(node.label); expect(found.description).toBe(node.description); }
});
it("only unresolved visibility differs between A and B", () => {
const a = buildABInputs(fixture, false); const b = buildABInputs(fixture, true);
expect(deepEqual(a.supportedEvidence, b.supportedEvidence)).toBe(true);
expect(deepEqual(a.findings, b.findings)).toBe(true);
expect(deepEqual(a.plausibleInterpretations, b.plausibleInterpretations)).toBe(true);
expect(deepEqual(a.scenarioState, b.scenarioState)).toBe(true);
expect(deepEqual(a.providerConfig, b.providerConfig)).toBe(true);
expect(a.unresolvedQuestions).not.toEqual(b.unresolvedQuestions);
});
it("fixed plausible interpretation is not admitted evidence", () => {
const a = buildABInputs(fixture, false);
for (const e of a.supportedEvidence) { expect(e.label).not.toContain(FIXED_PLAUSIBLE); expect(["known","supported"]).toContain(e.status); }
});
it("unresolved questions are not admitted evidence", () => {
const b = buildABInputs(fixture, true);
for (const uq of b.unresolvedQuestions) { const fn = fixture.situationGraph.nodes.find(n => n.id === uq.id); expect(fn.status).toBe("unknown"); expect(fn.kind).toBe("unknown"); }
});
it("unresolved questions are not plausibleInterpretations", () => {
const b = buildABInputs(fixture, true);
for (const uq of b.unresolvedQuestions) { expect(b.plausibleInterpretations).not.toContain(uq.label); expect(b.plausibleInterpretations[0]).toBe(FIXED_PLAUSIBLE); }
});
it("no fixture node conflates unresolved and evidence categories", () => {
for (const node of fixture.situationGraph.nodes) {
if (node.kind === "unknown" && node.status === "unknown") expect(["known","supported"]).not.toContain(node.status);
else if (["known","supported"].includes(node.status)) expect(node.kind).not.toBe("unknown");
}
});
it("inputs are constructed purely from fixture without provider or model dependency", () => {
const a = buildABInputs(fixture, false); const b = buildABInputs(fixture, true);
expect(typeof a.supportedEvidence).toBe("object");
expect(typeof a.findings).toBe("object");
expect(typeof a.plausibleInterpretations).toBe("object");
expect(typeof a.unresolvedQuestions).toBe("object");
});
// ── Prompt-builder identity proof (A/B produce identical SECTION A) ────
it("buildOverviewSynthesisPrompt produces identical prompt text when unknown nodes are present vs removed", async () => {
const { buildOverviewSynthesisPrompt } = await import(
"@/lib/graph/investigation-overview-synthesis.js"
);
const aInputs = buildABInputs(fixture, false);
const baseSg = fixture.situationGraph;
const knownSupportedNodes = baseSg.nodes.filter(
(n) => ["known", "supported"].includes(n.status)
);
// Version A: filtered graph (unknowns removed)
const promptA = buildOverviewSynthesisPrompt(
{ ...baseSg, nodes: knownSupportedNodes },
aInputs.supportedEvidence,
aInputs.findings,
aInputs.plausibleInterpretations
);
// Version B: full graph (unknowns included) — should be identical because
// buildOverviewSynthesisPrompt already filters to known+supported only
const promptB = buildOverviewSynthesisPrompt(
baseSg,
aInputs.supportedEvidence,
aInputs.findings,
aInputs.plausibleInterpretations
);
expect(promptA).toBe(promptB);
});
it("fixed plausible interpretation carries NON-EVIDENCE epistemic status", () => {
const a = buildABInputs(fixture, false);
expect(a.plausibleInterpretations).toHaveLength(1);
expect(a.plausibleInterpretations[0]).toBe(FIXED_PLAUSIBLE);
const el = a.supportedEvidence.map(e => e.label.toLowerCase());
const il = FIXED_PLAUSIBLE.toLowerCase();
for (const label of el) expect(label).not.toContain(il);
});
it("fixture has the minimum required content for A/B comparison", () => {
const sg = fixture.situationGraph;
expect(sg.nodes.filter(n => ["known","supported"].includes(n.status)).length).toBeGreaterThan(0);
expect(sg.nodes.filter(n => n.kind === "unknown" && n.status === "unknown").length).toBeGreaterThan(0);
expect(fixture.findings.filter(f => f.status === "provisional" && f.userDisposition === null).length).toBeGreaterThan(0);
});
});