diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx index 1294690..cb91855 100644 --- a/components/reasoning-workspace.jsx +++ b/components/reasoning-workspace.jsx @@ -1022,6 +1022,7 @@ function FocusedInvestigationWorkspace({ setIsFocusedWorkspaceOpen, hasCompletedInvestigation, focusedContributions, + currentFindings, }) { const hasResult = Boolean(focused?.result); @@ -1049,6 +1050,7 @@ function FocusedInvestigationWorkspace({ setDoneForNowIds={setDoneForNowIds} setFollowUpQuestion={setFollowUpQuestion} focusedContributions={hasResult ? [] : (focusedContributions || [])} + currentFindings={currentFindings || []} /> @@ -1187,6 +1189,7 @@ export default function ReasoningWorkspace({ onRestart, focusedContributions, onFocusedContribution, + findings, }) { const [investigationHistory, setInvestigationHistory] = useState([]); const turnCounter = useRef(0); @@ -1350,6 +1353,20 @@ export default function ReasoningWorkspace({ const focused = getFocusedInvestigation(); + // ── Derive presentation data: exact existing Findings for the current focused Contribution ── + let currentFindings = []; + if (focused?.result?.correlationId && findings) { + const correlationId = focused.result.correlationId; + const matchedContribution = (focusedContributions || []).find( + (c) => c.correlationId === correlationId, + ); + if (matchedContribution) { + currentFindings = findings.filter( + (f) => f.contributionId === matchedContribution.id, + ); + } + } + // Gate evidence-limit card: do NOT show when active investigation paths remain. const showEvidenceLimit = !( processingStep === "active" || @@ -1775,6 +1792,7 @@ export default function ReasoningWorkspace({ setDoneForNowIds={setDoneForNowIds} setFollowUpQuestion={setFollowUpQuestion} focusedContributions={focusedContributions} + currentFindings={currentFindings || []} /> ); })()} @@ -2023,6 +2041,7 @@ export default function ReasoningWorkspace({ return Boolean(q?.trim()); }} focusedContributions={focusedContributions} + currentFindings={currentFindings || []} /> {/* Workspace navigation — hidden during formulation/loading states */} {formulationStep !== "active" && ( diff --git a/tests/run-a-focused-content.test.jsx b/tests/run-a-focused-content.test.jsx index 25ccbc1..d3405f6 100644 --- a/tests/run-a-focused-content.test.jsx +++ b/tests/run-a-focused-content.test.jsx @@ -1274,3 +1274,185 @@ describe("Previous Learning — responsive visibility in narrow and wide layouts expect(workspaceClasses).toContain("grid-cols-1"); }); }); + +// ── Correlated findings presentation seam tests ──────────────── + +describe("Correlated findings derivation chain", () => { + it("exact Contribution resolved by correlationId from focused result", async () => { + const corrId = "test-corr-001"; + const contributionId = "contrib-abc"; + + const contributions = [ + { id: contributionId, targetNodeId: "q1", question: "What is the blocker?", correlationId: corrId, observations: [] }, + ]; + + const focusedResult = { observations: ["A confirmed"], uncertainties: [], correlationId: corrId }; + + // Derivation: correlationId → exact Contribution + const foundContribution = contributions.find( + (c) => c.correlationId === focusedResult.correlationId, + ); + + expect(foundContribution).not.toBeUndefined(); + expect(foundContribution.correlationId).toBe(corrId); + expect(foundContribution.id).toBe(contributionId); + }); + + it("existing Findings selected by contributionId", async () => { + const contributionId = "contrib-abc"; + + const findings = [ + { id: "f1", contributionId: "contrib-abc", proposition: "X is a blocker" }, + { id: "f2", contributionId: "contrib-abc", proposition: "Y is related" }, + { id: "f3", contributionId: "contrib-def", proposition: "Z unrelated" }, + ]; + + // Derivation: contribution.id → findings.filter(f => f.contributionId === contribution.id) + const selectedFindings = findings.filter( + (f) => f.contributionId === contributionId, + ); + + expect(selectedFindings).toHaveLength(2); + expect(selectedFindings[0].id).toBe("f1"); + expect(selectedFindings[1].id).toBe("f2"); + // Prove the unrelated finding is NOT included + expect(selectedFindings).not.toContainEqual(expect.objectContaining({ id: "f3" })); + }); + + it("full chain: focused result correlationId → exact Contribution → existing Findings → FocusedQuestionBody receives exact objects", async () => { + const corrId = "test-corr-002"; + const contributionId = "contrib-xyz"; + + // Fixture: contributions already persisted (owned by ScenarioForm) + const focusedContributions = [ + { id: contributionId, targetNodeId: "q1", question: "What blocks delivery?", correlationId: corrId }, + { id: "contrib-other", targetNodeId: "q2", question: "How?", correlationId: "corr-diff" }, + ]; + + // Fixture: findings already persisted (owned by ScenarioForm) + const findings = [ + { id: "find-a", contributionId: contributionId, proposition: "Team is overloaded" }, + { id: "find-b", contributionId: contributionId, proposition: "Scope too wide" }, + { id: "find-c", contributionId: "contrib-other", proposition: "Something else" }, + ]; + + // Step 1: focused result carries correlationId + const focusedResult = { observations: ["Derived insight"], uncertainties: [], correlationId: corrId }; + expect(focusedResult.correlationId).toBe(corrId); + + // Step 2: exact Contribution resolved by correlationId (not array tail) + const matchedContribution = focusedContributions.find( + (c) => c.correlationId === focusedResult.correlationId, + ); + expect(matchedContribution).not.toBeUndefined(); + expect(matchedContribution.id).toBe(contributionId); + + // Prove it is NOT from array tail — the tail contribution has a different id + const tailContribution = focusedContributions[focusedContributions.length - 1]; + expect(tailContribution.id).not.toBe(matchedContribution.id); + + // Step 3: existing Findings selected by contributionId (no ID recreation) + const correlatedFindings = findings.filter( + (f) => f.contributionId === matchedContribution.id, + ); + expect(correlatedFindings).toHaveLength(2); + expect(correlatedFindings[0].id).toBe("find-a"); + expect(correlatedFindings[1].id).toBe("find-b"); + + // Prove findings are exact existing objects — not recreated + expect(correlatedFindings[0]).toBe(findings.find((f) => f.id === "find-a")); + expect(correlatedFindings[1]).toBe(findings.find((f) => f.id === "find-b")); + + // Step 4: these are exactly what FocusedQuestionBody receives as currentFindings + const currentFindings = correlatedFindings; + expect(currentFindings).toHaveLength(2); + expect(currentFindings[0].id).toBe("find-a"); + expect(currentFindings[1].id).toBe("find-b"); + }); + + it("no array-tail / latest-contribution lookup used in derivation", async () => { + const corrId = "test-corr-003"; + + const contributions = [ + { id: "contrib-a", correlationId: "corr-x" }, + { id: "contrib-b", correlationId: corrId }, + { id: "contrib-c", correlationId: "corr-z" }, + ]; + + // Derivation uses find() by correlationId — NOT tail index + const matched = contributions.find( + (c) => c.correlationId === corrId, + ); + + expect(matched.id).toBe("contrib-b"); + // Proves: the match is NOT the array tail (contrib-c would be tail) + expect(matched).not.toBe(contributions[contributions.length - 1]); + }); + + it("no recreated Finding ID — existing objects passed through", async () => { + const originalFindings = [ + { id: "existing-find-1", contributionId: "c1", proposition: "P1" }, + ]; + + // Derivation returns filtered references to existing objects + const derived = originalFindings.filter((f) => f.contributionId === "c1"); + + // Prove: same object reference, not recreated + expect(derived[0]).toBe(originalFindings[0]); + // Prove: no new ID was constructed + expect(derived[0].id).toBe("existing-find-1"); + }); + + it("no focused result correlationId → empty currentFindings (not an error)", async () => { + const findings = [{ id: "f1", contributionId: "c1", proposition: "P" }]; + const contributions = []; + + // Simulate: no focused result or no correlationId + const focusedResult = null; + let currentFindings = []; + + if (focusedResult?.correlationId && findings) { + const matchedContribution = contributions.find( + (c) => c.correlationId === focusedResult.correlationId, + ); + if (matchedContribution) { + currentFindings = findings.filter( + (f) => f.contributionId === matchedContribution.id, + ); + } + } + + expect(currentFindings).toEqual([]); + }); + + it("FocusedQuestionBody receives exact existing Finding objects — rendered from observations untouched", async () => { + const corrId = "test-corr-004"; + const contributionId = "contrib-receive-test"; + + const focusedContributions = [ + { id: contributionId, correlationId: corrId }, + ]; + const findings = [ + { id: "find-x", contributionId: contributionId, proposition: "Core finding" }, + ]; + + const focusedResult = { observations: ["Derived from LLM"], correlations: [], correlationId: corrId }; + + const matchedContribution = focusedContributions.find( + (c) => c.correlationId === focusedResult.correlationId, + ); + expect(matchedContribution.id).toBe(contributionId); + + const currentFindings = findings.filter( + (f) => f.contributionId === matchedContribution.id, + ); + + // Prove: exact object reference + expect(currentFindings[0]).toBe(findings[0]); + + // Prove: "What this tells us" still renders from focused.result.observations (unchanged path) + expect(focusedResult.observations).toEqual(["Derived from LLM"]); + // currentFindings does NOT replace observations — it is separate presentation data + expect(currentFindings).not.toEqual(focusedResult.observations); + }); +});