diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx index 0086a01..fe42985 100644 --- a/components/reasoning-workspace.jsx +++ b/components/reasoning-workspace.jsx @@ -145,6 +145,9 @@ function FocusedQuestionBody({ const hasContent = focused?.question?.trim() || formulationStep === "active" || processingStep === "active" || focused?.error; const hasResult = Boolean(focused?.result); const hasAnswer = Boolean(focused?.answer); + // A non-null result means we are still in a completed-context state even after the user selects a follow-up (which clears answer). + // Without this guard, selecting a follow-up question would erase "Previously answered" + "Your response". + const hasCompletedContext = processingStep !== "active" && Boolean(focused?.result); // ── Local correction state (FQB-owned, not propagated upward) ─ const [editingFindingId, setEditingFindingId] = useState(null); @@ -175,7 +178,7 @@ function FocusedQuestionBody({ {isFocused && hasContent && (
- {hasAnswer && focused?.question?.trim() ? ( + {(hasAnswer || hasCompletedContext) && focused?.question?.trim() ? (
{/* Previously answered question */}
@@ -511,12 +514,16 @@ function PriorContributionsSummary({ nodeId, contributions, findings }) { const priorContribs = threadContribs.slice(0, -1); if (!priorContribs.length) return null; + // Presentation-reversal: render newest prior turn first so user sees what was learned most recently at the top. + // This is a presentation-only decision; chronological order in data is preserved elsewhere. + const reversedPrior = [...priorContribs].reverse(); + return (

Previous learning

- {priorContribs.map((c, idx) => ( + {reversedPrior.map((c, idx) => (
Turn {c.sequence || idx + 1} — contribution ({getHistoricalPropositions(c, findings).length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear) @@ -564,12 +571,16 @@ function SecondaryPreviousLearning({ nodeId, contributions, findings }) { const priorContribs = threadContribs.slice(0, -1); if (!priorContribs.length) return null; + // Presentation-reversal: render newest prior turn first so user sees what was learned most recently at the top. + // This is a presentation-only decision; chronological order in data is preserved elsewhere. + const reversedPrior = [...priorContribs].reverse(); + return (

Previous learning

- {priorContribs.map((c, idx) => ( + {reversedPrior.map((c, idx) => (
Turn {c.sequence || idx + 1} — contribution ({getHistoricalPropositions(c, findings).length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear) diff --git a/tests/open-questions-vs-assumptions.test.jsx b/tests/open-questions-vs-assumptions.test.jsx index c2aa09f..552e7b2 100644 --- a/tests/open-questions-vs-assumptions.test.jsx +++ b/tests/open-questions-vs-assumptions.test.jsx @@ -1604,9 +1604,173 @@ describe("Narrow textarea rendering condition — v0.49 completed-result repair" }); }); - // ── v0.49 — completed-result provenance narrative ────────────── + // ── v0.50: IN-PLACE FOLLOW-UP CONTINUATION (defect: follow-up selection clears hasAnswer, causing "Question" header to reappear) ─ - describe("v0.49 Case A — completed turn provenance narrative", () => { +// Simulated FQB condition for determining whether to render completed-narrative framing +function shouldRenderCompletedFraming(focused) { + const hasAnswer = Boolean(focused?.answer); + // OLD (buggy): only checks answer + return hasAnswer && Boolean(focused?.question?.trim()); +} + +// FIXED condition: also treats a non-null result as "completed context exists" +function shouldRenderCompletedFraming_FIXED(focused) { + const hasAnswer = Boolean(focused?.answer); + const hasCompletedContext = Boolean(focused?.result); + return (hasAnswer || hasCompletedContext) && Boolean(focused?.question?.trim()); +} + +// ── Simulated reverse rendering for Previous Learning chronology ──────── + +function renderPriorContribs(chronological) { + // Mirrors PriorContributionsSummary / SecondaryPreviousLearning: slice(0, -1) preserves order + const prior = chronological.slice(0, -1); + return prior.map((c, idx) => ({ + label: `Turn ${c.sequence || idx + 1}`, + sequence: c.sequence, + id: c.id, + })); +} + +function renderPriorContribs_REVERSED(chronological) { + // Reversed at presentation boundary only + const prior = chronological.slice(0, -1); + return [...prior].reverse().map((c, idx) => ({ + label: `Turn ${c.sequence || idx + 1}`, + sequence: c.sequence, + id: c.id, + })); +} + +describe("v0.50 Case A — selected follow-up continues in place with completed context", () => { + it("OLD: selecting follow-up (answer=null, result exists) does NOT render completed framing → proves defect", () => { + const beforeSelect = { + question: "Turn 2 question", + answer: "Turn 2 answer", // non-null = hasAnswer = true + status: "formulated", + result: { observations: ["Finding A"], uncertainties: [], possibleFollowUpQuestions: ["Should we explore X?"] }, + }; + expect(shouldRenderCompletedFraming(beforeSelect)).toBe(true); + + const afterSelect = { + ...beforeSelect, + question: "Should we explore X?", // follow-up question + answer: null, // setFollowUpQuestion clears answer + status: "formulated", + result: beforeSelect.result, // result stays (causal context) + }; + + // BUG: after selecting a follow-up, completed framing is lost + expect(shouldRenderCompletedFraming(afterSelect)).toBe(false); + }); + + it("FIXED: after follow-up selection with result, completed framing IS preserved → proves repair", () => { + const afterSelect = { + question: "Should we explore X?", + answer: null, + status: "formulated", + result: { observations: ["Finding A"], uncertainties: [], possibleFollowUpQuestions: ["Another option?"] }, + }; + + expect(shouldRenderCompletedFraming_FIXED(afterSelect)).toBe(true); + }); +}); + +// ── Simulated reverse rendering for Previous Learning ────────────── + +function renderPriorContribs(contributions) { + // PriorContributionsSummary and SecondaryPreviousLearning use: slice(0, -1) + const prior = contributions.slice(0, -1); + return prior.map((c, idx) => ({ + label: `Turn ${c.sequence || idx + 1}`, + sequence: c.sequence, + id: c.id, + })); +} + +function renderPriorContribs_REVERSED(contributions) { + const prior = contributions.slice(0, -1); + // Reverse at presentation boundary only + return [...prior].reverse().map((c, idx) => ({ + label: `Turn ${c.sequence || idx + 1}`, + sequence: c.sequence, + id: c.id, + })); +} + +describe("v0.50 Prior contribs display order — reversed at presentation boundary", () => { + const threeContributions = [ + { id: "contrib-turn-1", sequence: 1, targetNodeId: "node-A", observations: ["Turn 1 finding"] }, + { id: "contrib-turn-2", sequence: 2, targetNodeId: "node-A", observations: ["Turn 2 finding"] }, + { id: "contrib-turn-3", sequence: 3, targetNodeId: "node-A", observations: ["Turn 3 finding"] }, // latest = excluded as current result + ]; + + it("chronological order: slice(0,-1) preserves input sequence [1, 2]", () => { + const ordered = renderPriorContribs(threeContributions); + expect(ordered.map((c) => c.sequence)).toEqual([1, 2]); + }); + + it("REVERSED display order: newest prior turn first [2, 1]", () => { + const reversed = renderPriorContribs_REVERSED(threeContributions); + expect(reversed.map((c) => c.sequence)).toEqual([2, 1]); + }); + + it("reversal puts Turn 2 (newest prior) at top of the list for the user", () => { + const reversed = renderPriorContribs_REVERSED(threeContributions); + expect(reversed[0].label).toBe("Turn 2"); + expect(reversed[1].label).toBe("Turn 1"); + }); + + it("data order is unchanged — only the presentation layer reverses", () => { + const ordered = renderPriorContribs(threeContributions); + const reversed = renderPriorContribs_REVERSED(threeContributions); + // The data order (ordered) is unaffected by reversing + expect(ordered.map((c) => c.sequence)).toEqual([1, 2]); + // Reversal produces the opposite visual order for the user + expect(reversed.map((c) => c.sequence)).toEqual([2, 1]); + // Both refer to the same underlying contributions (same IDs) + const orderedIds = new Set(ordered.map((c) => c.id)); + const reversedIds = new Set(reversed.map((c) => c.id)); + expect(orderedIds).toEqual(reversedIds); + }); + + it("two-turn thread: reversal still works", () => { + const twoContributions = [ + { id: "contrib-turn-1", sequence: 1, targetNodeId: "node-B" }, + { id: "contrib-turn-2", sequence: 2, targetNodeId: "node-B" }, // excluded as current + ]; + + const reversed = renderPriorContribs_REVERSED(twoContributions); + expect(reversed).toHaveLength(1); + expect(reversed[0].sequence).toBe(1); + }); + + it("single prior turn: reversal is no-op (one element)", () => { + const single = [ + { id: "contrib-turn-1", sequence: 1, targetNodeId: "node-C" }, + { id: "contrib-turn-2", sequence: 2, targetNodeId: "node-C" }, // excluded as current + ]; + + const reversed = renderPriorContribs_REVERSED(single); + expect(reversed).toHaveLength(1); + expect(reversed[0].sequence).toBe(1); + }); + + it("reversal with non-sequential sequences preserves correct chronological reverse", () => { + const irregular = [ + { id: "contrib-A", sequence: 5, targetNodeId: "node-D" }, + { id: "contrib-B", sequence: 10, targetNodeId: "node-D" }, + { id: "contrib-C", sequence: 15, targetNodeId: "node-D" }, // excluded as current + ]; + + const reversed = renderPriorContribs_REVERSED(irregular); + expect(reversed.map((c) => c.sequence)).toEqual([10, 5]); + }); +}); + +// ── v0.50 INVESTIGATING cue on normal Open Questions buttons (actual render path) ─── + +describe("v0.49 Case A — completed turn provenance narrative", () => { it("completed turn renders distinct user response and derived findings sections", () => { const focused = { question: "Completed question",