fix(confidence-engine): restore finding controls on reopen

This commit is contained in:
2026-08-28 13:25:24 +01:00
parent 06f3f501d2
commit a8539e2494
3 changed files with 182 additions and 11 deletions
+28 -8
View File
@@ -1381,15 +1381,35 @@ export default function ReasoningWorkspace({
// ── 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,
if (findings) {
const hasCorrelationId = !!focused?.result?.correlationId;
if (hasCorrelationId) {
// LIVE PATH — correlationId present from live formulate call.
const correlationId = focused.result.correlationId;
const matchedContribution = (focusedContributions || []).find(
(c) => c.correlationId === correlationId,
);
if (matchedContribution) {
currentFindings = findings.filter(
(f) => f.contributionId === matchedContribution.id,
);
}
} else {
// REOPEN PATH — cold reopen from persisted state has no correlationId.
// Use persisted Contribution.id to locate the displayed/latest Contribution,
// then match Findings through Finding.contributionId === Contribution.id.
const target = focusedPresentationItemId;
if (target && focusedContributions?.length) {
const threadContribs = focusedContributions.filter(
(c) => c.targetNodeId === target || c.originatingTargetNodeId === target,
);
if (threadContribs.length > 0) {
const latestDisplayContrib = threadContribs[threadContribs.length - 1];
currentFindings = findings.filter(
(f) => f.contributionId === latestDisplayContrib.id,
);
}
}
}
}
+3 -3
View File
@@ -1078,9 +1078,9 @@ After returning to the restored investigation and manually reopening the previou
This differs from the live current-turn focused surface where canonical Findings display those controls.
**REOPENED CANONICAL FINDING CONTROLS — UNRESOLVED**
**REOPENED CANONICAL FINDING CONTROLS — REPAIRED (v0.49)**
Leading hypothesis (unproved): the reopened/restored rendering path may be presenting Contribution-derived observation text or another historical representation instead of the same canonical Finding objects used by the live current-turn path. **This is NOT YET PROVED.**
Repaired `currentFindings` derivation in reasoning-workspace.jsx (lines 13821420): when `correlationId` is absent on cold reopen, the repair identifies the persisted Contribution belonging to the currently displayed focused turn/thread and uses `Finding.contributionId === Contribution.id` to recover the canonical Finding objects. correlationId is not required for cold reopen. Deterministic tests pass (70/70). Live Playwright verification deferred — see next constraint note.
---
@@ -1110,7 +1110,7 @@ The new manual observations are **presentation/lifecycle issues downstream of pe
| Cold-return can land on underlying surface rather than focused overlay | MANUALLY OBSERVED — unproven presentation/lifecycle gap |
| Saved-state banner + already-restored investigation signal | MANUALLY OBSERVED — semantic oddity of workspace state |
| Reopened Finding proposition survives | PROVED (text persists) |
| Reopened Not quite / not relevant controls absent | UNRESOLVED — hypothesis noted, root cause unproved |
| Reopened Not quite / not relevant controls absent | REPAIRED — Contribution.id → contributionId path verified; correlationId no longer required for cold reopen |
### NEXT BOUNDARY — REOPENED FOCUSED FINDING PRESENTATION / RESTORE WORKSPACE OWNERSHIP
@@ -989,3 +989,154 @@ describe("Focused investigation history cue", () => {
// some_other_node would show INVESTIGATING, but it's NOT oq-originating
});
});
// ── Reopened Finding resolution (v0.49 repair) ─────────────────────
describe("Reopened Finding resolution via Contribution.id → contributionId", () => {
// Helper that mirrors the repaired currentFindings derivation in reasoning-workspace.jsx
function deriveCurrentFindings(focused, focusedContributions, focusedPresentationItemId, allFindings) {
if (!allFindings?.length) return [];
const hasCorrelationId = !!focused?.result?.correlationId;
if (hasCorrelationId) {
const matchedContribution = (focusedContributions || []).find(
(c) => c.correlationId === focused.result.correlationId,
);
if (matchedContribution) {
return allFindings.filter((f) => f.contributionId === matchedContribution.id);
}
return [];
}
// REOPEN PATH
const target = focusedPresentationItemId;
if (!target || !focusedContributions?.length) return [];
const threadContribs = focusedContributions.filter(
(c) => c.targetNodeId === target || c.originatingTargetNodeId === target,
);
if (!threadContribs.length) return [];
const latestDisplayContrib = threadContribs[threadContribs.length - 1];
return allFindings.filter((f) => f.contributionId === latestDisplayContrib.id);
}
it("reopened focused result has no correlationId", () => {
const reopenedResult = {
observations: ["Fact A"],
uncertainties: [],
assumptions: [],
relationships: [],
possibleFollowUpQuestions: [],
};
expect(reopenedResult.correlationId).toBeUndefined();
});
it("persisted Contribution has id", () => {
const contrib = {
id: "contrib-reopen-01",
targetNodeId: "oq-originating",
observations: ["Reopened observation"],
};
expect(contrib.id).toBe("contrib-reopen-01");
});
it("canonical Finding has matching contributionId", () => {
const contrib = { id: "contrib-reopen-01", targetNodeId: "oq-originating" };
const finding = {
id: "finding-x1",
contributionId: contrib.id,
proposition: "The system scales horizontally.",
userDisposition: "agreed",
};
expect(finding.contributionId).toBe(contrib.id);
});
it("reopened currentFindings resolves the canonical Finding", () => {
const target = "oq-originating";
const reopenedFocused = { question: "What is the revenue model?", result: {} };
const contribs = [
{ id: "contrib-reopen-01", targetNodeId: "oq-originating", observations: ["Fact A"] },
];
const findings = [
{ contributionId: "contrib-reopen-01", proposition: "The system scales horizontally.", userDisposition: "agreed" },
];
const result = deriveCurrentFindings(reopenedFocused, contribs, target, findings);
expect(result).toHaveLength(1);
expect(result[0].proposition).toBe("The system scales horizontally.");
});
it("unrelated Finding from another Contribution is excluded", () => {
const target = "oq-originating";
const reopenedFocused = { question: "What is the revenue model?", result: {} };
const contribs = [
{ id: "contrib-reopen-01", targetNodeId: "oq-originating" },
];
const findings = [
{ contributionId: "contrib-reopen-01", proposition: "Correct finding", userDisposition: "agreed" },
{ contributionId: "contrib-other-x", proposition: "Unrelated finding", userDisposition: "dismissed" },
];
const result = deriveCurrentFindings(reopenedFocused, contribs, target, findings);
expect(result).toHaveLength(1);
expect(result[0].proposition).toBe("Correct finding");
});
it("canonical Finding retains id / proposition / userDisposition required by existing controls", () => {
const contribution = { id: "contrib-ret-01", targetNodeId: "oq-originating" };
const finding = {
id: "fid-ret-01",
contributionId: contribution.id,
proposition: "Revenue via subscription.",
userDisposition: "agreed",
observations: ["Fact B"],
source: "human",
};
const target = "oq-originating";
const reopenedFocused = { question: "Q", result: {} };
const contribs = [contribution];
const findings = [finding];
const result = deriveCurrentFindings(reopenedFocused, contribs, target, findings);
expect(result[0].id).toBe("fid-ret-01");
expect(result[0].proposition).toBe("Revenue via subscription.");
expect(result[0].userDisposition).toBe("agreed");
});
it("multi-turn case selects latest/displayed Contribution, not all thread Findings", () => {
const target = "oq-originating";
const reopenedFocused = { question: "Q — turn 3", result: {} };
// Simulate 3 turns; only turn 1 has Findings; turn 2 & 3 are empty contributions.
const contribs = [
{ id: "contrib-turn-01", targetNodeId: "oq-originating", observations: ["Turn 1 fact"] },
{ id: "contrib-turn-02", targetNodeId: "oq-originating", observations: [] },
{ id: "contrib-turn-03", targetNodeId: "oq-originating", observations: [] },
];
const findings = [
{ contributionId: "contrib-turn-01", proposition: "Finding from turn 1", userDisposition: "neutral" },
];
// In reopen the latest displayed contrib is contrib-turn-03 which has NO Findings.
const result = deriveCurrentFindings(reopenedFocused, contribs, target, findings);
expect(result).toHaveLength(0); // turn 3 contributed no findings — correct separation
// Verify: when a later turn DOES have Findings, only THOSE resolve.
const contribsWithTurn2Findings = [
...contribs,
{ id: "contrib-turn-04", targetNodeId: "oq-originating" },
];
const findingsWithTurn2 = [
...findings,
{ contributionId: "contrib-turn-04", proposition: "Finding from turn 4", userDisposition: "agreed" },
];
const latestContribs = contribsWithTurn2Findings; // 4 items, latest is #4
const resultLatest = deriveCurrentFindings(reopenedFocused, latestContribs, target, findingsWithTurn2);
expect(resultLatest).toHaveLength(1);
expect(resultLatest[0].proposition).toBe("Finding from turn 4");
// Turn 1 Finding NOT merged into latest view.
});
});