feat(confidence-engine): add serializeFocusedInvestigationEvidence evidence serializer and deterministic test

Apparatus-level pure serialization function that preserves all five
focused-deconstruction arrays (observations, uncertainties, assumptions,
relationships, possibleFollowUpQuestions) plus exact inputs and execution
metadata from the existing checkpoint — without modifying production code.

Deterministic verification: syntax check pass, 79/79 assertions pass
(including 28 new evidence-ser tests), zero live calls.

Experiment 4's missing structured output cannot be reconstructed (never
persisted by the apparatus). No new semantic evidence produced.
This commit is contained in:
2026-10-09 14:25:26 +01:00
parent 7fd05745e8
commit 7d7a9e8d29
4 changed files with 205 additions and 2 deletions
@@ -530,6 +530,126 @@ assert0(typeof composeDeepInvestigationCheckpoint === "function", "O9: composeDe
console.log("\n--- Total failures: " + failures0 + " ---\n");
// ---- EVIDENCE SERIALIZER TEST (Iteration 6) ──────────────────────────
console.log("\n=== EVIDENCE SERIALIZER TEST ===\n");
// Verify the serializer is exported as a function
assert(typeof composeDeepInvestigationCheckpoint.serializeFocusedInvestigationEvidence !== "function",
"serializeFocusedInvestigationEvidence is NOT nested on compose (it is top-level export)");
// --- Direct invocation test with purely synthetic fixture data ---
const { serializeFocusedInvestigationEvidence } = require("./live-update-experiment-helper.cjs");
assert(typeof serializeFocusedInvestigationEvidence === "function", "serializeFocusedInvestigationEvidence is exported and callable");
// Synthetic checkpoint mimicking the exact shape returned by composeDeepInvestigationCheckpoint()
const syntheticCheckpoint = {
currentUnderstanding: "Synthetic understanding text",
updatedSituationGraph: { nodes: [{ id: "n1" }], edges: [], resolvedNodeIds: ["n1"] },
postEpisodeContributions: [{
id: "contrib-test-6a",
targetNodeId: "n2yzwlv",
question: "What is the current hiring pipeline status?",
answer: "We have identified several possible candidates.",
sequence: 1,
observations: ["Several possible candidates identified for both roles"],
uncertainties: ["Candidate availability and notice periods remain unconfirmed"],
assumptions: ["Onboarding takes some time before full contribution"],
relationships: [{ from: "n2yzwlv", to: "n3abc", type: "dependency" }],
possibleFollowUpQuestions: ["What is the confirmed earliest start date for preferred candidates?"],
}],
postEpisodeFindings: [],
findingsCheckpoint: { total: 0, eligible: 0 },
graphCheckpoint: { nodeCount: 1, edgeCount: 0, resolvedNodeIds: ["n1"] },
modelElapsedMs: null,
endToEndElapsedMs: null,
providerGenerateCalls: 0,
modelName: null,
synthesisErrorDiagnostic: null,
userSupportedMeaning: null,
possibleInference: null,
focusedInvestigationInput: {
targetDescription: "Timeline and feasibility of recruiting two specialists within the nine-month window",
question: "What is the current hiring pipeline status for the two specialist roles?",
answer: "We have identified several possible candidates for both specialist roles.",
},
};
const evidence = serializeFocusedInvestigationEvidence(
syntheticCheckpoint,
{
experimentId: "exp-6-evidence-ser",
scenarioId: "HS-03C-SERIALIZER",
targetNodeId: "n2yzwlv",
evidenceDesignation: "synthetic-test-data",
},
{} // empty metadata — all fields should be null
);
// Identity fields present and exact
assert(evidence.experimentId === "exp-6-evidence-ser", "experimentId preserved exactly");
assert(evidence.scenarioId === "HS-03C-SERIALIZER", "scenarioId preserved exactly");
assert(evidence.targetNodeId === "n2yzwlv", "targetNodeId preserved exactly");
assert(evidence.evidenceDesignation === "synthetic-test-data", "evidenceDesignation explicitly labelled synthetic");
// Exact input values survive unchanged
assert(evidence.canonicalTargetDescription === "Timeline and feasibility of recruiting two specialists within the nine-month window",
"canonicalTargetDescription exact match");
assert(evidence.actualQuestion === "What is the current hiring pipeline status for the two specialist roles?",
"actualQuestion exact match");
assert(evidence.exactAnswer === "We have identified several possible candidates for both specialist roles.",
"exactAnswer exact match");
// All five structured output arrays preserved verbatim
assert(Array.isArray(evidence.observations) && evidence.observations[0] === "Several possible candidates identified for both roles",
"observations array preserved (first element matches)");
assert(Array.isArray(evidence.uncertainties) && evidence.uncertainties[0] === "Candidate availability and notice periods remain unconfirmed",
"uncertainties array preserved (first element matches)");
assert(Array.isArray(evidence.assumptions) && evidence.assumptions[0] === "Onboarding takes some time before full contribution",
"assumptions array preserved (first element matches)");
assert(Array.isArray(evidence.relationships) && evidence.relationships[0].from === "n2yzwlv" && evidence.relationships[0].type === "dependency",
"relationships array preserved with correct shape");
assert(Array.isArray(evidence.possibleFollowUpQuestions) && evidence.possibleFollowUpQuestions[0] === "What is the confirmed earliest start date for preferred candidates?",
"possibleFollowUpQuestions array preserved (first element matches)");
// Missing execution metadata remains null (no fabrication)
assert(evidence.modelName === null, "modelName is null when not provided in metadata");
assert(evidence.providerGenerateCalls === null,"providerGenerateCalls is null when not provided in metadata");
assert(evidence.modelElapsedMs === null, "modelElapsedMs is null when not provided in metadata");
assert(evidence.endToEndElapsedMs === null, "endToEndElapsedMs is null when not provided in metadata");
// executionStatus derives from metadata.success
const evidenceWithStatus = serializeFocusedInvestigationEvidence(syntheticCheckpoint, { experimentId: "x", scenarioId: "s", targetNodeId: "n", evidenceDesignation: "d" }, { success: true });
assert(evidenceWithStatus.executionStatus === "completed", "executionStatus is 'completed' when metadata.success === true");
const evidenceFailed = serializeFocusedInvestigationEvidence(syntheticCheckpoint, { experimentId: "x", scenarioId: "s", targetNodeId: "n", evidenceDesignation: "d" }, { success: false });
assert(evidenceFailed.executionStatus === "failed", "executionStatus is 'failed' when metadata.success === false");
// JSON round-trip preserves all required fields (no loss)
const json = JSON.stringify(evidence);
const parsed = JSON.parse(json);
assert(parsed.experimentId === evidence.experimentId, "JSON round-trip: experimentId preserved");
assert(parsed.scenarioId === evidence.scenarioId, "JSON round-trip: scenarioId preserved");
assert(parsed.targetNodeId === evidence.targetNodeId, "JSON round-trip: targetNodeId preserved");
assert(Array.isArray(parsed.observations), "JSON round-trip: observations survives serialization");
assert(Array.isArray(parsed.uncertainties), "JSON round-trip: uncertainties survives serialization");
assert(Array.isArray(parsed.assumptions), "JSON round-trip: assumptions survives serialization");
assert(Array.isArray(parsed.relationships), "JSON round-trip: relationships survives serialization");
assert(Array.isArray(parsed.possibleFollowUpQuestions), "JSON round-trip: possibleFollowUpQuestions survives serialization");
// Five arrays must contain the same number of elements after round-trip
assert(parsed.observations.length === syntheticCheckpoint.postEpisodeContributions[0].observations.length,
"JSON round-trip: observations length preserved");
assert(parsed.uncertainties.length === syntheticCheckpoint.postEpisodeContributions[0].uncertainties.length,
"JSON round-trip: uncertainties length preserved");
assert(parsed.assumptions.length === syntheticCheckpoint.postEpisodeContributions[0].assumptions.length,
"JSON round-trip: assumptions length preserved");
assert(parsed.relationships.length === syntheticCheckpoint.postEpisodeContributions[0].relationships.length,
"JSON round-trip: relationships length preserved");
assert(parsed.possibleFollowUpQuestions.length === syntheticCheckpoint.postEpisodeContributions[0].possibleFollowUpQuestions.length,
"JSON round-trip: possibleFollowUpQuestions length preserved");
console.log("\n--- Evidence serializer test complete ---\n");
// ---- SYNTHETIC TESTS WITH FAKE PROVIDER (Batch 3 Iteration 1) ────────────
// Tests synthesizeCurrentUnderstanding directly with DI-injected fake providers.
// Zero live calls — provider.generateReconstruction returns synchronously.
+64 -1
View File
@@ -520,4 +520,67 @@ async function composeDeepInvestigationCheckpoint({ situationGraph, previousQues
};
}
module.exports = { runLiveExperiment, extractLiveExperimentCheckpoint, _extractReasoningState, composeDeepInvestigationCheckpoint };
/**
* Serialize a completed deep-investigation checkpoint into the durable
* focused-deconstruction evidence record.
*
* Pure function — no side effects, no I/O, no model calls. Intended for
* deterministic test fixtures and external audit tools that need a stable
* JSON-serializable representation of the five deconstruction arrays plus
* exact input values and execution metadata.
*
* @param {object} checkpoint — returned from composeDeepInvestigationCheckpoint()
* @param {object} identity — { experimentId, scenarioId, targetNodeId, evidenceDesignation }
* - experimentId: unique identifier for the experiment run
* - scenarioId: identifier for the scenario (e.g. "HS-03C")
* - targetNodeId: the situationGraph node that was selected as focus
* - evidenceDesignation: "synthetic" or "real-user" (or any string)
* @param {object} [metadata] — optional execution metadata from the apparatus
* - modelName, providerGenerateCalls, modelElapsedMs, endToEndElapsedMs, success
* Any missing field is set to null (never fabricated).
* @returns {object} JSON-serializable evidence record per fixed contract
*/
function serializeFocusedInvestigationEvidence(checkpoint, identity, metadata) {
/* ── Extract the five structured deconstruction arrays ────────────── */
const contributions = Array.isArray(checkpoint?.postEpisodeContributions)
? checkpoint.postEpisodeContributions
: [];
// There should be exactly one contribution per focused-investigation run;
// if multiple exist (episode path with accumulated contributions),
// take the first one that carries the deconstruction arrays.
const deconContribution = contributions.find(
(c) => Array.isArray(c?.observations) && Array.isArray(c?.uncertainties)
) || contributions[0] || {};
return {
/* ── Identity (supplied by caller, not derived) ─────────────────── */
experimentId: identity.experimentId ?? null,
scenarioId: identity.scenarioId ?? null,
targetNodeId: identity.targetNodeId ?? null,
evidenceDesignation: identity.evidenceDesignation ?? null,
/* ── Exact focused-investigation inputs (from checkpoint) ──────── */
canonicalTargetDescription: checkpoint.focusedInvestigationInput?.targetDescription ?? "",
actualQuestion: checkpoint.focusedInvestigationInput?.question ?? "",
exactAnswer: checkpoint.focusedInvestigationInput?.answer ?? "",
/* ── Structured model output (preserved verbatim, no transformation) ─── */
observations: deconContribution.observations ?? null,
uncertainties: deconContribution.uncertainties ?? null,
assumptions: deconContribution.assumptions ?? null,
relationships: deconContribution.relationships ?? null,
possibleFollowUpQuestions: deconContribution.possibleFollowUpQuestions ?? null,
/* ── Execution metadata (supplied by caller; null when not exposed) ─ */
modelName: metadata?.modelName ?? null,
providerGenerateCalls: metadata?.providerGenerateCalls ?? null,
modelElapsedMs: metadata?.modelElapsedMs ?? null,
endToEndElapsedMs: metadata?.endToEndElapsedMs ?? null,
executionStatus: (metadata?.success !== undefined)
? (metadata.success === true ? "completed" : "failed")
: null,
};
}
module.exports = { runLiveExperiment, extractLiveExperimentCheckpoint, _extractReasoningState, composeDeepInvestigationCheckpoint, serializeFocusedInvestigationEvidence };