diff --git a/components/graph-update-view.jsx b/components/graph-update-view.jsx index 3c35ee7..b0a959a 100644 --- a/components/graph-update-view.jsx +++ b/components/graph-update-view.jsx @@ -66,6 +66,11 @@ export default function GraphUpdateView({ updateResult }) {
{node.kind} · {node.confidence}
+ {node.confidenceAssessment && ( +
+ evidence {node.confidenceAssessment.evidenceConfidence} · completeness {node.confidenceAssessment.completenessStatus} · conclusion {node.confidenceAssessment.conclusionConfidence} +
+ )} {(update?.previousStatus || update?.newStatus || node.status) && (
{update?.previousStatus ? `Previous status: ${update.previousStatus}` : null} diff --git a/components/situation-graph-view.jsx b/components/situation-graph-view.jsx index d7bd050..2ee7cac 100644 --- a/components/situation-graph-view.jsx +++ b/components/situation-graph-view.jsx @@ -40,6 +40,11 @@ function NodeGroup({ {node.label} {node.status} {node.confidence} + {node.confidenceAssessment?.completenessStatus && ( + + completeness: {node.confidenceAssessment.completenessStatus} + + )} {resolvedNodeIds.has(node.id) && ( resolved unknown )} @@ -59,6 +64,12 @@ function NodeGroup({ {node.description && node.description !== node.label && (

{node.description}

)} + {node.confidenceAssessment && ( +

+ evidence: {node.confidenceAssessment.evidenceConfidence} · + conclusion: {node.confidenceAssessment.conclusionConfidence} +

+ )} ))} diff --git a/docs/v0.6-atomicity-experiment.md b/docs/v0.6-atomicity-experiment.md index 4e05778..6c5ce96 100644 --- a/docs/v0.6-atomicity-experiment.md +++ b/docs/v0.6-atomicity-experiment.md @@ -119,6 +119,8 @@ rather than asking the full broad explanation node directly. Recursive reasoning is complete only when decomposition and reconstruction are both deterministic. +Confidence must not outrun completeness or evidence. + For this experiment, reconstruction now behaves as follows: - when a child unknown resolves, that child keeps its own resolved status and answer evidence @@ -132,10 +134,26 @@ For the current conservative completion rule: - **one resolved child** → parent becomes `provisional` with higher confidence, but remains unresolved - **all direct child unknowns resolved** → parent resolves deterministically with `high` confidence +The confidence model is now explicitly separated into: + +- **evidence confidence**: how trustworthy the currently attached support is +- **completeness**: whether the required direct child structure is empty, partial, or complete +- **conclusion confidence**: how strongly the current parent state is justified given both evidence and completeness + +Deterministic propagation rules now enforce: + +- one resolved child may raise evidence confidence +- unresolved direct children cap conclusion confidence +- contradictory direct children block high conclusion confidence +- duplicate evidence does not increase confidence +- status changes do not raise confidence on their own +- parent resolution still requires the separate completion rule + Example progression: - parent before: `unknown`, `medium` -- after resolving `How the two observations were measured`: parent becomes `provisional`, `high` +- after resolving `How the two observations were measured`: parent becomes `provisional`, `medium` +- evidence confidence becomes `high`, completeness becomes `partial`, conclusion confidence becomes `medium` - next sibling becomes selectable and the engine moves on without recreating the resolved child ## Interpretation diff --git a/lib/graph/apply-proposal.js b/lib/graph/apply-proposal.js index ead3021..98dfa5b 100644 --- a/lib/graph/apply-proposal.js +++ b/lib/graph/apply-proposal.js @@ -439,6 +439,30 @@ function appendUniqueValue(values = [], nextValue) { : values; } +function getNodeConfidenceAssessment(node) { + return ( + node?.confidenceAssessment || { + evidenceConfidence: node?.confidence ?? "medium", + completenessStatus: + node?.status === "resolved" + ? "complete" + : node?.status === "provisional" + ? "partial" + : "empty", + conclusionConfidence: + node?.status === "resolved" + ? (node?.confidence ?? "high") + : node?.status === "provisional" + ? (node?.confidence ?? "medium") + : "low", + } + ); +} + +function confidenceFromAssessment(assessment) { + return assessment?.conclusionConfidence ?? "medium"; +} + function upsertProposalNodeUpdate(proposalSnapshot, update) { const existing = proposalSnapshot.updatedNodes.find( (candidate) => candidate.nodeId === update.nodeId, @@ -521,16 +545,6 @@ function buildAncestorChain(graph, node) { queue.push(candidate.id); } } - - for (const edge of graph.edges || []) { - if ( - edge.fromNodeId !== parentNode.id && - edge.toNodeId === parentNode.id && - edge.relationship === "depends_on" - ) { - queue.push(edge.fromNodeId); - } - } } return chain; @@ -562,28 +576,83 @@ function computeParentProgressState(graph, parentNode) { const progressedChildren = childUnknowns.filter((child) => ["resolved", "provisional"].includes(child.status), ); + const contradictoryChildren = childUnknowns.filter( + (child) => child.status === "contradicted", + ); const totalChildren = childUnknowns.length; + const resolvedCount = resolvedChildren.length; + const unresolvedCount = childUnknowns.filter( + (child) => child.status !== "resolved", + ).length; + const beforeAssessment = getNodeConfidenceAssessment(parentNode); if (totalChildren === 0) { + const nextAssessment = { + evidenceConfidence: beforeAssessment.evidenceConfidence, + completenessStatus: beforeAssessment.completenessStatus, + conclusionConfidence: beforeAssessment.conclusionConfidence, + }; return { totalChildren, resolvedChildren, progressedChildren, + contradictoryChildren, nextStatus: parentNode.status, - nextConfidence: parentNode.confidence, + nextConfidence: confidenceFromAssessment(nextAssessment), + nextConfidenceAssessment: nextAssessment, parentResolved: parentNode.status === "resolved", + confidenceCapReason: "no_child_structure", reason: "Parent has no child unknowns to aggregate.", }; } + let nextAssessment; + let confidenceCapReason; + + if (contradictoryChildren.length > 0) { + nextAssessment = { + evidenceConfidence: resolvedCount > 0 ? "medium" : "low", + completenessStatus: resolvedCount === 0 ? "empty" : "partial", + conclusionConfidence: "low", + }; + confidenceCapReason = "contradictory_direct_children"; + } else if (resolvedCount === 0) { + nextAssessment = { + evidenceConfidence: "low", + completenessStatus: "empty", + conclusionConfidence: "low", + }; + confidenceCapReason = "no_resolved_direct_children"; + } else if (resolvedCount < totalChildren) { + nextAssessment = { + evidenceConfidence: "high", + completenessStatus: "partial", + conclusionConfidence: "medium", + }; + confidenceCapReason = "unresolved_direct_children_cap_conclusion"; + } else { + nextAssessment = { + evidenceConfidence: "high", + completenessStatus: "complete", + conclusionConfidence: "high", + }; + confidenceCapReason = null; + } + if (resolvedChildren.length === totalChildren) { return { totalChildren, resolvedChildren, progressedChildren, + contradictoryChildren, nextStatus: "resolved", - nextConfidence: "high", + nextConfidence: confidenceFromAssessment(nextAssessment), + nextConfidenceAssessment: nextAssessment, parentResolved: true, + resolvedDirectChildren: resolvedCount, + unresolvedDirectChildren: unresolvedCount, + contradictoryDirectChildren: contradictoryChildren.length, + confidenceCapReason, reason: "All direct child unknowns are resolved, so the parent can now resolve deterministically.", }; @@ -594,9 +663,15 @@ function computeParentProgressState(graph, parentNode) { totalChildren, resolvedChildren, progressedChildren, + contradictoryChildren, nextStatus: "provisional", - nextConfidence: "high", + nextConfidence: confidenceFromAssessment(nextAssessment), + nextConfidenceAssessment: nextAssessment, parentResolved: false, + resolvedDirectChildren: resolvedCount, + unresolvedDirectChildren: unresolvedCount, + contradictoryDirectChildren: contradictoryChildren.length, + confidenceCapReason, reason: "At least one direct child has been progressed, so the parent becomes provisional but remains unresolved until all direct children are resolved.", }; @@ -606,9 +681,15 @@ function computeParentProgressState(graph, parentNode) { totalChildren, resolvedChildren, progressedChildren, + contradictoryChildren, nextStatus: parentNode.status, - nextConfidence: parentNode.confidence, + nextConfidence: confidenceFromAssessment(nextAssessment), + nextConfidenceAssessment: nextAssessment, parentResolved: parentNode.status === "resolved", + resolvedDirectChildren: resolvedCount, + unresolvedDirectChildren: unresolvedCount, + contradictoryDirectChildren: contradictoryChildren.length, + confidenceCapReason, reason: "No direct child progress exists yet for the parent.", }; } @@ -635,6 +716,17 @@ export function propagateResolvedChildEvidence({ parentStatusAfter: null, parentConfidenceBefore: null, parentConfidenceAfter: null, + evidenceConfidenceBefore: null, + evidenceConfidenceAfter: null, + completenessBefore: null, + completenessAfter: null, + conclusionConfidenceBefore: null, + conclusionConfidenceAfter: null, + resolvedDirectChildren: 0, + unresolvedDirectChildren: 0, + contradictoryDirectChildren: 0, + confidenceCapReason: null, + ancestorPropagationStoppedReason: "no_resolved_child_propagation_needed", affectedAncestorIds: [], nextSelectedSibling: null, parentResolved: false, @@ -646,6 +738,7 @@ export function propagateResolvedChildEvidence({ syncParentChildReferences(graph); const propagationEvents = []; const affectedAncestorIds = new Set(); + let ancestorPropagationStoppedReason = "no_ancestor_state_changed"; for (const resolvedChildNode of resolvedChildNodes) { const liveChildNode = graph.nodes.find( @@ -662,10 +755,24 @@ export function propagateResolvedChildEvidence({ for (const ancestorNode of ancestorChain) { const beforeStatus = ancestorNode.status; const beforeConfidence = ancestorNode.confidence; + const beforeAssessment = getNodeConfidenceAssessment(ancestorNode); const progressState = computeParentProgressState(graph, ancestorNode); ancestorNode.status = progressState.nextStatus; ancestorNode.confidence = progressState.nextConfidence; + ancestorNode.confidenceAssessment = + progressState.nextConfidenceAssessment; + + if ( + beforeStatus === progressState.nextStatus && + beforeConfidence === progressState.nextConfidence && + JSON.stringify(beforeAssessment) === + JSON.stringify(progressState.nextConfidenceAssessment) + ) { + continue; + } + + ancestorPropagationStoppedReason = "ancestor_state_changed"; if (progressState.parentResolved) { ensureResolvedUnknownId(proposalSnapshot, ancestorNode.id); @@ -688,6 +795,19 @@ export function propagateResolvedChildEvidence({ parentStatusAfter: progressState.nextStatus, parentConfidenceBefore: beforeConfidence, parentConfidenceAfter: progressState.nextConfidence, + evidenceConfidenceBefore: beforeAssessment.evidenceConfidence, + evidenceConfidenceAfter: + progressState.nextConfidenceAssessment.evidenceConfidence, + completenessBefore: beforeAssessment.completenessStatus, + completenessAfter: + progressState.nextConfidenceAssessment.completenessStatus, + conclusionConfidenceBefore: beforeAssessment.conclusionConfidence, + conclusionConfidenceAfter: + progressState.nextConfidenceAssessment.conclusionConfidence, + resolvedDirectChildren: progressState.resolvedDirectChildren, + unresolvedDirectChildren: progressState.unresolvedDirectChildren, + contradictoryDirectChildren: progressState.contradictoryDirectChildren, + confidenceCapReason: progressState.confidenceCapReason, parentResolved: progressState.parentResolved, reason: progressState.reason, }); @@ -717,6 +837,17 @@ export function propagateResolvedChildEvidence({ parentStatusAfter: firstEvent?.parentStatusAfter ?? null, parentConfidenceBefore: firstEvent?.parentConfidenceBefore ?? null, parentConfidenceAfter: firstEvent?.parentConfidenceAfter ?? null, + evidenceConfidenceBefore: firstEvent?.evidenceConfidenceBefore ?? null, + evidenceConfidenceAfter: firstEvent?.evidenceConfidenceAfter ?? null, + completenessBefore: firstEvent?.completenessBefore ?? null, + completenessAfter: firstEvent?.completenessAfter ?? null, + conclusionConfidenceBefore: firstEvent?.conclusionConfidenceBefore ?? null, + conclusionConfidenceAfter: firstEvent?.conclusionConfidenceAfter ?? null, + resolvedDirectChildren: firstEvent?.resolvedDirectChildren ?? 0, + unresolvedDirectChildren: firstEvent?.unresolvedDirectChildren ?? 0, + contradictoryDirectChildren: firstEvent?.contradictoryDirectChildren ?? 0, + confidenceCapReason: firstEvent?.confidenceCapReason ?? null, + ancestorPropagationStoppedReason, affectedAncestorIds: [...affectedAncestorIds], nextSelectedSibling: siblingSelection?.status === "selected" ? siblingSelection.nodeId : null, @@ -1767,6 +1898,20 @@ export function applyValidatedProposal({ const parentStatusAfter = propagationResult.parentStatusAfter; const parentConfidenceBefore = propagationResult.parentConfidenceBefore; const parentConfidenceAfter = propagationResult.parentConfidenceAfter; + const evidenceConfidenceBefore = propagationResult.evidenceConfidenceBefore; + const evidenceConfidenceAfter = propagationResult.evidenceConfidenceAfter; + const completenessBefore = propagationResult.completenessBefore; + const completenessAfter = propagationResult.completenessAfter; + const conclusionConfidenceBefore = + propagationResult.conclusionConfidenceBefore; + const conclusionConfidenceAfter = propagationResult.conclusionConfidenceAfter; + const resolvedDirectChildren = propagationResult.resolvedDirectChildren; + const unresolvedDirectChildren = propagationResult.unresolvedDirectChildren; + const contradictoryDirectChildren = + propagationResult.contradictoryDirectChildren; + const confidenceCapReason = propagationResult.confidenceCapReason; + const ancestorPropagationStoppedReason = + propagationResult.ancestorPropagationStoppedReason; const affectedAncestorIds = propagationResult.affectedAncestorIds; const nextSelectedSibling = propagationResult.nextSelectedSibling; const parentResolved = propagationResult.parentResolved; @@ -1894,6 +2039,17 @@ export function applyValidatedProposal({ parentStatusAfter, parentConfidenceBefore, parentConfidenceAfter, + evidenceConfidenceBefore, + evidenceConfidenceAfter, + completenessBefore, + completenessAfter, + conclusionConfidenceBefore, + conclusionConfidenceAfter, + resolvedDirectChildren, + unresolvedDirectChildren, + contradictoryDirectChildren, + confidenceCapReason, + ancestorPropagationStoppedReason, affectedAncestorIds, nextSelectedSibling, parentResolved, diff --git a/lib/graph/orchestrator.js b/lib/graph/orchestrator.js index 4995d18..551ec03 100644 --- a/lib/graph/orchestrator.js +++ b/lib/graph/orchestrator.js @@ -110,6 +110,17 @@ function buildUpdateDiagnostics({ parentStatusAfter, parentConfidenceBefore, parentConfidenceAfter, + evidenceConfidenceBefore, + evidenceConfidenceAfter, + completenessBefore, + completenessAfter, + conclusionConfidenceBefore, + conclusionConfidenceAfter, + resolvedDirectChildren, + unresolvedDirectChildren, + contradictoryDirectChildren, + confidenceCapReason, + ancestorPropagationStoppedReason, affectedAncestorIds, nextSelectedSibling, parentResolved, @@ -163,6 +174,17 @@ function buildUpdateDiagnostics({ parentStatusAfter: parentStatusAfter ?? null, parentConfidenceBefore: parentConfidenceBefore ?? null, parentConfidenceAfter: parentConfidenceAfter ?? null, + evidenceConfidenceBefore: evidenceConfidenceBefore ?? null, + evidenceConfidenceAfter: evidenceConfidenceAfter ?? null, + completenessBefore: completenessBefore ?? null, + completenessAfter: completenessAfter ?? null, + conclusionConfidenceBefore: conclusionConfidenceBefore ?? null, + conclusionConfidenceAfter: conclusionConfidenceAfter ?? null, + resolvedDirectChildren: resolvedDirectChildren ?? 0, + unresolvedDirectChildren: unresolvedDirectChildren ?? 0, + contradictoryDirectChildren: contradictoryDirectChildren ?? 0, + confidenceCapReason: confidenceCapReason ?? null, + ancestorPropagationStoppedReason: ancestorPropagationStoppedReason ?? null, affectedAncestorIds: affectedAncestorIds ?? [], nextSelectedSibling: nextSelectedSibling ?? null, parentResolved: parentResolved ?? false, @@ -440,6 +462,17 @@ async function updateCaseWithDependencies(body, dependencies = {}) { parentStatusAfter: null, parentConfidenceBefore: null, parentConfidenceAfter: null, + evidenceConfidenceBefore: null, + evidenceConfidenceAfter: null, + completenessBefore: null, + completenessAfter: null, + conclusionConfidenceBefore: null, + conclusionConfidenceAfter: null, + resolvedDirectChildren: 0, + unresolvedDirectChildren: 0, + contradictoryDirectChildren: 0, + confidenceCapReason: null, + ancestorPropagationStoppedReason: null, affectedAncestorIds: [], nextSelectedSibling: null, parentResolved: false, @@ -508,6 +541,20 @@ async function updateCaseWithDependencies(body, dependencies = {}) { parentStatusAfter: applicationResult.parentStatusAfter, parentConfidenceBefore: applicationResult.parentConfidenceBefore, parentConfidenceAfter: applicationResult.parentConfidenceAfter, + evidenceConfidenceBefore: applicationResult.evidenceConfidenceBefore, + evidenceConfidenceAfter: applicationResult.evidenceConfidenceAfter, + completenessBefore: applicationResult.completenessBefore, + completenessAfter: applicationResult.completenessAfter, + conclusionConfidenceBefore: + applicationResult.conclusionConfidenceBefore, + conclusionConfidenceAfter: applicationResult.conclusionConfidenceAfter, + resolvedDirectChildren: applicationResult.resolvedDirectChildren, + unresolvedDirectChildren: applicationResult.unresolvedDirectChildren, + contradictoryDirectChildren: + applicationResult.contradictoryDirectChildren, + confidenceCapReason: applicationResult.confidenceCapReason, + ancestorPropagationStoppedReason: + applicationResult.ancestorPropagationStoppedReason, affectedAncestorIds: applicationResult.affectedAncestorIds, nextSelectedSibling: applicationResult.nextSelectedSibling, parentResolved: applicationResult.parentResolved, @@ -560,6 +607,17 @@ async function updateCaseWithDependencies(body, dependencies = {}) { parentStatusAfter: null, parentConfidenceBefore: null, parentConfidenceAfter: null, + evidenceConfidenceBefore: null, + evidenceConfidenceAfter: null, + completenessBefore: null, + completenessAfter: null, + conclusionConfidenceBefore: null, + conclusionConfidenceAfter: null, + resolvedDirectChildren: 0, + unresolvedDirectChildren: 0, + contradictoryDirectChildren: 0, + confidenceCapReason: null, + ancestorPropagationStoppedReason: null, affectedAncestorIds: [], nextSelectedSibling: null, parentResolved: false, diff --git a/lib/graph/schema.js b/lib/graph/schema.js index 680a5bc..fbe173a 100644 --- a/lib/graph/schema.js +++ b/lib/graph/schema.js @@ -36,6 +36,20 @@ export const ConfidenceLevel = /** @type {const} */ ({ high: "high", }); +export const CompletenessStatus = /** @type {const} */ ({ + empty: "empty", + partial: "partial", + complete: "complete", +}); + +export const confidenceAssessmentSchema = z + .object({ + evidenceConfidence: z.enum(Object.values(ConfidenceLevel)), + completenessStatus: z.enum(Object.values(CompletenessStatus)), + conclusionConfidence: z.enum(Object.values(ConfidenceLevel)), + }) + .strict(); + // ── SituationNode ──────────────────────────────────── export const situationNodeSchema = z.object({ @@ -45,6 +59,7 @@ export const situationNodeSchema = z.object({ kind: z.enum(Object.values(SituationKind)), status: z.enum(Object.values(SituationStatus)), confidence: z.enum(Object.values(ConfidenceLevel)), + confidenceAssessment: confidenceAssessmentSchema.optional(), value: z.union([z.string(), z.number(), z.null()]).nullable().optional(), unit: z.string().nullable().optional(), evidenceIds: z.array(z.string()).default([]), @@ -188,6 +203,7 @@ export function makeNode(opts) { kind: opts.kind ?? "observation", status: opts.status ?? "unknown", confidence: opts.confidence ?? "medium", + confidenceAssessment: opts.confidenceAssessment, value: opts.value ?? null, unit: opts.unit ?? null, evidenceIds: opts.evidenceIds ?? [], diff --git a/tests/graph/confidence-propagation.test.js b/tests/graph/confidence-propagation.test.js new file mode 100644 index 0000000..c8e6440 --- /dev/null +++ b/tests/graph/confidence-propagation.test.js @@ -0,0 +1,179 @@ +import { describe, expect, it } from "vitest"; +import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js"; +import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js"; + +function makeFixture() { + const parent = makeNode({ + id: "n-parent", + label: "Explanation for why revenue increased while cash fell", + description: + "Need to understand what change or event could explain why these observations differ, because that is needed to investigate their relationship.", + kind: "unknown", + status: "unknown", + confidence: "medium", + }); + const children = [ + makeNode({ + id: "n-child-1", + label: "How the two observations were measured", + description: "Need evidence about the measure used for each observation.", + kind: "unknown", + status: "unknown", + confidence: "medium", + parentId: parent.id, + }), + makeNode({ + id: "n-child-2", + label: "Whether the two observations reflect different timing", + description: + "Need to know whether the two observations reflect different timing.", + kind: "unknown", + status: "unknown", + confidence: "medium", + parentId: parent.id, + }), + makeNode({ + id: "n-child-3", + label: "Possible change mainly affecting revenue", + description: + "Need to know whether a possible change mainly affected revenue.", + kind: "unknown", + status: "unknown", + confidence: "medium", + parentId: parent.id, + }), + makeNode({ + id: "n-child-4", + label: "Possible one-off event during the period", + description: + "Need to know whether a possible one-off event happened during the period.", + kind: "unknown", + status: "unknown", + confidence: "medium", + parentId: parent.id, + }), + ]; + + return makeGraph({ + centralStatement: "Revenue increased while cash fell.", + nodes: [parent, ...children], + edges: children.map((child, index) => + makeEdge({ + id: `e-${index + 1}`, + fromNodeId: child.id, + toNodeId: parent.id, + relationship: "depends_on", + description: `${child.label} feeds the parent.`, + }), + ), + activeUnknownNodeId: "n-child-1", + resolvedNodeIds: [], + currentSummary: "confidence propagation fixture", + }); +} + +function makeProposal({ resolvedIds, contradictedIds = [] }) { + return { + addedNodes: [ + makeNode({ + id: "n-anchor", + label: "Update anchor", + description: + "Anchor state introduced by the answer because the update must contain a meaningful change.", + kind: "state", + status: "known", + confidence: "low", + }), + ], + updatedNodes: [ + ...resolvedIds.map((id) => ({ + nodeId: id, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: `answer:${id}`, + reason: "resolved child", + })), + ...contradictedIds.map((id) => ({ + nodeId: id, + previousStatus: "unknown", + newStatus: "contradicted", + previousValue: null, + newValue: `contradiction:${id}`, + reason: "contradictory child evidence", + })), + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: resolvedIds, + affectedNodeIds: [], + selectedQuestion: null, + }; +} + +describe("confidence propagation", () => { + it("one of four children resolved does not yield high conclusion confidence", () => { + const result = applyValidatedProposal({ + situationGraph: makeFixture(), + proposal: makeProposal({ resolvedIds: ["n-child-1"] }), + previousQuestion: + "What evidence would clarify how the two observations were measured?", + answer: "Same accounting period and same management accounts.", + }); + + const parent = result.updatedSituationGraph.nodes.find( + (n) => n.id === "n-parent", + ); + expect(parent.status).toBe("provisional"); + expect(parent.confidence).toBe("medium"); + expect(parent.confidenceAssessment).toEqual({ + evidenceConfidence: "high", + completenessStatus: "partial", + conclusionConfidence: "medium", + }); + expect(result.confidenceCapReason).toBe( + "unresolved_direct_children_cap_conclusion", + ); + }); + + it("all children resolved with coherent evidence may yield high confidence", () => { + const result = applyValidatedProposal({ + situationGraph: makeFixture(), + proposal: makeProposal({ + resolvedIds: ["n-child-1", "n-child-2", "n-child-3", "n-child-4"], + }), + previousQuestion: + "What evidence would clarify how the two observations were measured?", + answer: "All direct child questions are answered.", + }); + + const parent = result.updatedSituationGraph.nodes.find( + (n) => n.id === "n-parent", + ); + expect(parent.status).toBe("resolved"); + expect(parent.confidenceAssessment).toEqual({ + evidenceConfidence: "high", + completenessStatus: "complete", + conclusionConfidence: "high", + }); + }); + + it("contradictory child evidence prevents high confidence", () => { + const result = applyValidatedProposal({ + situationGraph: makeFixture(), + proposal: makeProposal({ + resolvedIds: ["n-child-1"], + contradictedIds: ["n-child-2"], + }), + previousQuestion: + "What evidence would clarify how the two observations were measured?", + answer: "One child resolved, another contradicted.", + }); + + const parent = result.updatedSituationGraph.nodes.find( + (n) => n.id === "n-parent", + ); + expect(parent.confidenceAssessment.conclusionConfidence).toBe("low"); + expect(result.confidenceCapReason).toBe("contradictory_direct_children"); + }); +}); diff --git a/tests/graph/upward-propagation.test.js b/tests/graph/upward-propagation.test.js index 2e6731e..ed0658f 100644 --- a/tests/graph/upward-propagation.test.js +++ b/tests/graph/upward-propagation.test.js @@ -255,7 +255,16 @@ describe("upward propagation", () => { expect(result.parentStatusBefore).toBe("unknown"); expect(result.parentStatusAfter).toBe("provisional"); expect(result.parentConfidenceBefore).toBe("medium"); - expect(result.parentConfidenceAfter).toBe("high"); + expect(result.parentConfidenceAfter).toBe("medium"); + expect(result.evidenceConfidenceBefore).toBe("medium"); + expect(result.evidenceConfidenceAfter).toBe("high"); + expect(result.completenessBefore).toBe("empty"); + expect(result.completenessAfter).toBe("partial"); + expect(result.conclusionConfidenceBefore).toBe("low"); + expect(result.conclusionConfidenceAfter).toBe("medium"); + expect(result.confidenceCapReason).toBe( + "unresolved_direct_children_cap_conclusion", + ); expect(result.parentResolved).toBe(false); expect(result.affectedAncestorIds).toContain(ids.parent); expect(result.affectedAncestorIds).toContain(ids.ancestor); @@ -276,7 +285,12 @@ describe("upward propagation", () => { ); expect(parentNode).toMatchObject({ status: "provisional", - confidence: "high", + confidence: "medium", + confidenceAssessment: { + evidenceConfidence: "high", + completenessStatus: "partial", + conclusionConfidence: "medium", + }, }); const ancestorNode = result.updatedSituationGraph.nodes.find( @@ -284,7 +298,12 @@ describe("upward propagation", () => { ); expect(ancestorNode).toMatchObject({ status: "provisional", - confidence: "high", + confidence: "low", + confidenceAssessment: { + evidenceConfidence: "low", + completenessStatus: "empty", + conclusionConfidence: "low", + }, }); const resolvedChild = result.updatedSituationGraph.nodes.find( @@ -387,6 +406,14 @@ describe("upward propagation", () => { expect(result.resolvedUnknownNodeIds).toContain(ids.parent); expect( result.updatedSituationGraph.nodes.find((node) => node.id === ids.parent), - ).toMatchObject({ status: "resolved", confidence: "high" }); + ).toMatchObject({ + status: "resolved", + confidence: "high", + confidenceAssessment: { + evidenceConfidence: "high", + completenessStatus: "complete", + conclusionConfidence: "high", + }, + }); }); });