feat(confidence-engine): expose correlated findings to focused presentation
This commit is contained in:
@@ -1022,6 +1022,7 @@ function FocusedInvestigationWorkspace({
|
|||||||
setIsFocusedWorkspaceOpen,
|
setIsFocusedWorkspaceOpen,
|
||||||
hasCompletedInvestigation,
|
hasCompletedInvestigation,
|
||||||
focusedContributions,
|
focusedContributions,
|
||||||
|
currentFindings,
|
||||||
}) {
|
}) {
|
||||||
const hasResult = Boolean(focused?.result);
|
const hasResult = Boolean(focused?.result);
|
||||||
|
|
||||||
@@ -1049,6 +1050,7 @@ function FocusedInvestigationWorkspace({
|
|||||||
setDoneForNowIds={setDoneForNowIds}
|
setDoneForNowIds={setDoneForNowIds}
|
||||||
setFollowUpQuestion={setFollowUpQuestion}
|
setFollowUpQuestion={setFollowUpQuestion}
|
||||||
focusedContributions={hasResult ? [] : (focusedContributions || [])}
|
focusedContributions={hasResult ? [] : (focusedContributions || [])}
|
||||||
|
currentFindings={currentFindings || []}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1187,6 +1189,7 @@ export default function ReasoningWorkspace({
|
|||||||
onRestart,
|
onRestart,
|
||||||
focusedContributions,
|
focusedContributions,
|
||||||
onFocusedContribution,
|
onFocusedContribution,
|
||||||
|
findings,
|
||||||
}) {
|
}) {
|
||||||
const [investigationHistory, setInvestigationHistory] = useState([]);
|
const [investigationHistory, setInvestigationHistory] = useState([]);
|
||||||
const turnCounter = useRef(0);
|
const turnCounter = useRef(0);
|
||||||
@@ -1350,6 +1353,20 @@ export default function ReasoningWorkspace({
|
|||||||
|
|
||||||
const focused = getFocusedInvestigation();
|
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.
|
// Gate evidence-limit card: do NOT show when active investigation paths remain.
|
||||||
const showEvidenceLimit = !(
|
const showEvidenceLimit = !(
|
||||||
processingStep === "active" ||
|
processingStep === "active" ||
|
||||||
@@ -1775,6 +1792,7 @@ export default function ReasoningWorkspace({
|
|||||||
setDoneForNowIds={setDoneForNowIds}
|
setDoneForNowIds={setDoneForNowIds}
|
||||||
setFollowUpQuestion={setFollowUpQuestion}
|
setFollowUpQuestion={setFollowUpQuestion}
|
||||||
focusedContributions={focusedContributions}
|
focusedContributions={focusedContributions}
|
||||||
|
currentFindings={currentFindings || []}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -2023,6 +2041,7 @@ export default function ReasoningWorkspace({
|
|||||||
return Boolean(q?.trim());
|
return Boolean(q?.trim());
|
||||||
}}
|
}}
|
||||||
focusedContributions={focusedContributions}
|
focusedContributions={focusedContributions}
|
||||||
|
currentFindings={currentFindings || []}
|
||||||
/>
|
/>
|
||||||
{/* Workspace navigation — hidden during formulation/loading states */}
|
{/* Workspace navigation — hidden during formulation/loading states */}
|
||||||
{formulationStep !== "active" && (
|
{formulationStep !== "active" && (
|
||||||
|
|||||||
@@ -1274,3 +1274,185 @@ describe("Previous Learning — responsive visibility in narrow and wide layouts
|
|||||||
expect(workspaceClasses).toContain("grid-cols-1");
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user