From 8e3f4388d61faf13bee92274b4eb9d9a29bb0f75 Mon Sep 17 00:00:00 2001 From: robbond Date: Mon, 5 Oct 2026 09:35:04 +0100 Subject: [PATCH] test(confidence-engine): preserve focused investigation evidence seam --- ...date-experiment-helper-checkpoint.test.cjs | 38 +++++++++++++++- tests/graph/live-update-experiment-helper.cjs | 45 +++++++++++++++++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/tests/graph/live-update-experiment-helper-checkpoint.test.cjs b/tests/graph/live-update-experiment-helper-checkpoint.test.cjs index 60c7011..357d22a 100644 --- a/tests/graph/live-update-experiment-helper-checkpoint.test.cjs +++ b/tests/graph/live-update-experiment-helper-checkpoint.test.cjs @@ -311,8 +311,8 @@ assert(helperSrc.includes("resolvedNodeIds.add(targetNodeId)") || helperSrc.incl // E5. Post-episode graph feeds CU synthesis: verified by composeDeepInvestigationCheckpoint's Step 2 assert(helperSrc.includes("synthesizeCurrentUnderstanding"), "helper invokes synthesizeCurrentUnderstanding for CU synthesis"); -// E6. Prior + appended Findings accumulated exactly once for CU synthesis -assert(helperSrc.includes("postEpisodeFindings = [...priorFindings, ...accumulatedEpisodes]"), "findings accumulation: prior + appended concatenated exactly once"); +// E6. Prior + derived from Contribution + appended Findings accumulated exactly once for CU synthesis +assert(helperSrc.includes("postEpisodeFindings = [...priorFindings, ...newFindingsFromContributions, ...accumulatedEpisodes]") || helperSrc.includes("postEpisodeFindings : [...priorFindings, ...newFindingsFromContributions, ...accumulatedEpisodes]"), "findings accumulation: prior + derived from Contribution + appended concatenated exactly once"); // E7. Next graph returned via updatedSituationGraph in checkpoint object assert(helperSrc.includes("updatedSituationGraph: updatedGraph") || helperSrc.includes("updatedSituationGraph : updatedGraph"), "updatedSituationGraph returned in checkpoint output"); @@ -323,5 +323,39 @@ assert(helperSrc.includes("postEpisodeFindings: postEpisodeFindings"), "postEpis // E9. Provider-call accounting preserved (existing behavior) const extractResult = extractLiveExperimentCheckpoint(successResult(123, 456), successParams); assert(extractResult.providerGenerateCalls === 1, "providerGenerateCalls preserved from extraction path"); + +// ---- CONTRIBUTION SEAM VERIFICATION (Iteration 4) ────────────────────── +console.log("\n=== CONTRIBUTION SEAM VERIFICATION ===\n"); + +// C1. composeDeepInvestigationCheckpoint accepts contributions parameter (defaults to []) +assert(composeDeepInvestigationCheckpoint.length >= 1, "composeDeepInvestigationCheckpoint accepts at least 1 param (contributions default)"); + +// C2. Production parity: helper derives Contribution from answer when not supplied +assert(helperSrc.includes("_deriveContributionFromAnswer"), "helper has _deriveContributionFromAnswer (mirrors production appendFocusedContribution)"); + +// C3. Contribution carries targetNodeId matching focused target +assert(helperSrc.includes("targetNodeId: targetNodeId") || helperSrc.includes("targetNodeId : targetNodeId"), "Contribution derives targetNodeId from focused target"); + +// C4. Contribution fields match production shape +assert(helperSrc.includes("observations:") && helperSrc.includes("answer:"), "Contribution includes observations and answer fields (production shape)"); + +// C5. deriveFindingsFromContributions is used to derive Findings from new Contribution (same production helper) +assert(helperSrc.includes("deriveFindingsFromContributions"), "helper reuses produce deriveFindingsFromContributions for Finding derivation"); + +// C6. Effective contributions passed to prepareCompletedEpisode (not empty array) +assert(helperSrc.includes("contributions: effectiveContributions") || helperSrc.includes("contributions : effectiveContributions"), "helper passes effectiveContributions to prepareCompletedEpisode (not empty)"); + +// C7. Post-episode graph feeds CU synthesis +assert(helperSrc.includes("synthesizeCurrentUnderstanding"), "CU synthesis receives updated graph for full accumulated findings"); + +// C8. postEpisodeFindings includes prior + derived from new Contribution + appended episodes (exactly-once) +assert(helperSrc.includes("newFindingsFromContributions"), "postEpisodeFindings includes newly derived Findings from Contribution"); + +// C9. next Contributions returned in checkpoint for longitudinal Episode N+1 state feeding +assert(helperSrc.includes("postEpisodeContributions: effectiveContributions") || helperSrc.includes("postEpisodeContributions : effectiveContributions"), "postEpisodeContributions returned in checkpoint"); + +// C10. Source code verifies contributions parameter default behavior (empty array) +assert(helperSrc.includes("contributions = []"), "contributions defaults to empty array when not supplied"); + console.log("\n--- Total failures: " + failures + " ---\n"); process.exit(failures > 0 ? 1 : 0); \ No newline at end of file diff --git a/tests/graph/live-update-experiment-helper.cjs b/tests/graph/live-update-experiment-helper.cjs index f35c8a2..5aa23b4 100644 --- a/tests/graph/live-update-experiment-helper.cjs +++ b/tests/graph/live-update-experiment-helper.cjs @@ -18,6 +18,9 @@ const dotenv = require("dotenv"); dotenv.config({ path: ".env.local" }); +// Re-export production helper for apparatus use — avoids fabricating Finding propositions. +const { deriveFindingsFromContributions } = require("../../lib/graph/finding-helpers.js"); + function assertRequiredEnv(name) { const value = process.env[name]; if (!value) { @@ -268,7 +271,25 @@ async function runLiveExperiment({ graph, previousQuestion, answer, findings = [ * - currentUnderstanding from the synthesis result * - findingsCheckpoint with accurate post-episode accumulated findings count */ -async function composeDeepInvestigationCheckpoint({ situationGraph, previousQuestion, answer, findings = [], targetNodeId, episodeMode }) { +/** + * Deterministically derive a canonical Contribution from the fixed answer + * when the caller does not supply one. Mirrors production appendFocusedContribution + * shape: id, targetNodeId, question, answer, sequence, observations, etc. + */ +function _deriveContributionFromAnswer({ answer, previousQuestion, targetNodeId, sequence }) { + return { + id: `contrib-ep-${String(sequence).padStart(4, "0")}`, + targetNodeId: targetNodeId ?? null, + question: previousQuestion ?? "", + answer: answer ?? "", + sequence: sequence, + observations: [answer], + uncertainties: [], + possibleFollowUpQuestions: [], + }; +} + +async function composeDeepInvestigationCheckpoint({ situationGraph, previousQuestion, answer, findings = [], contributions = [], targetNodeId, episodeMode }) { dotenv.config({ path: ".env.local" }); const baseUrl = assertRequiredEnv("OLLAMA_BASE_URL"); @@ -306,6 +327,19 @@ async function composeDeepInvestigationCheckpoint({ situationGraph, previousQues let episodeResult; /* ── Production-equivalent focused-episode seam: targetNodeId triggers the same path as executeEpisodeDone → handleEpisodeMode ─── */ + + // Derive a canonical Contribution from the fixed answer when none supplied. + // Mirrors production appendFocusedContribution: user answer → canonical Contribution + const effectiveContributions = contributions.length > 0 ? [...contributions] : []; + let newFindingsFromContributions = []; + if (effectiveContributions.length === 0 && answer && targetNodeId) { + const contribSeq = effectiveContributions.length + 1; + const derivedContrib = _deriveContributionFromAnswer({ answer, previousQuestion, targetNodeId, sequence: contribSeq }); + effectiveContributions.push(derivedContrib); + // Derive canonical Findings from the Contribution (production seam) + newFindingsFromContributions = deriveFindingsFromContributions([derivedContrib]).findings; + } + if (episodeMode === true && targetNodeId) { // Mirrors produceEpisodeAwareGraph: prepareCompletedEpisode → reconsiderCompletedEpisode → applyValidatedProposal const { prepareCompletedEpisode } = await import("../../lib/graph/episode-preparation.js"); @@ -314,7 +348,7 @@ async function composeDeepInvestigationCheckpoint({ situationGraph, previousQues const prepared = prepareCompletedEpisode({ situationGraph, targetNodeId, - contributions: [], // no focused contributions in checkpoint helper (mirrors production when called without explicit contributions) + contributions: effectiveContributions, findings, }); @@ -365,10 +399,10 @@ async function composeDeepInvestigationCheckpoint({ situationGraph, previousQues /* ── Step 2: Current Understanding synthesis (mirrors production synthesizeFn) ─── */ const updatedGraph = episodeResult.updatedSituationGraph; - // Accumulate prior findings + newly appended Findings for CU synthesis + // Accumulate prior findings + newly derived Findings (from new Contribution) for CU synthesis const priorFindings = Array.isArray(findings) ? [...findings] : []; const accumulatedEpisodes = episodeResult.appendedFindings ?? []; - const postEpisodeFindings = [...priorFindings, ...accumulatedEpisodes]; + const postEpisodeFindings = [...priorFindings, ...newFindingsFromContributions, ...accumulatedEpisodes]; let currentUnderstanding = null; if (episodeResult.success && updatedGraph) { @@ -405,6 +439,9 @@ async function composeDeepInvestigationCheckpoint({ situationGraph, previousQues /* ── updatedSituationGraph (NEW — for longitudinal Episode N+1 state feeding) ─── */ updatedSituationGraph: updatedGraph, + /* ── postEpisodeContributions (NEW — accumulated Contributions for longitudinal tracking) ─── */ + postEpisodeContributions: effectiveContributions, + /* ── postEpisodeFindings (NEW — accumulated Findings for Episode N+1) ─── */ postEpisodeFindings: postEpisodeFindings,