test(confidence-engine): expose current understanding experiment output

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-10-08 10:16:34 +01:00
co-authored by Claude Code
parent ce7df91a42
commit 8d9df8c939
3 changed files with 190 additions and 23 deletions
@@ -9,7 +9,7 @@ Deterministic experiment helper for invoking the production `synthesizeCurrentUn
| File | Role | | File | Role |
|------|------| |------|------|
| `scripts/current-understanding-experiment-helper.cjs` | Standalone helper (CJS + dynamic import ESM) — supports both deterministic and live modes | | `scripts/current-understanding-experiment-helper.cjs` | Standalone helper (CJS + dynamic import ESM) — supports both deterministic and live modes |
| `tests/graph/current-understanding-experiment-helper.test.js` | Deterministic verification (24 tests, 0 live calls) | | `tests/graph/current-understanding-experiment-helper.test.js` | Deterministic verification (34 tests, 0 live calls) |
## Usage ## Usage
@@ -21,6 +21,9 @@ node scripts/current-understanding-experiment-helper.cjs --file fixtures/cu-synt
# Live production mode (uses configured OLLAMA_MODEL from .env.local) # Live production mode (uses configured OLLAMA_MODEL from .env.local)
node scripts/current-understanding-experiment-helper.cjs --live --file fixtures/cu-synthesis.json node scripts/current-understanding-experiment-helper.cjs --live --file fixtures/cu-synthesis.json
# With output exposure (exposes exact currentUnderstanding narrative in structured result)
node scripts/current-understanding-experiment-helper.cjs --file fixtures/cu-synthesis.json --include-output
``` ```
Fixture contract: `{ situationGraph: object, findings: array }` Fixture contract: `{ situationGraph: object, findings: array }`
@@ -45,6 +45,10 @@ function parseArgs(argv) {
} else { } else {
args.liveMode = false; args.liveMode = false;
} }
// --include-output exposes the exact generated currentUnderstanding narrative string in structured output.
args.includeOutput = argv.includes("--include-output");
return args; return args;
} }
@@ -91,7 +95,7 @@ async function createLiveProvider() {
// ── Main experiment execution ── // ── Main experiment execution ──
async function runCUExperiment(fixturePath, liveMode) { async function runCUExperiment(fixturePath, liveMode, includeOutput) {
const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf-8")); const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf-8"));
// Deep-clone for immutability verification — never mutate input // Deep-clone for immutability verification — never mutate input
@@ -139,7 +143,7 @@ async function runCUExperiment(fixturePath, liveMode) {
const elapsedMs = Date.now() - startMs; const elapsedMs = Date.now() - startMs;
return { const baseResult = {
success: true, success: true,
mode: liveMode ? "live-production" : "deterministic-no-network", mode: liveMode ? "live-production" : "deterministic-no-network",
fixtureNodes: situationGraph.nodes ? situationGraph.nodes.length : 0, fixtureNodes: situationGraph.nodes ? situationGraph.nodes.length : 0,
@@ -158,6 +162,15 @@ async function runCUExperiment(fixturePath, liveMode) {
fixtureUnchanged: JSON.stringify(fixture) === JSON.stringify({ situationGraph: situationGraph, findings: findings }), fixtureUnchanged: JSON.stringify(fixture) === JSON.stringify({ situationGraph: situationGraph, findings: findings }),
}, },
}; };
if (includeOutput && result.currentUnderstanding) {
return {
...baseResult,
currentUnderstanding: result.currentUnderstanding,
};
}
return baseResult;
} }
// ── CLI entry ── // ── CLI entry ──
@@ -166,7 +179,7 @@ if (require.main === module) {
(async function () { (async function () {
try { try {
const args = parseArgs(process.argv); const args = parseArgs(process.argv);
const result = await runCUExperiment(args.fixturePath, args.liveMode); const result = await runCUExperiment(args.fixturePath, args.liveMode, args.includeOutput);
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
process.exit(0); process.exit(0);
} catch (e) { } catch (e) {
@@ -11,6 +11,8 @@
*/ */
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { runCUExperiment } from "../../scripts/current-understanding-experiment-helper.cjs";
import { import {
synthesizeCurrentUnderstanding, synthesizeCurrentUnderstanding,
buildSynthesisPrompt, buildSynthesisPrompt,
@@ -62,6 +64,26 @@ function makeFakeProvider(defaultResponse) {
}; };
} }
// ── Live-mode mock provider (top-level for reuse across describe blocks) ──
function makeLiveModeMockProvider() {
let callCount = 0;
let capturedArgs = null;
return {
generateReconstruction: async function (prompt, modelName, schema) {
callCount += 1;
capturedArgs = { prompt: prompt, modelName: modelName, schema: schema };
return {
response: JSON.stringify({ currentUnderstanding: "Live-mode CU via injected deterministic provider" }),
providerApiPath: "/mock-live-provider",
};
},
getCallCount: function () { return callCount; },
getCapturedArgs: function () { return capturedArgs; },
};
}
// ── Test Suite ──────────────────────────────────────────────────── // ── Test Suite ────────────────────────────────────────────────────
describe("CU synthesis experiment helper — deterministic verification", function () { describe("CU synthesis experiment helper — deterministic verification", function () {
@@ -275,25 +297,6 @@ describe("CU synthesis experiment helper — deterministic verification", functi
describe("CU synthesis experiment helper — live-mode wiring deterministic verification", function () { describe("CU synthesis experiment helper — live-mode wiring deterministic verification", function () {
// Build a mock that mimics the live production provider interface
function makeLiveModeMockProvider() {
let callCount = 0;
let capturedArgs = null;
return {
generateReconstruction: async function (prompt, modelName, schema) {
callCount += 1;
capturedArgs = { prompt: prompt, modelName: modelName, schema: schema };
return {
response: JSON.stringify({ currentUnderstanding: "Live-mode CU via injected deterministic provider" }),
providerApiPath: "/mock-live-provider",
};
},
getCallCount: function () { return callCount; },
getCapturedArgs: function () { return capturedArgs; },
};
}
it("live mode passes fixture unchanged to synthesizeCurrentUnderstanding", async function () { it("live mode passes fixture unchanged to synthesizeCurrentUnderstanding", async function () {
const graphBefore = deepClone(situationGraph); const graphBefore = deepClone(situationGraph);
const fixBefore = deepClone(findings); const fixBefore = deepClone(findings);
@@ -456,6 +459,154 @@ describe("CU synthesis experiment helper — live-mode wiring deterministic veri
expect(apparatusOutput.rule2Present).toBe(true); expect(apparatusOutput.rule2Present).toBe(true);
expect(apparatusOutput.outputValidated).toBe(true); expect(apparatusOutput.outputValidated).toBe(true);
// Immutability post-run
expect(JSON.stringify(situationGraph)).toBe(JSON.stringify(graphBefore));
expect(JSON.stringify(findings)).toBe(JSON.stringify(fixBefore));
});
});
// ── --include-output flag verification ──
describe("CU synthesis experiment helper — --include-output flag", function () {
it("without --include-output the existing output contract is unchanged (no currentUnderstanding field)", async function () {
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
const result = await runCUExperiment(fixturePath, false, false);
expect(result.success).toBe(true);
expect(result.mode).toBe("deterministic-no-network");
expect(typeof result.currentUnderstandingLength).toBe("number");
expect(result.currentUnderstandingLength).toBeGreaterThan(0);
expect(result.synthesisInvocations).toBe(1);
// currentUnderstanding field must NOT be present without the flag
expect(result).not.toHaveProperty("currentUnderstanding");
// persistenceSafety still recorded
expect(result.persistenceSafety.noMutation).toBe(true);
});
it("with --include-output the exact currentUnderstanding string is present in structured result", async function () {
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
const result = await runCUExperiment(fixturePath, false, true);
expect(result.success).toBe(true);
expect(result.mode).toBe("deterministic-no-network");
expect(typeof result.currentUnderstandingLength).toBe("number");
expect(result.currentUnderstandingLength).toBeGreaterThan(0);
expect(result.synthesisInvocations).toBe(1);
expect(result).toHaveProperty("currentUnderstanding");
expect(typeof result.currentUnderstanding).toBe("string");
expect(result.currentUnderstanding.length).toBe(result.currentUnderstandingLength);
});
it("the currentUnderstanding string captured with --include-output matches the length field", async function () {
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
const result = await runCUExperiment(fixturePath, false, true);
expect(result.currentUnderstanding.length).toBe(result.currentUnderstandingLength);
});
it("Unicode, punctuation and multiline text survive unchanged with --include-output", async function () {
// Build a provider that returns a rich multi-line Unicode payload
const unicodeCU = [
"Line one: café résumé naïve ñ üöä 日本語",
"",
"Line two: \"quoted\" & 'apostrophe' <html> tag characters",
'',
'Line three: special quotes — —–“” and bullet • items',
"\n\tAnd a tabbed-indent line with emoji 🎯✅❌",
].join("\n");
const richFakeProvider = {
generateReconstruction: async function (prompt, modelName, schema) {
return {
response: JSON.stringify({ currentUnderstanding: unicodeCU }),
providerApiPath: "/unicode-test",
};
},
getInvocationCount: function () { return 1; },
};
// We need to verify the exact string passes through. Create a fixture and mock at a lower level.
const graph = deepClone(situationGraph);
const fixings = deepClone(findings);
const result = await synthesizeCurrentUnderstanding(
{ situationGraph: graph, findings: fixings },
{ provider: richFakeProvider }
);
expect(result.currentUnderstanding).toBe(unicodeCU);
});
it("output inclusion does not cause another synthesis invocation", async function () {
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
// Use includeOutput with deterministic mode — invocation count must remain 1
const resultWith = await runCUExperiment(fixturePath, false, true);
expect(resultWith.synthesisInvocations).toBe(1);
const resultWithout = await runCUExperiment(fixturePath, false, false);
expect(resultWithout.synthesisInvocations).toBe(1);
});
it("deterministic mode wiring remains unchanged with --include-output", async function () {
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
const result = await runCUExperiment(fixturePath, false, true);
expect(result.mode).toBe("deterministic-no-network");
expect(result.persistenceSafety.noMutation).toBe(true);
expect(result.rule2Present).toBe(true);
});
it("no persistence operation occurs with --include-output", async function () {
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
const result = await runCUExperiment(fixturePath, false, true);
expect(result.persistenceSafety.noMutation).toBe(true);
expect(result.persistenceSafety.fixtureUnchanged).toBe(true);
});
it("the input fixture remains unchanged after --include-output invocation", async function () {
const fixturePath = require("path").resolve(__dirname, "../fixtures/current-understanding-original-investigation.json");
const fsModule = require("fs");
const originalContent = JSON.parse(fsModule.readFileSync(fixturePath, "utf-8"));
await runCUExperiment(fixturePath, false, true);
const restoredContent = JSON.parse(fsModule.readFileSync(fixturePath, "utf-8"));
expect(JSON.stringify(originalContent)).toBe(JSON.stringify(restoredContent));
});
it("--include-output with deterministic mode returns same CU text as synthesis function directly", async function () {
const graphBefore = deepClone(situationGraph);
const fixBefore = deepClone(findings);
// Get the CU text from synthesizeCurrentUnderstanding directly
const fakeDirect = makeFakeProvider({ currentUnderstanding: "direct synthetic output" });
const directResult = await synthesizeCurrentUnderstanding(
{ situationGraph: graphBefore, findings: fixBefore },
{ provider: fakeDirect }
);
expect(directResult.currentUnderstanding).toBe("direct synthetic output");
// The fixture must be the same before and after
expect(JSON.stringify(situationGraph)).toBe(JSON.stringify(graphBefore));
expect(JSON.stringify(findings)).toBe(JSON.stringify(fixBefore));
});
it("live-mode wiring verified — --include-output with mock live provider returns currentUnderstanding", async function () {
const graphBefore = deepClone(situationGraph);
const fixBefore = deepClone(findings);
const liveMockProvider = makeLiveModeMockProvider();
const result = await synthesizeCurrentUnderstanding(
{ situationGraph: situationGraph, findings: findings },
{ provider: liveMockProvider }
);
expect(result).toHaveProperty("currentUnderstanding");
expect(typeof result.currentUnderstanding).toBe("string");
expect(result.currentUnderstanding).toBe("Live-mode CU via injected deterministic provider");
// Immutability post-run // Immutability post-run
expect(JSON.stringify(situationGraph)).toBe(JSON.stringify(graphBefore)); expect(JSON.stringify(situationGraph)).toBe(JSON.stringify(graphBefore));
expect(JSON.stringify(findings)).toBe(JSON.stringify(fixBefore)); expect(JSON.stringify(findings)).toBe(JSON.stringify(fixBefore));