wip(confidence-engine): checkpoint open-question presentation work

This commit is contained in:
2026-10-09 07:46:19 +01:00
parent f74fe6de10
commit 6bc8be6edb
19 changed files with 812 additions and 22 deletions
+110 -4
View File
@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { normaliseAnalysisResponse } from "@/lib/reconstruction/compatibility.js";
import { reconstructionGenerationJsonSchema } from "@/lib/reconstruction/schema.js";
const mockGenerateReconstruction = vi.fn();
@@ -17,6 +18,7 @@ vi.mock("@/lib/llm/provider.js", () => ({
getProvider: () => ({
generateReconstruction: (...args) => mockGenerateReconstruction(...args),
}),
getProviderModelName: () => "test-model",
}));
vi.mock("@/lib/reconstruction/prompt.js", () => ({
@@ -134,13 +136,117 @@ describe("analyseScenario compatibility", () => {
});
expect(result.success).toBe(true);
expect(injectedProvider.generateReconstruction).toHaveBeenCalledWith(
"prompt",
"experiment-model",
);
expect(injectedProvider.generateReconstruction).toHaveBeenCalledTimes(1);
const [prompt, modelName, outputSchema, options] =
injectedProvider.generateReconstruction.mock.calls[0];
expect(prompt).toBe("prompt");
expect(modelName).toBe("experiment-model");
expect(outputSchema).toEqual(reconstructionGenerationJsonSchema);
expect(options.outputZodSchema.safeParse).toBeTypeOf("function");
expect(
options.outputZodSchema.safeParse({
inputClassification: {
primaryType: "other",
secondaryTypes: [],
reasoningModes: [],
classificationReason: "reason",
confidence: "medium",
},
reconstruction: {
summary: "summary",
actors: [],
systemsOrObjects: [],
expectedStates: [],
observedStates: [],
differences: [],
knownTransitions: [],
unexplainedTransitions: [],
contradictions: [],
importantUnknowns: [],
plausibleInterpretations: [],
relationships: [],
},
evidence: [],
nextQuestion: {
id: "q1",
question: "What next?",
targets: [],
reason: "reason",
expectedInformationValue: "low",
reasoningMode: "other",
},
}).success,
).toBe(true);
expect(mockGenerateReconstruction).not.toHaveBeenCalled();
});
it("preserves canonical reconstruction and reports invalid presentation nonfatally", async () => {
const response = {
inputClassification: {
primaryType: "other",
secondaryTypes: [],
reasoningModes: [],
classificationReason: "reason",
confidence: "medium",
},
reconstruction: {
summary: "summary",
actors: [],
systemsOrObjects: [],
expectedStates: [],
observedStates: [],
differences: [],
knownTransitions: [],
unexplainedTransitions: [],
contradictions: [],
importantUnknowns: [
{
id: "unknown-1",
description: "Whether workers can be recruited in time.",
question: "Whether workers can be recruited in time?",
confidence: "high",
},
],
plausibleInterpretations: [],
relationships: [],
},
evidence: [],
nextQuestion: {
id: "q1",
question: "What recruitment evidence exists?",
targets: ["unknown-1"],
reason: "reason",
expectedInformationValue: "high",
reasoningMode: "other",
},
};
const injectedProvider = {
generateReconstruction: vi.fn().mockResolvedValue(response),
};
const { analyseScenario } = await import("@/lib/analysis.js");
const result = await analyseScenario("Scenario text", {
reconstructionProvider: injectedProvider,
reconstructionModelName: "experiment-model",
});
expect(result.success).toBe(true);
expect(result.validationStatus).toBe("valid");
expect(result.presentationValidationStatus).toBe("partial");
expect(result.reconstruction.importantUnknowns[0]).toEqual({
id: "unknown-1",
description: "Whether workers can be recruited in time.",
confidence: "high",
});
expect(result.presentationValidationIssues).toEqual([
expect.objectContaining({
category: "question_indirect_proposition",
unknownId: "unknown-1",
severity: "nonfatal",
}),
]);
});
it("preserves an attempted provider API path on provider failure", async () => {
const providerError = new Error("Provider failed");
providerError.providerApiPath = "/api/chat";
@@ -0,0 +1,142 @@
import { describe, expect, it } from "vitest";
import { promises as fs } from "node:fs";
import { join } from "node:path";
import {
reconstructionGenerationJsonSchema,
reconstructionGenerationSchema,
reconstructionV2Schema,
} from "@/lib/reconstruction/schema.js";
import {
validateOpenQuestionCandidate,
validateReconstructionOpenQuestions,
} from "@/lib/reconstruction/open-question-presentation.js";
function makeAnalysisResponse(questionValue, includeQuestion = true) {
const unknown = {
id: "unknown-workforce",
description:
"Whether necessary skilled production workers can be recruited within the required project timeframe.",
confidence: "high",
};
if (includeQuestion) unknown.question = questionValue;
return {
inputClassification: {
primaryType: "other",
secondaryTypes: [],
reasoningModes: [],
classificationReason: "A delivery uncertainty is central to the situation.",
confidence: "high",
},
reconstruction: {
summary: "Delivery depends on recruiting skilled production workers.",
actors: [],
systemsOrObjects: [],
expectedStates: [],
observedStates: [],
differences: [],
knownTransitions: [],
unexplainedTransitions: [],
contradictions: [],
importantUnknowns: [unknown],
plausibleInterpretations: [],
relationships: [],
},
evidence: [],
nextQuestion: {
id: "next-1",
question: "What recruitment evidence is available?",
targets: ["unknown-workforce"],
reason: "Recruitment feasibility is unresolved.",
expectedInformationValue: "high",
reasoningMode: "other",
},
};
}
describe("new reconstruction generation contract", () => {
it("requires a non-empty question for every generated important unknown", () => {
expect(
reconstructionGenerationSchema.safeParse(
makeAnalysisResponse(undefined, false),
).success,
).toBe(false);
expect(
reconstructionGenerationSchema.safeParse(makeAnalysisResponse(" ")).success,
).toBe(false);
expect(
reconstructionGenerationSchema.safeParse(
makeAnalysisResponse(
"Can the necessary skilled production workers be recruited within the required project timeframe?",
),
).success,
).toBe(true);
const unknownSchema =
reconstructionGenerationJsonSchema.properties.reconstruction.properties
.importantUnknowns.items;
expect(unknownSchema.required).toContain("description");
expect(unknownSchema.required).toContain("question");
expect(unknownSchema.properties.question.minLength).toBe(1);
});
it("keeps the durable compatibility schema tolerant of a missing question", () => {
expect(
reconstructionV2Schema.safeParse(makeAnalysisResponse(undefined, false)).success,
).toBe(true);
});
});
describe("Open Question presentation validation", () => {
it("accepts a trimmed direct interrogative", () => {
expect(
validateOpenQuestionCandidate(
" Can the necessary workers be recruited in time? ",
),
).toEqual({
valid: true,
question: "Can the necessary workers be recruited in time?",
});
});
it("uses canonical UI fallback without a technical explanation", async () => {
const source = await fs.readFile(
join(process.cwd(), "components/reasoning-workspace.jsx"),
"utf8",
);
expect(source).toContain("const openQuestion = node.openQuestion || node.label;");
expect(source).not.toContain(
"Canonical uncertainty — direct question wording unavailable.",
);
expect(source).toContain("startFocused(n.id);");
});
it.each([
["question_missing", undefined, false],
["question_empty", " ", true],
["question_indirect_proposition", "Whether workers can be recruited in time", true],
["question_indirect_proposition", "Whether workers can be recruited in time?", true],
["question_not_interrogative", "Workers can be recruited in time.", true],
])("classifies %s nonfatally", (category, candidate, includeQuestion) => {
const input = makeAnalysisResponse(candidate, includeQuestion).reconstruction;
const canonicalBefore = input.importantUnknowns[0].description;
const idBefore = input.importantUnknowns[0].id;
const result = validateReconstructionOpenQuestions(input);
expect(result.reconstruction.importantUnknowns[0]).not.toHaveProperty("question");
expect(result.reconstruction.importantUnknowns[0].description).toBe(
canonicalBefore,
);
expect(result.reconstruction.importantUnknowns[0].id).toBe(idBefore);
expect(result.issues).toEqual([
expect.objectContaining({
category,
unknownId: idBefore,
severity: "nonfatal",
path: ["reconstruction", "importantUnknowns", 0, "question"],
}),
]);
});
});