feat: advance reasoning after comparability is resolved

This commit is contained in:
2026-08-02 17:06:34 +01:00
parent 7d408701b5
commit 25a989450c
9 changed files with 649 additions and 5 deletions
+39
View File
@@ -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 (
<div className="space-y-4">
<section className="rounded-lg border border-blue-200 bg-blue-50 p-4">
@@ -134,12 +153,32 @@ export default function GraphUpdateView({ updateResult }) {
{selectedQuestion.question}
</div>
)}
{previousComparabilityStatus && newComparabilityStatus && (
<div>
<span className="font-medium">Comparability:</span>{" "}
{previousComparabilityStatus} {newComparabilityStatus}
</div>
)}
{relationshipStatus && (
<div>
<span className="font-medium">Relationship status:</span>{" "}
{relationshipStatus}
</div>
)}
{!selectedQuestion?.question && !newActiveUnknownNodeId && previousActiveUnknownNodeId && (
<div>
<span className="font-medium">Next question status:</span> No next question selected yet.
</div>
)}
</div>
{reasoningStagesAfter.length > 0 && (
<div className="mt-3 text-sm text-blue-950">
<span className="font-medium">Reasoning stages:</span>{" "}
{reasoningStagesAfter
.map((stage) => `${stage.stage}: ${stage.status}`)
.join(" → ")}
</div>
)}
</section>
<ListSection
+6
View File
@@ -36,3 +36,9 @@ After comparability assessment, observations now pass through a deterministic re
Yes, in minimal deterministic form.
The repeated pattern appeared in four scenarios, so a small pre-contradiction comparability assessment is justified.
## Two-step experiment result
A comparison question is useful only if its answer advances the reasoning stage rather than merely adding more text.
In the revenue-versus-cash scenario, the first question now confirms whether the figures are comparable, and the answer resolves that existing uncertainty instead of creating a parallel note. After that update, the engine progresses from comparability assessment to cautious relationship assessment and can select one broad non-expert follow-up question.
+92 -2
View File
@@ -1,5 +1,7 @@
import { describeGraph } from "./builder.js";
import {
buildReasoningState,
COMPARABILITY_REASONING_NODE_ID,
formulateQuestion,
formulateTieResolutionQuestion,
} from "./question-formulator.js";
@@ -426,7 +428,67 @@ function buildChangesApplied(proposal, affectedNodeIds) {
};
}
export function applyValidatedProposal({ situationGraph, proposal }) {
function isComparabilityQuestion(question) {
const text = String(question || "").toLowerCase();
return (
text.includes("same basis") ||
text.includes("same scale") ||
text.includes("same period")
);
}
function answerConfirmsComparability(answer) {
const text = String(answer || "").toLowerCase();
return (
/\byes\b/.test(text) &&
(text.includes("same accounting period") ||
text.includes("same management accounts") ||
text.includes("same basis") ||
text.includes("same scale") ||
text.includes("both figures cover the same"))
);
}
function deriveReasoningStateOverride({
graph,
previousQuestion,
answer,
resolvedUnknownNodeIds,
}) {
const previousReasoningState = buildReasoningState(graph);
const previousComparabilityStatus =
previousReasoningState.comparabilityStatus ?? null;
if (
previousComparabilityStatus === "uncertain" &&
isComparabilityQuestion(previousQuestion) &&
answerConfirmsComparability(answer)
) {
return {
reasoningStateOverride: {
comparabilityStatus: "confirmed",
comparabilityReason:
"Comparability was confirmed by the user answer covering the same period and source basis.",
comparabilityEvidence: resolvedUnknownNodeIds,
},
resolvedReasoningNodeIds: [COMPARABILITY_REASONING_NODE_ID],
previousReasoningState,
};
}
return {
reasoningStateOverride: {},
resolvedReasoningNodeIds: [],
previousReasoningState,
};
}
export function applyValidatedProposal({
situationGraph,
proposal,
previousQuestion = null,
answer = null,
}) {
const graphValidation = situationGraphSchema.safeParse(situationGraph);
const proposalValidation = graphUpdateSchema.safeParse(proposal);
@@ -574,6 +636,12 @@ export function applyValidatedProposal({ situationGraph, proposal }) {
const proposalSnapshot = cloneJsonSafe(validatedProposal);
const previousActiveUnknownNodeId = graphSnapshot.activeUnknownNodeId ?? null;
const affectedNodeIds = buildAffectedNodeIds(graphSnapshot, proposalSnapshot);
const reasoningResolution = deriveReasoningStateOverride({
graph: graphSnapshot,
previousQuestion,
answer,
resolvedUnknownNodeIds: validatedProposal.resolvedUnknownNodeIds,
});
const applied = applyGraphUpdate(graphSnapshot, proposalSnapshot);
if (!applied.success) {
@@ -590,6 +658,11 @@ export function applyValidatedProposal({ situationGraph, proposal }) {
edges: applied.edges,
resolvedNodeIds: applied.resolvedNodeIds,
};
const nextReasoningState = buildReasoningState(
updatedSituationGraph,
reasoningResolution.reasoningStateOverride,
);
updatedSituationGraph.reasoningState = nextReasoningState;
const activeUnknownWasResolved =
previousActiveUnknownNodeId != null &&
@@ -676,7 +749,21 @@ export function applyValidatedProposal({ situationGraph, proposal }) {
strategy: formulatedQuestion?.strategy,
investigationStrategy: formulatedQuestion?.investigationStrategy,
}
: null;
: (() => {
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,
};
}
+32 -1
View File
@@ -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 || [],
+69 -2
View File
@@ -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,
+21
View File
@@ -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<typeof situationGraphSchema>} SituationGraph */
@@ -201,5 +221,6 @@ export function makeGraph(opts) {
activeUnknownNodeId: opts.activeUnknownNodeId ?? null,
resolvedNodeIds: opts.resolvedNodeIds ?? [],
currentSummary: opts.currentSummary || "",
reasoningState: opts.reasoningState,
});
}
+174
View File
@@ -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);
});
});
+152
View File
@@ -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");
+64
View File
@@ -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(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess({
selectedQuestion: {
nodeId: "n-next-unknown",
question:
"What changed during that period that could help explain why revenue increased by 18%, but cash in the bank fell over the same period?",
reason: "A broad follow-up is now justified.",
},
}),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
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(
<SituationGraphView