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(