fix(confidence-engine): preserve investigation activity across turns

ThreadContributionsBadge, PriorContributionsSummary, and
SecondaryPreviousLearning all filtered contributions via
c.targetNodeId === nodeId. Multi-turn follow-up Contributions carry a
different immediate targetNodeId while the canonical origin remains on
Findings (originatingTargetNodeId).

Repaired: all contribution filters now match on EITHER
c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId.
handleDeconstructSubmit carries originatingTargetNodeId from
focusedPresentationItemId as provenance for cold-return recovery.
This commit is contained in:
2026-08-28 11:56:31 +01:00
parent b9a54589f0
commit 10cbcbdd05
3 changed files with 162 additions and 5 deletions
+133 -1
View File
@@ -749,7 +749,10 @@ describe("same-node focused result reopen", () => {
describe("Focused investigation history cue", () => {
function getThreadContribs(nodeId, contributions) {
return (contributions || []).filter((c) => c.targetNodeId === nodeId);
// v0.49 repaired: match on targetNodeId OR originatingTargetNodeId
return (contributions || []).filter(
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
);
}
function showsInvestigatingCue(threadContribCount) {
@@ -856,4 +859,133 @@ describe("Focused investigation history cue", () => {
expect(threadContribs).toHaveLength(0);
expect(showsInvestigatingCue(threadContribs.length)).toBe(false);
});
// ── v0.49 MULTI-TURN FOLLOW-UP REGRESSION ───────────────────
// Hypothesis: ThreadContributionsBadge depends only on the latest/direct
// Contribution targetNodeId, causing focused history to become invisible
// when follow-up turns retain a different immediate target identity.
it("multi-turn follow-up contribution with different targetNodeId causes OPEN QUESTION A to show NO INVESTIGATING (regression)", () => {
// Turn 1: direct contribution to Open Question A — this is the baseline that works
const contribTurn1 = {
id: "contrib-001",
targetNodeId: "oq-originating", // same as the Open Question node
question: "First focused question on A",
observations: ["Fact from turn 1"],
uncertainties: [],
assumptions: [],
relationships: [],
};
// Turn 2: follow-up contribution belongs to the SAME focused investigation
// but targets a different intermediate node (the follow-up itself)
const contribTurn2 = {
id: "contrib-002",
targetNodeId: "follow_up_intermediate", // DIFFERENT from oq-originating
question: "Follow-up on turn 1",
observations: ["Fact from turn 2"],
uncertainties: [],
assumptions: [],
relationships: [],
};
const contributions = [contribTurn1, contribTurn2];
// Both contributions exist — baseline check passes
expect(getThreadContribs("oq-originating", contributions).length).toBe(1);
expect(showsInvestigatingCue(getThreadContribs("oq-originating", contributions).length)).toBe(true);
});
it("multi-turn FOLLOW-UP ONLY: Open Question A shows INVESTIGATING when Turn 2 carries originatingTargetNodeId (repaired)", () => {
// After repair: follow-up contribution carries originatingTargetNodeId linking back to A.
const contribTurn2 = {
id: "contrib-002",
targetNodeId: "follow_up_intermediate",
originatingTargetNodeId: "oq-originating",
question: "Follow-up on turn 1",
observations: ["Fact from turn 2"],
uncertainties: [],
assumptions: [],
relationships: [],
};
const contributions = [contribTurn2];
// The repaired filter matches originatingTargetNodeId back to A
const threadContribs = getThreadContribs("oq-originating", contributions);
expect(threadContribs).toHaveLength(1);
expect(showsInvestigatingCue(threadContribs.length)).toBe(true);
});
it("multi-turn with contributing to A directly still shows INVESTIGATING (no regression for direct contributions)", () => {
const contribToOrigin = {
id: "contrib-005",
targetNodeId: "oq-originating",
observations: ["direct to origin"],
};
const threadContribs = getThreadContribs("oq-originating", [contribToOrigin]);
expect(threadContribs).toHaveLength(1);
expect(showsInvestigatingCue(threadContribs.length)).toBe(true);
});
it("multi-turn with originatingTargetNodeId recovers INVESTIGATING for A (repaired expectation)", () => {
// Simulated repaired state: Contribution carries originatingTargetNodeId
const contribTurn1 = {
id: "contrib-001",
targetNodeId: "oq-originating",
originatingTargetNodeId: "oq-originating",
observations: ["Fact from turn 1"],
};
const contribTurn2 = {
id: "contrib-002",
targetNodeId: "follow_up_intermediate",
originatingTargetNodeId: "oq-originating",
observations: ["Fact from turn 2"],
};
const contributions = [contribTurn1, contribTurn2];
// Repaired filter: matches direct OR originating target
const getThreadContribsRepaired = (nodeId, c) =>
(c || []).filter(
(item) => item.targetNodeId === nodeId || item.originatingTargetNodeId === nodeId,
);
const threadContribs = getThreadContribsRepaired("oq-originating", contributions);
expect(threadContribs).toHaveLength(2);
expect(showsInvestigatingCue(threadContribs.length)).toBe(true);
});
it("multi-turn: repaired filter recovers INVESTIGATING when ONLY follow-up exists (cold return repaired)", () => {
const contribTurn2 = {
id: "contrib-002",
targetNodeId: "follow_up_intermediate",
originatingTargetNodeId: "oq-originating",
observations: ["Fact from turn 2"],
};
const getThreadContribsRepaired = (nodeId, c) =>
(c || []).filter(
(item) => item.targetNodeId === nodeId || item.originatingTargetNodeId === nodeId,
);
const threadContribs = getThreadContribsRepaired("oq-originating", [contribTurn2]);
expect(threadContribs).toHaveLength(1);
expect(showsInvestigatingCue(threadContribs.length)).toBe(true);
});
it("unrelated follow-up does NOT falsely show INVESTIGATING on any Open Question", () => {
const unrelated = {
id: "contrib-unrelated",
targetNodeId: "some_other_node",
observations: [],
};
expect(getThreadContribs("oq-originating", [unrelated]).length).toBe(0);
expect(showsInvestigatingCue(getThreadContribs("oq-originating", [unrelated]).length)).toBe(false);
expect(getThreadContribs("some_other_node", [unrelated]).length).toBe(1);
// some_other_node would show INVESTIGATING, but it's NOT oq-originating
});
});