feat(confidence-engine): expose correlated findings to focused presentation
This commit is contained in:
@@ -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" && (
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user