experiment(confidence-engine): isolate reconstruction observation

This commit is contained in:
2026-09-06 10:31:45 +01:00
parent 215c783d11
commit 0bb2f01100
5 changed files with 60 additions and 0 deletions
+6
View File
@@ -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. - 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. - `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 ## Current product architecture
Three distinct routes, not a single page: Three distinct routes, not a single page:
+20
View File
@@ -412,6 +412,26 @@ export async function startCase(body, dependencies = {}) {
const graphReferenceValidation = validateGraphReferences( const graphReferenceValidation = validateGraphReferences(
initialSituationGraph, 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 provider = dependencies.provider ?? getProvider();
const modelName = dependencies.modelName ?? analysis?.modelName ?? null; const modelName = dependencies.modelName ?? analysis?.modelName ?? null;
const initialQuestionResult = await determineGraphBackedQuestion({ const initialQuestionResult = await determineGraphBackedQuestion({
+5
View File
@@ -64,6 +64,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
let startCase = options.startCase; let startCase = options.startCase;
let reconstructionProvider = options.reconstructionProvider; let reconstructionProvider = options.reconstructionProvider;
let reconstructionModelName = options.reconstructionModelName; let reconstructionModelName = options.reconstructionModelName;
let reconstructionOnly = options.reconstructionOnly;
const isMock = process.env.START_CASE_EXPERIMENT_HELPER_MOCK === "1"; const isMock = process.env.START_CASE_EXPERIMENT_HELPER_MOCK === "1";
if (process.env.START_CASE_EXPERIMENT_PROVIDER === "openai") { if (process.env.START_CASE_EXPERIMENT_PROVIDER === "openai") {
@@ -80,6 +81,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
fetchImpl: fetch, fetchImpl: fetch,
}); });
reconstructionModelName = "gpt-5.6-terra"; reconstructionModelName = "gpt-5.6-terra";
reconstructionOnly = true;
} }
if (!startCase && isMock) { if (!startCase && isMock) {
@@ -120,6 +122,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
result = await startCase(scenarioInput, { result = await startCase(scenarioInput, {
reconstructionProvider, reconstructionProvider,
reconstructionModelName, reconstructionModelName,
reconstructionOnly,
}); });
} finally { } finally {
// Clean up experiment env var after execution regardless of outcome // Clean up experiment env var after execution regardless of outcome
@@ -141,6 +144,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
return { return {
success: result.success, success: result.success,
summary: result.summary ?? null, summary: result.summary ?? null,
reconstruction: result.reconstruction ?? null,
situationGraphNodeCount: result.situationGraph?.nodes?.length ?? 0, situationGraphNodeCount: result.situationGraph?.nodes?.length ?? 0,
situationGraphEdgeCount: result.situationGraph?.edges?.length ?? 0, situationGraphEdgeCount: result.situationGraph?.edges?.length ?? 0,
selectedQuestion: result.selectedQuestion?.question ?? null, selectedQuestion: result.selectedQuestion?.question ?? null,
@@ -148,6 +152,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction, opti
assessmentProgress: result.assessment?.progress ?? null, assessmentProgress: result.assessment?.progress ?? null,
endToEndElapsedMs: Date.now() - endToEndStartedAt, endToEndElapsedMs: Date.now() - endToEndStartedAt,
diagnostics: result.diagnostics ?? null, diagnostics: result.diagnostics ?? null,
providerApiPath: result.providerApiPath ?? null,
error: result.success ? null : (result.error ?? "unknown"), error: result.success ? null : (result.error ?? "unknown"),
statusCode: result.statusCode ?? (result.success ? 200 : 500), statusCode: result.statusCode ?? (result.success ? 200 : 500),
}; };
+28
View File
@@ -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 () => { it("rejects invalid request input without throwing", async () => {
const { startCase } = await import("@/lib/graph/orchestrator.js"); const { startCase } = await import("@/lib/graph/orchestrator.js");
@@ -174,6 +174,7 @@ describe("start-case-experiment-helper.cjs apparatus", () => {
startCase: async (_input, dependencies) => { startCase: async (_input, dependencies) => {
expect(typeof dependencies.reconstructionProvider?.generateReconstruction).toBe("function"); expect(typeof dependencies.reconstructionProvider?.generateReconstruction).toBe("function");
expect(dependencies.reconstructionModelName).toBe("gpt-5.6-terra"); expect(dependencies.reconstructionModelName).toBe("gpt-5.6-terra");
expect(dependencies.reconstructionOnly).toBe(true);
return { return {
success: true, success: true,
situationGraph: { nodes: [], edges: [] }, situationGraph: { nodes: [], edges: [] },