Feature/product platform foundation v0.62 #1

Merged
robbond merged 683 commits from feature/product-platform-foundation-v0.62 into feature/emergent-unknowns-v0.5 2026-09-09 07:58:20 +01:00
2 changed files with 201 additions and 0 deletions
Showing only changes of commit 7858650334 - Show all commits
+19
View File
@@ -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 || []}
/>
</div>
@@ -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" && (
+182
View File
@@ -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);
});
});