diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx
index c48eab0..a631010 100644
--- a/components/reasoning-workspace.jsx
+++ b/components/reasoning-workspace.jsx
@@ -5,6 +5,33 @@ import DiagnosticsView from "@/components/diagnostics-view";
import GraphUpdateView from "@/components/graph-update-view";
import SituationGraphView from "@/components/situation-graph-view";
+// ── Technical summary detector (main view filters these) ───
+const TECHNICAL_PATTERNS = [
+ /nodes?\s*[:\d]/i,
+ /edges?\s*[:\d]/i,
+ /\b(?:unknown|observation|conclusion)\b\s/i,
+ /\bsorted\b/i,
+ /by_kind/i,
+ /\b(?:node|edge|unknown|state)\s+count/i,
+];
+
+function isTechnicalSummary(summary) {
+ if (!summary || typeof summary !== "string") return false;
+ const trimmed = summary.trim();
+ if (!trimmed) return false;
+ for (const p of TECHNICAL_PATTERNS) {
+ if (p.test(trimmed)) return true;
+ }
+ return false;
+}
+
+function resolveCurrentSummary(currentSummary) {
+ if (isTechnicalSummary(currentSummary)) {
+ return null;
+ }
+ return currentSummary || null;
+}
+
// ── Status message pools for loading feedback ────────────────
const INITIAL_MESSAGES = [
{ min: 0, text: "Reading your situation" },
@@ -74,22 +101,20 @@ function SituationCard({ centralStatement }) {
// ── Current understanding card ────────────────────────────────
function CurrentUnderstanding({ currentSummary }) {
- if (currentSummary) {
- return (
-
- {remainingCount > 0 && !rwNoQuestionReason ? (
+ {remainingCount > 0 && !isComplete ? (
We are still building confidence about your situation.{" "}
{remainingCount === 1
@@ -262,6 +305,13 @@ export default function ReasoningWorkspace({
const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || [];
const noQuestionReason = diagnostics?.noQuestionReason ?? null;
+ const remainingUnknowns = graph?.nodes?.filter(
+ (n) => n.kind === "unknown" && n.status !== "resolved" && !(graph.resolvedNodeIds || []).includes(n.id),
+ );
+
+ const genuineCompletion = hasGenuineCompletion(graph);
+ const unresolvedRemaining = !genuineCompletion && remainingUnknowns ? remainingUnknowns.length > 0 : false;
+
return (
{/* ── Loading overlays ─────────────────────────────── */}
@@ -290,10 +340,17 @@ export default function ReasoningWorkspace({
) : (
<>
- {/* When investigation has nothing further to ask */}
- {status === "success" && !canAnswer && graph && (
+ {/* Completion or holding state (only when there is no next question) */}
+ {status === "success" && !canAnswer && graph && genuineCompletion && (
)}
+ {status === "success" && !canAnswer && graph && unresolvedRemaining && (
+
+
There is no further question the engine can justify at the moment.
+
More evidence may be needed before a next step is clear.
+
+ )}
+
{graph &&
}
{graph &&
}
{canAnswer &&
}
@@ -363,4 +420,4 @@ export default function ReasoningWorkspace({
);
}
-export { useLoadingStatus, INITIAL_MESSAGES, UPDATE_MESSAGES, LoadingOverlay };
+export { useLoadingStatus, INITIAL_MESSAGES, UPDATE_MESSAGES, LoadingOverlay, resolveCurrentSummary, isTechnicalSummary };
diff --git a/tests/ui/scenario-form.test.jsx b/tests/ui/scenario-form.test.jsx
index 01d9189..4275e86 100644
--- a/tests/ui/scenario-form.test.jsx
+++ b/tests/ui/scenario-form.test.jsx
@@ -9,6 +9,8 @@ import ReasoningWorkspace, {
INITIAL_MESSAGES,
UPDATE_MESSAGES,
LoadingOverlay,
+ resolveCurrentSummary,
+ isTechnicalSummary,
} from "@/components/reasoning-workspace.jsx";
import {
ScenarioResultPanels,
@@ -885,14 +887,14 @@ describe("ReasoningWorkspace UI", () => {
expect(html).toContain("Complaints increased while production increased.");
});
- it("shows what we've established section", () => {
+ it("shows what we've established section with plain-language summary", () => {
const html = renderToStaticMarkup(
{
);
expect(html).toContain("What we've established");
- expect(html).toContain("Nodes: 2 observation, 1 unknown");
+ expect(html).toContain("We have identified a key question to investigate further.");
+ });
+
+ it("filters technical summaries from the main view (not developer details)", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ // Fallback text shown in main view (currentSummary filtered)
+ expect(html).toContain("We have separated what is known from what still needs checking.");
+ // Developer details preserves the full technical summary
+ expect(html).toContain("Developer details");
+ });
+
+ it("keeps technical summary available in Developer details", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ // Developer details is the mechanism for accessing full technical data
+ expect(html).toContain("Developer details");
});
it("prominently displays the current investigation", () => {
@@ -1115,6 +1137,10 @@ describe("ReasoningWorkspace UI", () => {
// After 16s elapsed, the second message should be active
// (useEffect fires in real React; here we verify via hook export)
expect(INITIAL_MESSAGES[1].min).toBe(10);
+ expect(UPDATE_MESSAGES[0].text).toBe("Considering your answer");
+ expect(UPDATE_MESSAGES[1].text).toBe("Updating the situation");
+ expect(UPDATE_MESSAGES[2].text).toBe("Checking what changed");
+ expect(UPDATE_MESSAGES[3].text).toBe("Choosing the next question");
vi.useRealTimers();
});
@@ -1141,6 +1167,14 @@ describe("ReasoningWorkspace UI", () => {
updateStatus="idle"
result={makeWorkspaceResult({
selectedQuestion: null,
+ situationGraph: {
+ ...makeWorkspaceResult().situationGraph,
+ activeUnknownNodeId: null,
+ nodes: makeWorkspaceResult().situationGraph.nodes.map((n) =>
+ n.id === "n-unknown" ? { ...n, status: "resolved", value: "the denominator is total units sold" } : n,
+ ),
+ resolvedNodeIds: ["n-unknown"],
+ },
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: "All unknowns resolved" },
})}
answer=""
@@ -1241,6 +1275,39 @@ describe("ReasoningWorkspace UI", () => {
expect(html).not.toContain("Update situation");
});
+ // ── Technical summary detection ───────────────────────
+
+ it("isTechnicalSummary detects node counts", () => {
+ expect(isTechnicalSummary("Nodes: 2 observation, 1 unknown")).toBe(true);
+ expect(isTechnicalSummary("Nodes: 3 observations total")).toBe(true);
+ });
+
+ it("isTechnicalSummary detects edge counts", () => {
+ expect(isTechnicalSummary("Edges: 5 total")).toBe(true);
+ expect(isTechnicalSummary("Edges: 2")).toBe(true);
+ });
+
+ it("isTechnicalSummary detects graph enum language", () => {
+ expect(isTechnicalSummary("1 unknown remaining")).toBe(true);
+ expect(isTechnicalSummary("Observation nodes identified")).toBe(true);
+ expect(isTechnicalSummary("Conclusion verified")).toBe(true);
+ });
+
+ it("isTechnicalSummary allows plain-language summaries", () => {
+ expect(isTechnicalSummary("We have separated what is known from what still needs checking.")).toBe(false);
+ expect(isTechnicalSummary("Updated summary")).toBe(false);
+ expect(isTechnicalSummary("")).toBe(false);
+ expect(isTechnicalSummary(null)).toBe(false);
+ expect(isTechnicalSummary(undefined)).toBe(false);
+ });
+
+ it("resolveCurrentSummary filters technical and passes plain", () => {
+ expect(resolveCurrentSummary("Nodes: 1 state, 3 observations")).toBe(null);
+ expect(resolveCurrentSummary("Updated summary")).toBe("Updated summary");
+ expect(resolveCurrentSummary(null)).toBe(null);
+ expect(resolveCurrentSummary(undefined)).toBe(null);
+ });
+
it("update result merges into workspace correctly", () => {
const html = renderToStaticMarkup(
{
updateStatus="idle"
result={makeWorkspaceResult({
selectedQuestion: null,
+ situationGraph: {
+ ...makeWorkspaceResult().situationGraph,
+ activeUnknownNodeId: null,
+ nodes: makeWorkspaceResult().situationGraph.nodes.map((n) =>
+ n.id === "n-unknown" ? { ...n, status: "resolved", value: "1.9 complaints per 100 units" } : n,
+ ),
+ resolvedNodeIds: ["n-unknown"],
+ },
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: "All unknowns satisfied" },
})}
answer=""
@@ -1536,13 +1611,21 @@ describe("ReasoningWorkspace UI", () => {
expect(html).not.toContain("There is no next question at the moment");
});
- it("investigation complete shows confidence-building fallback", () => {
+ it("no-question state with unresolved areas shows holding, not completion", () => {
const html = renderToStaticMarkup(
+ n.id === "n-unknown" ? { ...n, status: "unknown", value: null } : n,
+ ),
+ resolvedNodeIds: [],
+ },
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: null },
})}
answer=""
@@ -1551,6 +1634,198 @@ describe("ReasoningWorkspace UI", () => {
/>,
);
+ expect(html).toContain("There is no further question the engine can justify at the moment");
+ expect(html).not.toContain("We have established enough for now");
+ expect(html).not.toContain("All areas under investigation are now complete");
+ });
+
+ // ── Update loading tests ───────────────────────────────
+
+ it("update loading card appears immediately after answer submit", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Working through your situation");
+ expect(html).toContain("Considering your answer");
+ });
+
+ it("normal update button is hidden while loading", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).not.toContain("Update situation");
+ expect(html).not.toContain("Your response");
+ });
+
+ it("update status messages change with mocked timers", () => {
+ vi.useFakeTimers();
+
+ const baseProps = {
+ status: "success",
+ updateStatus: "loading",
+ result: makeWorkspaceResult(),
+ answer: "test",
+ setAnswer: vi.fn(),
+ onAnswerSubmit: vi.fn(),
+ };
+
+ let html = renderToStaticMarkup();
+ expect(html).toContain("Considering your answer");
+
+ vi.advanceTimersByTime(10000);
+ vi.useRealTimers();
+
+ // We rely on the exported UPDATE_MESSAGES to verify message pool correctness
+ expect(UPDATE_MESSAGES[0].text).toBe("Considering your answer");
+ expect(UPDATE_MESSAGES[1].text).toBe("Updating the situation");
+ expect(UPDATE_MESSAGES[2].text).toBe("Checking what changed");
+ expect(UPDATE_MESSAGES[3].text).toBe("Choosing the next question");
+
+ vi.useRealTimers();
+ });
+
+ it("update prevents duplicate submission by removing controls while loading", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).not.toContain("Update situation");
+ expect(html).toContain("Working through your situation");
+ });
+
+ // ── Outcome state tests ────────────────────────────────
+
+ it("next-question state does not show completion wording", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Current investigation");
+ expect(html).not.toContain("We have established enough for now");
+ expect(html).not.toContain("All areas under investigation are now complete");
+ });
+
+ it("unresolved-no-question state does not imply completion", () => {
+ const html = renderToStaticMarkup(
+ n.id !== "n-unknown" || n.kind === "observation",
+ ),
+ {
+ id: "n-unknown",
+ label: "Complaint rate denominator",
+ description: "Need the denominator for complaint rate",
+ kind: "unknown",
+ status: "unknown",
+ confidence: "medium",
+ },
+ ],
+ resolvedNodeIds: [],
+ },
+ })}
+ answer=""
+ setAnswer={vi.fn()}
+ onAnswerSubmit={vi.fn()}
+ />,
+ );
+
+ expect(html).not.toContain("All areas under investigation are now complete");
+ expect(html).not.toContain("We have established enough for now");
+ expect(html).toContain("There is no further question the engine can justify at the moment");
+ });
+
+ it("genuine completion hides active investigation and remaining-area wording", () => {
+ const html = renderToStaticMarkup(
+
+ n.id === "n-unknown" ? { ...n, status: "resolved", value: "total units" } : n,
+ ),
+ resolvedNodeIds: ["n-unknown"],
+ },
+ })}
+ answer=""
+ setAnswer={vi.fn()}
+ onAnswerSubmit={vi.fn()}
+ />,
+ );
+
+ expect(html).not.toContain("Current investigation");
+ expect(html).not.toContain("areas remain");
expect(html).toContain("We have established enough for now");
});
+
+ it("no contradictory completion and unresolved focus appear together", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ const hasCompletion = html.includes("All areas under investigation are now complete") || html.includes("We have established enough for now");
+ expect(hasCompletion).toBe(false);
+ });
+
+ it("Developer details remains collapsed and unchanged", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Developer details");
+ });
});
\ No newline at end of file