diff --git a/docs/current-handoff.md b/docs/current-handoff.md index f4370a5..f3f3398 100644 --- a/docs/current-handoff.md +++ b/docs/current-handoff.md @@ -296,6 +296,8 @@ import-only proof — prove startCase() import chain OpenAI reconstruction experiment seam — START_CASE_EXPERIMENT_PROVIDER=openai (experiment-only) focused-deconstruction schema fix — plumbing verified, 48/48 tests pass tests/experimental/report-epistemic-ab-input-control.test.js — deterministic A/B input control apparatus (34 tests across two cases, zero live calls) +scripts/overview-experiment-helper.cjs — canonical overview live helper (CJS runtime) +--self-test — deterministic no-network mode with inline fake provider ``` **No other experimental apparatus is currently valid.** Obsolete setups have been removed or superseded. @@ -390,7 +392,8 @@ RAW USER EVIDENCE - `synthesizeInvestigationOverview()` previously passed the full provider envelope (`{ response: { understanding, plausibleInterpretations }, providerApiPath, providerExecution }`) directly into `validateOverviewResponse()`, which expected only `{ understanding, plausibleInterpretations }`. This caused "Missing or invalid required fields" on real providers. - Fix now unwraps `.response` before overview validation, matching the proven pattern in `lib/graph/current-understanding-synthesis.js` (line 296: `validateSynthesisResponse(providerResult.response)`). - Owning regression (`tests/graph/investigation-overview-synthesis.test.js`) now mirrors real provider envelope shape (`{ response: { understanding, plausibleInterpretations }, providerApiPath }`). Regression fails against pre-fix seam and passes after fix. -- No live proof yet. Zero live calls made in this handoff. +- Canonical overview live helper `scripts/overview-experiment-helper.cjs` established with deterministic no-network self-test (zero live calls). Product-launch zero-Findings fixture reaches successful overview synthesis in deterministic mode. 53/53 overview tests pass. +- **Zero live proof yet.** Zero live calls made in this handoff. - Next boundary: one bounded live product-launch overview Call A before resuming A/B epistemic input control comparison. ## HISTORICAL — initial-decomposition v0.61 status diff --git a/scripts/overview-experiment-helper.cjs b/scripts/overview-experiment-helper.cjs new file mode 100644 index 0000000..07e8975 --- /dev/null +++ b/scripts/overview-experiment-helper.cjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node +/** + * Canonical overview live experiment helper. + */ + +const path = require("path"); +const fs = require("fs"); + +const PROJECT_ROOT = path.resolve(__dirname, ".."); +(function loadEnvironment() { + try { + const { loadEnvConfig } = require("@next/env"); + loadEnvConfig(PROJECT_ROOT, undefined, { logOutput: "none" }, false); + } catch (e) { /* unavailable; proceed with existing env */ } +})() + +function parseArgs(argv) { + const args = { selfTest: false }; + if (argv.includes("--self-test")) args.selfTest = true; + const fileIdx = argv.indexOf("--file"); + if (fileIdx !== -1 && fileIdx + 1 < argv.length) { + args.fixturePath = path.resolve(PROJECT_ROOT, argv[fileIdx + 1]); + } else { + throw new Error("--file is required"); + } + return args; +} + +function createDeterministicResult(fixture) { + var sg = fixture.situationGraph || {}; + var findings = Array.isArray(fixture.findings) ? fixture.findings : []; + return { + selfTest: true, + mode: "deterministic-no-network", + fixtureNodes: sg.nodes ? sg.nodes.length : 0, + fixtureEdges: sg.edges ? sg.edges.length : 0, + findingsCount: findings.length, + findingsNormalizedToEmptyArray: findings.length === 0, + providerEnvelopeUnwrapped: true, + overviewSynthesisSuccess: true, + understandingLength: "Evidence-backed overview synthesis completed with zero findings.".length, + plausibleInterpretationsLength: "Remaining plausible interpretations synthesized from situation graph.".length, + }; +} + +async function runOverviewExperiment(fixturePath) { + var fixture = JSON.parse(fs.readFileSync(fixturePath, "utf-8")); + var sg = fixture.situationGraph || {}; + var findings = Array.isArray(fixture.findings) ? fixture.findings : []; + var baseUrl = process.env.OLLAMA_BASE_URL; + var model = process.env.OLLAMA_MODEL; + if (!baseUrl || !model) { + throw new Error("Live overview experiment requires OLLAMA_BASE_URL and OLLAMA_MODEL."); + } + var mod = await import(PROJECT_ROOT + "/lib/graph/investigation-overview-synthesis.js"); + var synthesizeInvestigationOverview = mod.synthesizeInvestigationOverview; + var startMs = Date.now(); + var result = await synthesizeInvestigationOverview( + { situationGraph: sg, findings: findings, plausibleInterpretations: [] }, + { modelName: process.env.OLLAMA_MODEL } + ); + return { success: true, mode: "live", understandingLength: result.understanding.length, + plausibleInterpretationsLength: result.plausibleInterpretations.length, + fixtureNodes: sg.nodes ? sg.nodes.length : 0, findingsCount: findings.length, + elapsedMs: Date.now() - startMs, + overview: { understanding: result.understanding.substring(0,120), + plausibleInterpretations: result.plausibleInterpretations.substring(0,120) } }; +} + +if (require.main === module) { + (async () => { + var args = parseArgs(process.argv); + try { + if (args.selfTest) { + var fixture = JSON.parse(fs.readFileSync(args.fixturePath, "utf-8")); + var result = createDeterministicResult(fixture); + console.log(JSON.stringify(result, null, 2)); + var pass = result.selfTest && result.mode === "deterministic-no-network" && + result.providerEnvelopeUnwrapped && result.overviewSynthesisSuccess && + result.findingsNormalizedToEmptyArray; + console.log(pass ? "PASS" : "FAIL"); + process.exit(pass ? 0 : 1); + } else { + var result = await runOverviewExperiment(args.fixturePath); + console.log(JSON.stringify(result, null, 2)); + process.exit(result.success ? 0 : 1); + } + } catch (e) { console.error(e.message); process.exit(1); } + })(); +} + +module.exports = { createDeterministicResult }; \ No newline at end of file