test(confidence-engine): use canonical focused deconstruction

This commit is contained in:
2026-10-06 17:28:40 +01:00
parent 9362c78f81
commit 814360e368
2 changed files with 107 additions and 29 deletions
@@ -324,23 +324,25 @@ assert(helperSrc.includes("postEpisodeFindings: postEpisodeFindings"), "postEpis
const extractResult = extractLiveExperimentCheckpoint(successResult(123, 456), successParams);
assert(extractResult.providerGenerateCalls === 1, "providerGenerateCalls preserved from extraction path");
// ---- CONTRIBUTION SEAM VERIFICATION (Iteration 4) ──────────────────────
// ---- CONTRIBUTION SEAM VERIFICATION (Iteration 4 + Iteration 5 deconstruction) ──────────────────────
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)");
// C2. Canonical deconstruction replaces synthetic raw-answer path for production-faithful Contribution derivation
assert(helperSrc.includes("focusedDeconstruct"), "helper uses canonical focusedDeconstruct for Contribution derivation");
assert(!helperSrc.includes("_deriveContributionFromAnswer"), "synthetic _deriveContributionFromAnswer removed from apparatus");
// C3. Contribution carries targetNodeId matching focused target
assert(helperSrc.includes("targetNodeId: targetNodeId") || helperSrc.includes("targetNodeId : targetNodeId"), "Contribution derives targetNodeId from focused target");
// C3. Contribution targetNodeId comes from focusedDeconstruct result / production focus input
assert(helperSrc.includes("targetNodeId: targetNodeId") || helperSrc.includes("targetNodeId : targetNodeId") || helperSrc.includes("targetNodeId: focused.targetNodeId"), "Contribution derives targetNodeId from deconstruction (production target)");
// C4. Contribution fields match production shape
assert(helperSrc.includes("observations:") && helperSrc.includes("answer:"), "Contribution includes observations and answer fields (production shape)");
// C4. All five deconstruction semantic fields preserved in Contribution shape
assert(helperSrc.includes("observations:") && helperSrc.includes("uncertainties:") && helperSrc.includes("assumptions:") && helperSrc.includes("relationships:"), "Contribution carries all five deconstruction semantic fields");
assert(helperSrc.includes("possibleFollowUpQuestions:"), "Contribution includes possibleFollowUpQuestions from deconstruction");
// C5. deriveFindingsFromContributions is used to derive Findings from new Contribution (same production helper)
assert(helperSrc.includes("deriveFindingsFromContributions"), "helper reuses produce deriveFindingsFromContributions for Finding derivation");
// C5. deriveFindingsFromContributions is used to derive Findings from Contribution (production seam unchanged)
assert(helperSrc.includes("deriveFindingsFromContributions"), "helper reuses production 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)");
@@ -357,5 +359,56 @@ assert(helperSrc.includes("postEpisodeContributions: effectiveContributions") ||
// C10. Source code verifies contributions parameter default behavior (empty array)
assert(helperSrc.includes("contributions = []"), "contributions defaults to empty array when not supplied");
// ---- DECONSTRUCTION INTEGRATION TESTS (Iteration 5) ──────────────────────
console.log("\n=== DECONSTRUCTION INTEGRATION TESTS ===\n");
// D1. Compose function is actually callable (not just exported stub)
assert(typeof composeDeepInvestigationCheckpoint === "function", "composeDeepInvestigationCheckpoint is a real function (callable)");
// D2. Uses canonical focusedDeconstruct import (production seam, not synthetic derivation)
assert(helperSrc.includes("focused-deconstruct") || helperSrc.includes("focusedDeconstruct"), "focusedDeconstruct available via production seam in apparatus");
// D3. Reuses existing provider/model plumbing — no second getProvider call within composeDeepInvestigationCheckpoint
// The helper's single getProvider() in composeDeepInvestigationCheckpoint feeds both the timedWrapper and focusedDeconstruct
const composeHelperSection = helperSrc.substring(
helperSrc.indexOf("async function composeDeepInvestigationCheckpoint"),
helperSrc.indexOf("module.exports")
);
const cpGetProviderCalls = (composeHelperSection.match(/getProvider\(\)/g) || []).length;
assert(cpGetProviderCalls === 1, `composeDeepInvestigationCheckpoint getsProvider() exactly once (${cpGetProviderCalls}) — reused for timedProvider and deconstruction`);
// D4. All five semantic fields preserved from focusedDeconstruct result → Contribution (not synthetic defaults)
assert(helperSrc.includes("observations: focused.observations"), "observations from focusedDeconstruct, not [answer]");
assert(helperSrc.includes("uncertainties: focused.uncertainties"), "uncertainties from focusedDeconstruct, not []");
assert(helperSrc.includes("assumptions: focused.assumptions"), "assumptions from focusedDeconstruct, not synthetic");
assert(helperSrc.includes("relationships: focused.relationships"), "relationships from focusedDeconstruct, not synthetic");
assert(helperSrc.includes("possibleFollowUpQuestions: focused.possibleFollowUpQuestions"), "follow-ups from focusedDeconstruct, not []");
// D5. Findings derive from deconstructed observations via production seam — NOT raw answer text
assert(!helperSrc.includes("observations: [answer]"), "Findings NOT derived from raw answer as observations");
assert(helperSrc.includes("deriveFindingsFromContributions([derivedContrib])"), "Findings derived from deconstruction-derived Contribution");
// D6. Prior Contributions accumulate exactly once — effectiveContributions built once, pushed once
assert(helperSrc.includes("effectiveContributions.push(derivedContrib)"), "one derived Contribution accumulated (exactly-once push)");
// D7. Prior Findings accumulate exactly once — postEpisodeFindings = prior + newDeconstructed + appended
assert(composeHelperSection.includes("postEpisodeFindings") && composeHelperSection.includes("[...priorFindings, ...newFindingsFromContributions"), "findings accumulation: prior + derived from deconstruction (exactly-once)");
// D8. Episode reconsideration still executes via reconsiderCompletedEpisode
assert(helperSrc.includes("reconsiderCompletedEpisode"), "episode reconsideration still executes post-deconstruction");
// D9. CU synthesis still executes with accumulated findings
assert(helperSrc.includes("postEpisodeFindings") && helperSrc.includes("synthesizeCurrentUnderstanding"), "CU synthesis uses full accumulated findings");
// D10. Total provider accounting: focusedDeconstruct (1) + reconsideration (1) = 2 before CU synthesis (3 total in full composed episode)
const deconProviderCalls = (helperSrc.match(/focused\.success|focused\.errors/g) || []).length;
assert(deconProviderCalls >= 2, "deconstruction result checked (success/errors — confirms provider call used)");
// D11. Next graph returned via updatedSituationGraph
assert(helperSrc.includes("updatedSituationGraph: updatedGraph"), "next graph returned in checkpoint");
// D12. Next Contributions/Findings returned for Episode N+1
assert(helperSrc.includes("postEpisodeContributions") && helperSrc.includes("postEpisodeFindings"), "next Contributions and Findings returned for longitudinal tracking");
console.log("\n--- Total failures: " + failures + " ---\n");
process.exit(failures > 0 ? 1 : 0);
+45 -20
View File
@@ -272,23 +272,9 @@ async function runLiveExperiment({ graph, previousQuestion, answer, findings = [
* - findingsCheckpoint with accurate post-episode accumulated findings count
*/
/**
* 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.
* Compose the canonical post-episode deep-investigation checkpoint sequence.
* Uses production-focused deconstruct for Contribution derivation (not synthetic raw-answer path).
*/
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" });
@@ -328,15 +314,54 @@ async function composeDeepInvestigationCheckpoint({ situationGraph, previousQues
/* ── 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
/* ── Canonical deconstruction: use focusedDeconstruct for Contribution derivation ─── */
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 });
/* ── Load production deconstruction seam (dynamic import to resolve ESM path aliases) ─── */
const { focusedDeconstruct } = await import("../../lib/graph/focused-investigation.js");
// Extract node context for deconstruction prompt
const targetNode = situationGraph.nodes.find(n => n.id === targetNodeId);
const targetLabel = targetNode?.label ?? "";
const targetDescription = targetNode?.description ?? "";
const centralStatement = situationGraph.centralStatement ?? "";
// Call production-focused deconstruct — exactly one provider generation
const focused = focusedDeconstruct({
provider,
modelName: model,
targetNodeId,
targetLabel,
targetDescription,
centralStatement,
question: previousQuestion ?? "",
answer,
});
if (!focused.success) {
throw new Error(`focusedDeconstruct failed for episode ${contribSeq}: ${JSON.stringify(focused.errors)}`);
}
// Map deconstruction result → canonical Contribution shape
const derivedContrib = {
id: `contrib-ep-${String(contribSeq).padStart(4, "0")}`,
targetNodeId: focused.targetNodeId ?? targetNodeId,
question: previousQuestion ?? "",
answer: answer,
sequence: contribSeq,
observations: focused.observations,
uncertainties: focused.uncertainties,
assumptions: focused.assumptions,
relationships: focused.relationships,
possibleFollowUpQuestions: focused.possibleFollowUpQuestions,
};
effectiveContributions.push(derivedContrib);
// Derive canonical Findings from the Contribution (production seam)
// Derive canonical Findings from the deconstruction-derived Contribution (production seam)
newFindingsFromContributions = deriveFindingsFromContributions([derivedContrib]).findings;
}