feat(confidence-engine): isolate finding-informed understanding

This commit is contained in:
2026-08-28 08:16:37 +01:00
parent c45b703b3a
commit e221bd3bf8
3 changed files with 186 additions and 2 deletions
+138
View File
@@ -222,6 +222,34 @@ function makeProposal(overrides = {}) {
};
}
// ── v0.49: finding factories for post-authoritative seam test ──
/** Finding with null disposition → eligible provisional working interpretation */
function nullDispositionFinding() {
return {
id: "finding-a",
proposition: "Production volume increased by 12% in Q3.",
status: "provisional",
userDisposition: null,
originatingTargetNodeId: "n-unknown",
contributionId: "contrib-test-a",
sourceObservation: "user_input",
};
}
/** Finding with not_relevant disposition → excluded from understanding */
function notRelevantFinding() {
return {
id: "finding-b",
proposition: "Office humidity levels fluctuate seasonally.",
status: "provisional",
userDisposition: "not_relevant",
originatingTargetNodeId: "n-unknown",
contributionId: "contrib-test-b",
sourceObservation: "user_input",
};
}
function makeComparabilityScenarioGraph() {
return makeGraph({
centralStatement:
@@ -1474,6 +1502,116 @@ describe("lib/graph/orchestrator startCase", () => {
);
});
// ── v0.49: post-authoritative finding-informed understanding seam test ──
it("post-authoritative findings modify summary but preserve authoritative graph result invariant", async () => {
const { updateCase } = await import("@/lib/graph/orchestrator.js");
const mockGraph = makeGraph({
centralStatement: "Complaint counts increased while production also increased.",
nodes: [
makeNode({
id: "n-unknown",
label: "Complaint rate denominator",
description: "Need the denominator for the complaint rate",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
edges: [],
activeUnknownNodeId: "n-unknown",
resolvedNodeIds: [],
currentSummary: "Nodes: 1 unknown | Edges: 0 total",
});
const mockApplicationResult = {
success: true,
updatedSituationGraph: mockGraph,
graphUpdate: { addedNodes: [], updatedNodes: [] },
selectedQuestion: null,
affectedNodeIds: [],
resolvedUnknownNodeIds: ["n-unknown"],
previousActiveUnknownNodeId: "n-unknown",
newActiveUnknownNodeId: null,
changesApplied: 1,
reasoningState: {
comparabilityStatus: "uncertain",
comparabilityReason: "deferred",
comparabilityEvidence: [],
relationshipStatus: "insufficient_information",
relationshipReason: "deferred",
relationshipAssessed: false,
contradictionReasoningAllowed: false,
reasoningStages: [],
},
previousReasoningState: null,
graphReferenceValidation: { valid: true, errors: [] },
diagnosticEvidence: {},
};
const applyValidatedProposal = vi.fn().mockReturnValue(mockApplicationResult);
const provider = {
generateReconstruction: vi.fn().mockResolvedValue(makeProposal()),
};
// Control: no findings → summary should remain as authoritative gives
const controlRequest = makeUpdateRequest({ situationGraph: mockGraph });
const controlResult = await updateCase(controlRequest, {
provider,
config: MOCK_CONFIG,
applyValidatedProposal,
applyProposal: true,
});
// Treatment A: eligible null disposition Finding (schema expects "findings" key)
const treatmentAResult = await updateCase(
{ ...controlRequest, findings: [nullDispositionFinding()] },
{ provider, config: MOCK_CONFIG, applyValidatedProposal, applyProposal: true },
);
// Treatment B: not_relevant Finding (should NOT affect summary)
const treatmentBResult = await updateCase(
{ ...controlRequest, findings: [notRelevantFinding()] },
{ provider, config: MOCK_CONFIG, applyValidatedProposal, applyProposal: true },
);
// 1. Authoritative graph result is structurally identical (control vs treatment)
expect(controlResult.success).toBe(true);
expect(treatmentAResult.success).toBe(true);
expect(treatmentBResult.success).toBe(true);
expect(controlResult.updatedSituationGraph).toEqual(mockApplicationResult.updatedSituationGraph);
expect(treatmentAResult.updatedSituationGraph).toEqual(mockApplicationResult.updatedSituationGraph);
expect(treatmentBResult.updatedSituationGraph).toEqual(mockApplicationResult.updatedSituationGraph);
// 2. summary differs: control == authoritative, treatmentA has evidence appended
expect(controlResult.summary).toBe(mockGraph.currentSummary);
expect(treatmentAResult.summary).not.toBe(mockGraph.currentSummary);
expect(treatmentAResult.summary).toContain("Evidence");
expect(treatmentAResult.summary).toContain(
"Production volume increased by 12% in Q3.",
);
// 3. not_relevant Finding does NOT affect summary
expect(treatmentBResult.summary).toBe(mockGraph.currentSummary);
// 4. selectedQuestion / frontier fields are identical across all cases
expect(controlResult.selectedQuestion).toEqual(treatmentAResult.selectedQuestion);
expect(controlResult.selectedQuestion).toEqual(treatmentBResult.selectedQuestion);
expect(controlResult.resolvedUnknownNodeIds).toEqual(treatmentAResult.resolvedUnknownNodeIds);
expect(controlResult.resolvedUnknownNodeIds).toEqual(treatmentBResult.resolvedUnknownNodeIds);
// 5. authoritative stages and proposal are identical
expect(controlResult.stage).toBe(treatmentAResult.stage);
expect(controlResult.stage).toBe(treatmentBResult.stage);
expect(controlResult.proposal).toEqual(treatmentAResult.proposal);
expect(controlResult.proposal).toEqual(treatmentBResult.proposal);
// 6. Findings still never enter buildGraphUpdatePrompt (verify provider was called)
expect(provider.generateReconstruction).toHaveBeenCalledTimes(3);
});
it("startCase no longer copies analysis nextQuestion directly when a graph-backed question exists", async () => {
mockAnalyseScenario.mockResolvedValue(makeAnalysisResult());
const { startCase } = await import("@/lib/graph/orchestrator.js");