From 25a989450c34031aa690c8e934bf0bdc4b4c1e12 Mon Sep 17 00:00:00 2001 From: robbond Date: Sun, 2 Aug 2026 17:06:34 +0100 Subject: [PATCH] feat: advance reasoning after comparability is resolved --- components/graph-update-view.jsx | 39 ++++++ docs/v0.6-comparability-experiment.md | 6 + lib/graph/apply-proposal.js | 94 +++++++++++++- lib/graph/orchestrator.js | 33 ++++- lib/graph/question-formulator.js | 71 ++++++++++- lib/graph/schema.js | 21 ++++ tests/graph/apply-proposal.test.js | 174 ++++++++++++++++++++++++++ tests/graph/orchestrator.test.js | 152 ++++++++++++++++++++++ tests/ui/scenario-form.test.jsx | 64 ++++++++++ 9 files changed, 649 insertions(+), 5 deletions(-) diff --git a/components/graph-update-view.jsx b/components/graph-update-view.jsx index 23a0109..3c35ee7 100644 --- a/components/graph-update-view.jsx +++ b/components/graph-update-view.jsx @@ -28,6 +28,8 @@ export default function GraphUpdateView({ updateResult }) { proposal, previousSituationGraph, updatedSituationGraph, + reasoningState, + previousReasoningState, } = updateResult; const newlySurfacedUnknownNodeIds = (proposal.addedNodes || []) @@ -109,6 +111,23 @@ export default function GraphUpdateView({ updateResult }) { : null, ].filter(Boolean); + const previousComparabilityStatus = + previousReasoningState?.comparabilityStatus || + previousSituationGraph?.reasoningState?.comparabilityStatus || + null; + const newComparabilityStatus = + reasoningState?.comparabilityStatus || + updatedSituationGraph?.reasoningState?.comparabilityStatus || + null; + const relationshipStatus = + reasoningState?.relationshipStatus || + updatedSituationGraph?.reasoningState?.relationshipStatus || + null; + const reasoningStagesAfter = + reasoningState?.reasoningStages || + updatedSituationGraph?.reasoningState?.reasoningStages || + []; + return (
@@ -134,12 +153,32 @@ export default function GraphUpdateView({ updateResult }) { {selectedQuestion.question}
)} + {previousComparabilityStatus && newComparabilityStatus && ( +
+ Comparability:{" "} + {previousComparabilityStatus} → {newComparabilityStatus} +
+ )} + {relationshipStatus && ( +
+ Relationship status:{" "} + {relationshipStatus} +
+ )} {!selectedQuestion?.question && !newActiveUnknownNodeId && previousActiveUnknownNodeId && (
Next question status: No next question selected yet.
)} + {reasoningStagesAfter.length > 0 && ( +
+ Reasoning stages:{" "} + {reasoningStagesAfter + .map((stage) => `${stage.stage}: ${stage.status}`) + .join(" → ")} +
+ )} { + const relationshipFallback = formulateTieResolutionQuestion({ + graph: updatedSituationGraph, + }); + return relationshipFallback?.question + ? { + nodeId: null, + question: relationshipFallback.question, + reason: relationshipFallback.reason, + strategy: relationshipFallback.strategy, + investigationStrategy: + relationshipFallback.investigationStrategy, + } + : null; + })(); const resultGraphValidation = situationGraphSchema.safeParse( updatedSituationGraph, @@ -725,10 +812,13 @@ export function applyValidatedProposal({ situationGraph, proposal }) { graphUpdate: validatedProposal, affectedNodeIds, resolvedUnknownNodeIds: validatedProposal.resolvedUnknownNodeIds, + resolvedReasoningNodeIds: reasoningResolution.resolvedReasoningNodeIds, previousActiveUnknownNodeId, newActiveUnknownNodeId, selectedQuestion: finalSelectedQuestion, changesApplied: buildChangesApplied(validatedProposal, affectedNodeIds), graphReferenceValidation: resultReferenceValidation, + previousReasoningState: reasoningResolution.previousReasoningState, + reasoningState: nextReasoningState, }; } diff --git a/lib/graph/orchestrator.js b/lib/graph/orchestrator.js index 21d0c64..d2f4ccf 100644 --- a/lib/graph/orchestrator.js +++ b/lib/graph/orchestrator.js @@ -15,7 +15,10 @@ import { import { buildInitialGraph, describeGraph } from "./builder.js"; import { applyValidatedProposal } from "./apply-proposal.js"; import { buildGraphUpdatePrompt } from "./prompt-builder.js"; -import { formulateTieResolutionQuestion } from "./question-formulator.js"; +import { + buildReasoningState, + formulateTieResolutionQuestion, +} from "./question-formulator.js"; import { parseGraphUpdateProposal } from "./update-proposal.js"; import { explainUnknownSelection, @@ -83,6 +86,9 @@ function buildUpdateDiagnostics({ graphReferenceValidation, selectedQuestion, unknownSelectionExplanation, + previousReasoningState, + reasoningState, + resolvedReasoningNodeIds, }) { return { promptVersion: promptVersion ?? "v0.4", @@ -100,6 +106,14 @@ function buildUpdateDiagnostics({ selectedQuestion?.investigationStrategy ?? selectedQuestion?.strategy ?? null, + previousComparabilityStatus: + previousReasoningState?.comparabilityStatus ?? null, + comparabilityStatus: reasoningState?.comparabilityStatus ?? null, + relationshipStatus: reasoningState?.relationshipStatus ?? null, + relationshipAssessed: reasoningState?.relationshipAssessed ?? null, + reasoningStagesBefore: previousReasoningState?.reasoningStages ?? [], + reasoningStagesAfter: reasoningState?.reasoningStages ?? [], + resolvedReasoningNodeIds: resolvedReasoningNodeIds ?? [], unknownSelectionExplanation: unknownSelectionExplanation ?? null, }; } @@ -159,6 +173,12 @@ export async function startCase(body) { activeUnknownNodeId, resolvedNodeIds: [], currentSummary, + reasoningState: buildReasoningState({ + centralStatement: scenario, + nodes: initialGraph.nodes, + edges: initialGraph.edges, + resolvedNodeIds: [], + }), }); situationGraphSchema.parse(situationGraph); @@ -322,6 +342,8 @@ async function updateCaseWithDependencies(body, dependencies = {}) { const applicationResult = applyProposalUpdate({ situationGraph, proposal: parsedProposal.proposal, + previousQuestion, + answer, }); if (!applicationResult.success) { @@ -338,6 +360,9 @@ async function updateCaseWithDependencies(body, dependencies = {}) { graph: situationGraph, graphReferenceValidation: graphReferenceValidation, selectedQuestion: null, + previousReasoningState: buildReasoningState(situationGraph), + reasoningState: buildReasoningState(situationGraph), + resolvedReasoningNodeIds: [], unknownSelectionExplanation: explainUnknownSelection( situationGraph, situationGraph.resolvedNodeIds || [], @@ -372,6 +397,9 @@ async function updateCaseWithDependencies(body, dependencies = {}) { graph: applicationResult.updatedSituationGraph, graphReferenceValidation: applicationResult.graphReferenceValidation, selectedQuestion: applicationResult.selectedQuestion, + previousReasoningState: applicationResult.previousReasoningState, + reasoningState: applicationResult.reasoningState, + resolvedReasoningNodeIds: applicationResult.resolvedReasoningNodeIds, unknownSelectionExplanation: buildUnknownSelectionDiagnostics( applicationResult.updatedSituationGraph, applicationResult.updatedSituationGraph.resolvedNodeIds || [], @@ -393,6 +421,9 @@ async function updateCaseWithDependencies(body, dependencies = {}) { graph: situationGraph, graphReferenceValidation, selectedQuestion: null, + previousReasoningState: buildReasoningState(situationGraph), + reasoningState: buildReasoningState(situationGraph), + resolvedReasoningNodeIds: [], unknownSelectionExplanation: buildUnknownSelectionDiagnostics( situationGraph, situationGraph.resolvedNodeIds || [], diff --git a/lib/graph/question-formulator.js b/lib/graph/question-formulator.js index 1e3eaf8..7497430 100644 --- a/lib/graph/question-formulator.js +++ b/lib/graph/question-formulator.js @@ -152,7 +152,28 @@ function analyseObservationText(text) { }; } +export const COMPARABILITY_REASONING_NODE_ID = "reasoning:comparability"; + +function readStoredComparabilityState(graph) { + const reasoningState = graph?.reasoningState; + if (!reasoningState?.comparabilityStatus) return null; + + return { + comparabilityStatus: reasoningState.comparabilityStatus, + reason: + reasoningState.comparabilityReason || + "Comparability state was carried forward from earlier reasoning.", + contradictionReasoningAllowed: + reasoningState.comparabilityStatus === "confirmed", + }; +} + export function assessComparability(graph) { + const storedState = readStoredComparabilityState(graph); + if (storedState) { + return storedState; + } + const observations = collectObservationNodes(graph); const centralText = normaliseText(graph?.centralStatement || ""); const profiles = observations.map((node) => @@ -308,6 +329,9 @@ function classifyObservationRelationshipWhenComparable(graph) { const directions = profiles.map((profile) => extractObservationDirection(profile), ); + const conceptUnion = new Set(conceptSets.flat()); + const hasRevenueCashPair = + conceptUnion.has("revenue") && conceptUnion.has("cash"); if ( sharedConcepts.size > 0 && @@ -336,6 +360,19 @@ function classifyObservationRelationshipWhenComparable(graph) { }; } + if ( + hasRevenueCashPair && + directions.every((direction) => direction !== "unknown") + ) { + return { + relationshipStatus: "potentially_related", + reason: + "The observations concern connected business signals but do not establish a direct contradiction or cause.", + contradictionReasoningAllowed: false, + questionRequired: true, + }; + } + if ( sharedConcepts.size === 0 && directions.every((direction) => direction !== "unknown") @@ -396,6 +433,30 @@ export function classifyObservationRelationship(graph) { }; } +export function buildReasoningState(graph, overrides = {}) { + const relationship = classifyObservationRelationship({ + ...graph, + reasoningState: { + ...(graph?.reasoningState || {}), + ...(overrides || {}), + }, + }); + + return { + comparabilityStatus: relationship.reasoningStages[0]?.status ?? null, + comparabilityReason: relationship.reasoningStages[0]?.outcome ?? null, + comparabilityEvidence: + overrides.comparabilityEvidence ?? + graph?.reasoningState?.comparabilityEvidence ?? + [], + relationshipStatus: relationship.relationshipStatus, + relationshipReason: relationship.reason, + relationshipAssessed: relationship.relationshipAssessed, + contradictionReasoningAllowed: relationship.contradictionReasoningAllowed, + reasoningStages: relationship.reasoningStages, + }; +} + function buildComparabilityQuestion(graph, assessment) { const centralText = normaliseText(graph?.centralStatement || ""); const mentionsPeriod = @@ -430,6 +491,13 @@ function detectContradictionContext(graph) { }; } +function buildBroadInvestigationQuestion(graph) { + const central = sanitizeQuestionText( + stripTrailingPunctuation(graph?.centralStatement || "these observations"), + ); + return `What changed during that period that could help explain why ${central}?`; +} + export function formulateTieResolutionQuestion({ graph }) { const comparability = assessComparability(graph); if (comparability.comparabilityStatus === "uncertain") { @@ -475,8 +543,7 @@ export function formulateTieResolutionQuestion({ graph }) { if (relationship.relationshipStatus === "potentially_related") { return { - question: - "What connection, if any, should we check between these observations?", + question: buildBroadInvestigationQuestion(graph), reason: "Formulated as a neutral relationship question because the observations may be related without being contradictory.", strategy: null, diff --git a/lib/graph/schema.js b/lib/graph/schema.js index eff3ef6..680a5bc 100644 --- a/lib/graph/schema.js +++ b/lib/graph/schema.js @@ -84,6 +84,25 @@ export const situationEdgeSchema = z.object({ // ── SituationGraph ─────────────────────────────────── +const reasoningStageSchema = z.object({ + stage: z.string().min(1), + status: z.string().min(1), + outcome: z.string().min(1), +}); + +export const reasoningStateSchema = z + .object({ + comparabilityStatus: z.string().min(1).nullable().optional(), + comparabilityReason: z.string().min(1).nullable().optional(), + comparabilityEvidence: z.array(z.string()).default([]), + relationshipStatus: z.string().min(1).nullable().optional(), + relationshipReason: z.string().min(1).nullable().optional(), + relationshipAssessed: z.boolean().optional(), + contradictionReasoningAllowed: z.boolean().optional(), + reasoningStages: z.array(reasoningStageSchema).default([]), + }) + .strict(); + export const situationGraphSchema = z.object({ centralStatement: z.string().min(1), nodes: z.array(situationNodeSchema).min(1), @@ -91,6 +110,7 @@ export const situationGraphSchema = z.object({ activeUnknownNodeId: z.string().nullable(), resolvedNodeIds: z.array(z.string()).default([]), currentSummary: z.string().min(1), + reasoningState: reasoningStateSchema.optional(), }); /** @typedef {z.infer} SituationGraph */ @@ -201,5 +221,6 @@ export function makeGraph(opts) { activeUnknownNodeId: opts.activeUnknownNodeId ?? null, resolvedNodeIds: opts.resolvedNodeIds ?? [], currentSummary: opts.currentSummary || "", + reasoningState: opts.reasoningState, }); } diff --git a/tests/graph/apply-proposal.test.js b/tests/graph/apply-proposal.test.js index 12bd5bc..c97af00 100644 --- a/tests/graph/apply-proposal.test.js +++ b/tests/graph/apply-proposal.test.js @@ -3,6 +3,120 @@ import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js"; import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js"; import { validateGraphReferences } from "@/lib/graph/utils.js"; +function makeComparabilityUpdateFixture() { + const comparabilityUnknown = makeNode({ + id: "n-comparability-unknown", + label: "Whether the figures are comparable", + description: + "Need to know whether the figures use the same period, basis, and scale before comparing them.", + kind: "unknown", + status: "unknown", + confidence: "high", + }); + const revenueObservation = makeNode({ + id: "n-revenue-observation", + label: "Revenue increased by 18%.", + description: "Revenue increased by 18%.", + kind: "observation", + status: "supported", + confidence: "high", + }); + const cashObservation = makeNode({ + id: "n-cash-observation", + label: "Cash in the bank decreased over the same period.", + description: "Cash in the bank decreased over the same period.", + kind: "observation", + status: "supported", + confidence: "high", + }); + const unrelatedNode = makeNode({ + id: "n-unrelated", + label: "Board update", + description: "A separate unchanged note.", + kind: "state", + status: "known", + confidence: "low", + }); + + const graph = makeGraph({ + centralStatement: + "Revenue increased by 18%, but cash in the bank fell over the same period.", + nodes: [ + comparabilityUnknown, + revenueObservation, + cashObservation, + unrelatedNode, + ], + edges: [ + makeEdge({ + id: "e-revenue-comparability", + fromNodeId: revenueObservation.id, + toNodeId: comparabilityUnknown.id, + relationship: "supports", + confidence: "medium", + description: "Revenue observation requires comparability confirmation.", + }), + makeEdge({ + id: "e-cash-comparability", + fromNodeId: cashObservation.id, + toNodeId: comparabilityUnknown.id, + relationship: "supports", + confidence: "medium", + description: "Cash observation requires comparability confirmation.", + }), + ], + activeUnknownNodeId: comparabilityUnknown.id, + resolvedNodeIds: [], + currentSummary: "Initial comparability fixture", + reasoningState: { + comparabilityStatus: "uncertain", + comparabilityReason: + "Comparability between the observations is not yet established across period, scale, or measurement basis.", + comparabilityEvidence: [], + relationshipStatus: "insufficient_information", + relationshipReason: + "Relationship classification is deferred until comparability is established.", + relationshipAssessed: false, + contradictionReasoningAllowed: false, + reasoningStages: [ + { + stage: "comparability", + status: "uncertain", + outcome: + "Comparability between the observations is not yet established across period, scale, or measurement basis.", + }, + { + stage: "relationship", + status: "insufficient_information", + outcome: "not assessed until comparability is established", + }, + ], + }, + }); + + const proposal = { + addedNodes: [], + updatedNodes: [ + { + nodeId: comparabilityUnknown.id, + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: + "Both figures cover the same accounting period and are taken from the same management accounts.", + reason: "The answer confirms the figures are comparable.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [comparabilityUnknown.id], + affectedNodeIds: [], + selectedQuestion: null, + }; + + return { graph, proposal, comparabilityUnknownId: comparabilityUnknown.id }; +} + function makeApplicationFixture() { const complaintRateUnknown = makeNode({ id: "n-complaint-rate-unknown", @@ -997,4 +1111,64 @@ describe("applyValidatedProposal", () => { "price", ); }); + + it("resolves the existing comparability unknown and advances reasoning after the answer", () => { + const { graph, proposal, comparabilityUnknownId } = + makeComparabilityUpdateFixture(); + const originalUnrelatedNode = JSON.stringify( + graph.nodes.find((node) => node.id === "n-unrelated"), + ); + + const result = applyValidatedProposal({ + situationGraph: graph, + proposal, + previousQuestion: + "Were these figures measured on the same basis and at the same scale?", + answer: + "Yes. Both figures cover the same accounting period and are taken from the same management accounts.", + }); + + expect(result.success).toBe(true); + expect(result.resolvedUnknownNodeIds).toContain(comparabilityUnknownId); + expect(result.resolvedReasoningNodeIds).toEqual([ + "reasoning:comparability", + ]); + expect(result.previousReasoningState.comparabilityStatus).toBe("uncertain"); + expect(result.reasoningState).toMatchObject({ + comparabilityStatus: "confirmed", + relationshipStatus: "potentially_related", + relationshipAssessed: true, + }); + expect(result.reasoningState.comparabilityEvidence).toEqual([ + comparabilityUnknownId, + ]); + expect(result.selectedQuestion?.question).toMatch( + /^What changed during that period that could help explain why /, + ); + expect(result.selectedQuestion?.question).not.toContain("same basis"); + expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch( + /dso|debtor days|receivables turnover|working capital|receivables/, + ); + expect(result.reasoningState.reasoningStages).toEqual([ + { + stage: "comparability", + status: "confirmed", + outcome: + "Comparability was confirmed by the user answer covering the same period and source basis.", + }, + { + stage: "relationship", + status: "potentially_related", + outcome: + "The observations concern connected business signals but do not establish a direct contradiction or cause.", + }, + ]); + expect( + JSON.stringify( + result.updatedSituationGraph.nodes.find( + (node) => node.id === "n-unrelated", + ), + ), + ).toBe(originalUnrelatedNode); + }); }); diff --git a/tests/graph/orchestrator.test.js b/tests/graph/orchestrator.test.js index ffffd04..9e6d161 100644 --- a/tests/graph/orchestrator.test.js +++ b/tests/graph/orchestrator.test.js @@ -118,6 +118,90 @@ function makeProposal(overrides = {}) { }; } +function makeComparabilityScenarioGraph() { + return makeGraph({ + centralStatement: + "Revenue increased by 18%, but cash in the bank fell over the same period.", + nodes: [ + makeNode({ + id: "n-comparability-unknown", + label: "Whether the figures are comparable", + description: + "Need to know whether the figures use the same period, basis, and scale before comparing them.", + kind: "unknown", + status: "unknown", + confidence: "high", + }), + makeNode({ + id: "n-revenue-observation", + label: "Revenue increased by 18%.", + description: "Revenue increased by 18%.", + kind: "observation", + status: "supported", + confidence: "high", + }), + makeNode({ + id: "n-cash-observation", + label: "Cash in the bank decreased over the same period.", + description: "Cash in the bank decreased over the same period.", + kind: "observation", + status: "supported", + confidence: "high", + }), + ], + edges: [], + activeUnknownNodeId: "n-comparability-unknown", + resolvedNodeIds: [], + currentSummary: "Comparability scenario", + reasoningState: { + comparabilityStatus: "uncertain", + comparabilityReason: + "Comparability between the observations is not yet established across period, scale, or measurement basis.", + comparabilityEvidence: [], + relationshipStatus: "insufficient_information", + relationshipReason: + "Relationship classification is deferred until comparability is established.", + relationshipAssessed: false, + contradictionReasoningAllowed: false, + reasoningStages: [ + { + stage: "comparability", + status: "uncertain", + outcome: + "Comparability between the observations is not yet established across period, scale, or measurement basis.", + }, + { + stage: "relationship", + status: "insufficient_information", + outcome: "not assessed until comparability is established", + }, + ], + }, + }); +} + +function makeComparabilityProposal() { + return { + addedNodes: [], + updatedNodes: [ + { + nodeId: "n-comparability-unknown", + previousStatus: "unknown", + newStatus: "resolved", + previousValue: null, + newValue: + "Both figures cover the same accounting period and are taken from the same management accounts.", + reason: "The answer confirms comparability.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: ["n-comparability-unknown"], + affectedNodeIds: [], + selectedQuestion: null, + }; +} + describe("lib/graph/orchestrator startCase", () => { beforeEach(() => { vi.resetModules(); @@ -930,6 +1014,74 @@ describe("lib/graph/orchestrator startCase", () => { }); }); + it("advances reasoning after comparability is resolved by the update answer", async () => { + const { updateCase } = await import("@/lib/graph/orchestrator.js"); + const provider = { + generateReconstruction: vi + .fn() + .mockResolvedValue(makeComparabilityProposal()), + }; + + const result = await updateCase( + { + situationGraph: makeComparabilityScenarioGraph(), + previousQuestion: + "Were these figures measured on the same basis and at the same scale?", + answer: + "Yes. Both figures cover the same accounting period and are taken from the same management accounts.", + promptVersion: "v0.4", + }, + { + provider, + config: MOCK_CONFIG, + applyProposal: true, + }, + ); + + expect(result.success).toBe(true); + expect(result.resolvedUnknownNodeIds).toEqual(["n-comparability-unknown"]); + expect(result.diagnostics).toMatchObject({ + previousComparabilityStatus: "uncertain", + comparabilityStatus: "confirmed", + relationshipStatus: "potentially_related", + relationshipAssessed: true, + resolvedReasoningNodeIds: ["reasoning:comparability"], + }); + expect(result.diagnostics.reasoningStagesBefore).toEqual([ + { + stage: "comparability", + status: "uncertain", + outcome: + "Comparability between the observations is not yet established across period, scale, or measurement basis.", + }, + { + stage: "relationship", + status: "insufficient_information", + outcome: "not assessed until comparability is established", + }, + ]); + expect(result.diagnostics.reasoningStagesAfter).toEqual([ + { + stage: "comparability", + status: "confirmed", + outcome: + "Comparability was confirmed by the user answer covering the same period and source basis.", + }, + { + stage: "relationship", + status: "potentially_related", + outcome: + "The observations concern connected business signals but do not establish a direct contradiction or cause.", + }, + ]); + expect(result.selectedQuestion?.question).toMatch( + /^What changed during that period that could help explain why /, + ); + expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch( + /same basis|dso|receivables|debtor days|working capital/, + ); + }); + it("startCase behaviour remains unchanged", async () => { mockAnalyseScenario.mockResolvedValue(makeAnalysisResult()); const { startCase } = await import("@/lib/graph/orchestrator.js"); diff --git a/tests/ui/scenario-form.test.jsx b/tests/ui/scenario-form.test.jsx index 0a29573..04e0c05 100644 --- a/tests/ui/scenario-form.test.jsx +++ b/tests/ui/scenario-form.test.jsx @@ -166,6 +166,40 @@ function makeUpdateSuccess(overrides = {}) { resolvedUnknownNodeIds: ["n-unknown"], previousActiveUnknownNodeId: "n-unknown", newActiveUnknownNodeId: "n-next-unknown", + previousReasoningState: { + comparabilityStatus: "uncertain", + reasoningStages: [ + { + stage: "comparability", + status: "uncertain", + outcome: + "Comparability between the observations is not yet established across period, scale, or measurement basis.", + }, + { + stage: "relationship", + status: "insufficient_information", + outcome: "not assessed until comparability is established", + }, + ], + }, + reasoningState: { + comparabilityStatus: "confirmed", + relationshipStatus: "insufficient_information", + reasoningStages: [ + { + stage: "comparability", + status: "confirmed", + outcome: + "Comparability was confirmed by the user answer covering the same period and source basis.", + }, + { + stage: "relationship", + status: "insufficient_information", + outcome: + "There is not enough structure to classify the relationship safely.", + }, + ], + }, changesApplied: { updatedNodeCount: 2, resolvedUnknownCount: 1, @@ -458,6 +492,36 @@ describe("graph-backed UI rendering", () => { ); }); + it("update view shows comparability progression without raw ids in the normal view", () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain("Comparability:"); + expect(html).toContain("uncertain → confirmed"); + expect(html).toContain("Relationship status:"); + expect(html).toContain("insufficient_information"); + expect(html).toContain("Reasoning stages:"); + expect(html).toContain("comparability: confirmed"); + expect(html).toContain("relationship: insufficient_information"); + expect(html).toContain( + "What changed during that period that could help explain why revenue increased by 18%, but cash in the bank fell over the same period?", + ); + expect(html).not.toContain("reasoning:comparability"); + }); + it("situation graph marks newly surfaced and active unknowns", () => { const html = renderToStaticMarkup(