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:
@@ -471,7 +471,9 @@ function EvidenceLimitCard({ summary }) {
|
|||||||
// ── Prior contribution summary (embedded within FocusedQuestionBody) ───
|
// ── Prior contribution summary (embedded within FocusedQuestionBody) ───
|
||||||
|
|
||||||
function PriorContributionsSummary({ nodeId, contributions }) {
|
function PriorContributionsSummary({ nodeId, contributions }) {
|
||||||
const threadContribs = contributions.filter((c) => c.targetNodeId === nodeId);
|
const threadContribs = (contributions || []).filter(
|
||||||
|
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
|
||||||
|
);
|
||||||
if (!threadContribs.length) return null;
|
if (!threadContribs.length) return null;
|
||||||
|
|
||||||
// Exclude the most recent contribution — it is already shown as the current result above.
|
// Exclude the most recent contribution — it is already shown as the current result above.
|
||||||
@@ -519,7 +521,9 @@ function PriorContributionsSummary({ nodeId, contributions }) {
|
|||||||
// ── Standalone previous learning block (for two-column secondary placement) ───
|
// ── Standalone previous learning block (for two-column secondary placement) ───
|
||||||
|
|
||||||
function SecondaryPreviousLearning({ nodeId, contributions }) {
|
function SecondaryPreviousLearning({ nodeId, contributions }) {
|
||||||
const threadContribs = contributions.filter((c) => c.targetNodeId === nodeId);
|
const threadContribs = (contributions || []).filter(
|
||||||
|
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
|
||||||
|
);
|
||||||
if (!threadContribs.length) return null;
|
if (!threadContribs.length) return null;
|
||||||
|
|
||||||
// Exclude the most recent contribution — it is already shown as the current result above.
|
// Exclude the most recent contribution — it is already shown as the current result above.
|
||||||
@@ -567,7 +571,9 @@ function SecondaryPreviousLearning({ nodeId, contributions }) {
|
|||||||
// ── Thread contributions badge (standalone — used outside focused body) ───
|
// ── Thread contributions badge (standalone — used outside focused body) ───
|
||||||
|
|
||||||
function ThreadContributionsBadge({ nodeId, contributions }) {
|
function ThreadContributionsBadge({ nodeId, contributions }) {
|
||||||
const threadContribs = contributions.filter((c) => c.targetNodeId === nodeId);
|
const threadContribs = (contributions || []).filter(
|
||||||
|
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
|
||||||
|
);
|
||||||
if (!threadContribs.length) return null;
|
if (!threadContribs.length) return null;
|
||||||
|
|
||||||
// Show most recent contribution summary inline
|
// Show most recent contribution summary inline
|
||||||
@@ -1401,7 +1407,7 @@ export default function ReasoningWorkspace({
|
|||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
const priorContribs = (focusedContributions || []).filter(
|
const priorContribs = (focusedContributions || []).filter(
|
||||||
(c) => c.targetNodeId === target,
|
(c) => c.targetNodeId === target || c.originatingTargetNodeId === target,
|
||||||
);
|
);
|
||||||
|
|
||||||
setFocusedPresentationItemId(target);
|
setFocusedPresentationItemId(target);
|
||||||
@@ -1511,6 +1517,7 @@ export default function ReasoningWorkspace({
|
|||||||
// Persist contribution to case-level owner (ScenarioForm)
|
// Persist contribution to case-level owner (ScenarioForm)
|
||||||
onFocusedContribution?.({
|
onFocusedContribution?.({
|
||||||
targetNodeId,
|
targetNodeId,
|
||||||
|
originatingTargetNodeId: focusedPresentationItemId,
|
||||||
targetLabel: targetNode?.label || "",
|
targetLabel: targetNode?.label || "",
|
||||||
targetDescription: targetNode?.description || "",
|
targetDescription: targetNode?.description || "",
|
||||||
question: focused.question,
|
question: focused.question,
|
||||||
|
|||||||
@@ -1017,6 +1017,24 @@ The remaining boundaries are NOT persistence issues. They belong to the next fea
|
|||||||
- Finding merge/split ownership
|
- Finding merge/split ownership
|
||||||
- Final Finding → graph mapping mechanism
|
- Final Finding → graph mapping mechanism
|
||||||
|
|
||||||
|
### v0.49 — MULTI-TURN / COLD-RETURN ACTIVITY VISIBILITY REPAIR (2026-08-28)
|
||||||
|
|
||||||
|
**Closed:** Multi-turn/cold-return regression in Open Question activity visibility (INVESTIGATING cue).
|
||||||
|
|
||||||
|
**Root cause:** `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`). When only the follow-up turn survives cold return, the filter finds zero matches.
|
||||||
|
|
||||||
|
**Repaired identity rule:** All contribution filters now match on EITHER `c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId`. This captures both direct-target contributions and follow-up contributions whose origin anchors to a different Open Question.
|
||||||
|
|
||||||
|
**New persisted field on Contribution:** `originatingTargetNodeId` — set from `focusedPresentationItemId` at deconstruct submit time. It is NOT a new investigation flag; it is provenance that lets existing discovery surfaces recover multi-turn history.
|
||||||
|
|
||||||
|
**Files changed:**
|
||||||
|
- `components/reasoning-workspace.jsx` — patched 4 filter sites + `handleDeconstructSubmit` to carry `originatingTargetNodeId`
|
||||||
|
- `tests/open-questions-vs-assumptions.test.jsx` — added v0.49 multi-turn regression tests and repaired `getThreadContribs` helper
|
||||||
|
|
||||||
|
**No changes to:** Finding eligibility, Finding disposition, Current Understanding, Done-for-now promotion, SituationGraph, persistence provider, or graph reasoning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### BRANCH CLEAN STATE
|
### BRANCH CLEAN STATE
|
||||||
|
|
||||||
This branch has NO tracked working tree changes at HEAD b215846. Documentation closure recorded in this section only. No production or test modifications required.
|
This branch has NO tracked working tree changes at HEAD b215846. Documentation closure recorded in this section only. No production or test modifications required.
|
||||||
|
|||||||
@@ -749,7 +749,10 @@ describe("same-node focused result reopen", () => {
|
|||||||
|
|
||||||
describe("Focused investigation history cue", () => {
|
describe("Focused investigation history cue", () => {
|
||||||
function getThreadContribs(nodeId, contributions) {
|
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) {
|
function showsInvestigatingCue(threadContribCount) {
|
||||||
@@ -856,4 +859,133 @@ describe("Focused investigation history cue", () => {
|
|||||||
expect(threadContribs).toHaveLength(0);
|
expect(threadContribs).toHaveLength(0);
|
||||||
expect(showsInvestigatingCue(threadContribs.length)).toBe(false);
|
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
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user