diff --git a/docs/current-handoff.md b/docs/current-handoff.md index 6de96b0..8d959dc 100644 --- a/docs/current-handoff.md +++ b/docs/current-handoff.md @@ -145,6 +145,12 @@ - OpenAI strict transport projection now derives structure from canonical Zod-generated JSON Schema and canonical input optionality from the Zod contract, including `.optional().default(...)`. Canonically omittable fields are required-but-nullable for transport; null placeholders are omitted only for those fields before canonical Zod validation, while genuinely required fields remain non-nullable. - `reconstructionV2Schema` and Ollama behavior remain unchanged. `START_CASE_EXPERIMENT_PROVIDER=openai` is experiment-only selection; default behavior remains Ollama/Qwen. Deterministic closeout passed with zero live calls; next boundary is exactly one live GPT-5.6 Terra reconstruction. +## Reconstruction-only OpenAI observation seam + +- The first Terra observation was blocked before execution because normal `startCase()` would continue into Ollama-backed question generation. No Terra inference occurred. +- An experiment-only reconstruction-only seam now returns after initial reconstruction and graph construction, before `determineGraphBackedQuestion`. Normal production/default `startCase()` behavior remains unchanged; explicit OpenAI helper selection guarantees one selected reconstruction provider call and zero downstream question-generation calls. +- Zero live calls occurred while implementing and verifying this seam. Next boundary: exactly one live Terra reconstruction with no retry. + ## Current product architecture Three distinct routes, not a single page: diff --git a/lib/graph/orchestrator.js b/lib/graph/orchestrator.js index fcc725b..1604d0f 100644 --- a/lib/graph/orchestrator.js +++ b/lib/graph/orchestrator.js @@ -412,6 +412,26 @@ export async function startCase(body, dependencies = {}) { const graphReferenceValidation = validateGraphReferences( initialSituationGraph, ); + + if (dependencies.reconstructionOnly) { + return { + success: graphReferenceValidation.valid, + summary: analysis.reconstruction?.summary ?? null, + reconstruction: analysis.reconstruction, + situationGraph: initialSituationGraph, + selectedQuestion: null, + diagnostics: buildDiagnostics({ + analysis, + graph: initialSituationGraph, + graphReferenceValidation, + }), + validationErrors: graphReferenceValidation.valid + ? undefined + : graphReferenceValidation.errors, + statusCode: graphReferenceValidation.valid ? 200 : 500, + }; + } + const provider = dependencies.provider ?? getProvider(); const modelName = dependencies.modelName ?? analysis?.modelName ?? null; const initialQuestionResult = await determineGraphBackedQuestion({ diff --git a/scripts/start-case-experiment-helper.cjs b/scripts/start-case-experiment-helper.cjs index 82556df..6871dbf 100755 --- a/scripts/start-case-experiment-helper.cjs +++ b/scripts/start-case-experiment-helper.cjs @@ -64,6 +64,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti let startCase = options.startCase; let reconstructionProvider = options.reconstructionProvider; let reconstructionModelName = options.reconstructionModelName; + let reconstructionOnly = options.reconstructionOnly; const isMock = process.env.START_CASE_EXPERIMENT_HELPER_MOCK === "1"; if (process.env.START_CASE_EXPERIMENT_PROVIDER === "openai") { @@ -80,6 +81,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti fetchImpl: fetch, }); reconstructionModelName = "gpt-5.6-terra"; + reconstructionOnly = true; } if (!startCase && isMock) { @@ -120,6 +122,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti result = await startCase(scenarioInput, { reconstructionProvider, reconstructionModelName, + reconstructionOnly, }); } finally { // Clean up experiment env var after execution regardless of outcome @@ -141,6 +144,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti return { success: result.success, summary: result.summary ?? null, + reconstruction: result.reconstruction ?? null, situationGraphNodeCount: result.situationGraph?.nodes?.length ?? 0, situationGraphEdgeCount: result.situationGraph?.edges?.length ?? 0, selectedQuestion: result.selectedQuestion?.question ?? null, @@ -148,6 +152,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti assessmentProgress: result.assessment?.progress ?? null, endToEndElapsedMs: Date.now() - endToEndStartedAt, diagnostics: result.diagnostics ?? null, + providerApiPath: result.providerApiPath ?? null, error: result.success ? null : (result.error ?? "unknown"), statusCode: result.statusCode ?? (result.success ? 200 : 500), }; diff --git a/tests/graph/orchestrator.test.js b/tests/graph/orchestrator.test.js index 4fe3f4f..a4728c3 100644 --- a/tests/graph/orchestrator.test.js +++ b/tests/graph/orchestrator.test.js @@ -374,6 +374,34 @@ describe("lib/graph/orchestrator startCase", () => { }); }); + it("returns initial reconstruction evidence before downstream question generation", async () => { + const analysis = makeAnalysisResult(); + mockAnalyseScenario.mockResolvedValue(analysis); + const downstreamProvider = { + generateReconstruction: vi.fn(() => { + throw new Error("downstream question generation must not run"); + }), + }; + const { startCase } = await import("@/lib/graph/orchestrator.js"); + + const result = await startCase( + { scenario: "Scenario text" }, + { reconstructionOnly: true, provider: downstreamProvider }, + ); + + expect(result).toMatchObject({ + success: true, + reconstruction: analysis.reconstruction, + selectedQuestion: null, + }); + expect(result.situationGraph.nodes.length).toBeGreaterThan(0); + expect(result.diagnostics.graphReferenceValidation).toEqual({ + valid: true, + errors: [], + }); + expect(downstreamProvider.generateReconstruction).not.toHaveBeenCalled(); + }); + it("rejects invalid request input without throwing", async () => { const { startCase } = await import("@/lib/graph/orchestrator.js"); diff --git a/tests/scripts/start-case-experiment-helper.test.js b/tests/scripts/start-case-experiment-helper.test.js index 21d934a..b382ecd 100644 --- a/tests/scripts/start-case-experiment-helper.test.js +++ b/tests/scripts/start-case-experiment-helper.test.js @@ -174,6 +174,7 @@ describe("start-case-experiment-helper.cjs apparatus", () => { startCase: async (_input, dependencies) => { expect(typeof dependencies.reconstructionProvider?.generateReconstruction).toBe("function"); expect(dependencies.reconstructionModelName).toBe("gpt-5.6-terra"); + expect(dependencies.reconstructionOnly).toBe(true); return { success: true, situationGraph: { nodes: [], edges: [] },