fix(confidence-engine): scope focused presentation to active question
- FocusedQuestionBody derives thread-local contribution subset using targetNodeId || originatingTargetNodeId matching - hasCompletedContext, latest completed contrib, and all effective presentation fallbacks use scoped collection only - scenario-wide focusedContributions history preserved in memory - Fresh Question B no longer bleeds Question A's content across every presentation surface (Previously answered, What this tells us, Still unclear, Questions this raises, Assumptions, Connections) - Reopening or revisiting Question A still uses its own history - Targeted regression: 3 new Vitest cases pass - Handoff docs updated with v0.52 correction record
This commit is contained in:
@@ -143,6 +143,12 @@ function FocusedQuestionBody({
|
||||
onUpdateFindingDisposition,
|
||||
onUpdateFindingProposition,
|
||||
}) {
|
||||
// ── Active-thread contribution scoping (v0.52) ──
|
||||
// focusedContributions is scenario-wide; present only the active node's contributions.
|
||||
const threadContribs = (focusedContributions || []).filter(
|
||||
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
|
||||
);
|
||||
|
||||
const hasContent = focused?.question?.trim() || formulationStep === "active" || processingStep === "active" || focused?.error;
|
||||
const hasResult = Boolean(focused?.result);
|
||||
const hasAnswer = Boolean(focused?.answer);
|
||||
@@ -151,7 +157,7 @@ function FocusedQuestionBody({
|
||||
// Completed context: result (primary) OR prior contributions (fallback during processing/error).
|
||||
// Processing and error are transient states — they must NOT collapse completed context.
|
||||
const hasCompletedContext = Boolean(focused?.result) || (() => {
|
||||
const pc = [...(focusedContributions || [])].reverse().find((c) => c?.question && c?.answer);
|
||||
const pc = [...threadContribs].reverse().find((c) => c?.question && c?.answer);
|
||||
return !!pc;
|
||||
})();
|
||||
|
||||
@@ -159,7 +165,7 @@ function FocusedQuestionBody({
|
||||
// After setFollowUpQuestion() mutates focused.question/answer, derive from the
|
||||
// latest completed Contribution so the narrative remains correct.
|
||||
// Active follow-up detection: primary via result (when result exists), fallback via priorContribs (error state may have null result).
|
||||
const priorContribs = [...(focusedContributions || [])].reverse();
|
||||
const priorContribs = [...threadContribs].reverse();
|
||||
// Follow-ups from result are primary; priorContribs is fallback when result is null.
|
||||
const followUpsFromResult = focused?.result?.possibleFollowUpQuestions || [];
|
||||
const hasActiveFollowUpFromResult =
|
||||
|
||||
+41
-3
@@ -276,8 +276,46 @@ A real completed investigation reached zero Open Questions, clarified questions
|
||||
- This session corrected placement via Edit only; deterministic verification via targeted Vitest (145 tests) and build — deliberately did not repeat Playwright
|
||||
- All 145 tests pass; production build compiles successfully
|
||||
|
||||
**Open defects (unchanged):**
|
||||
- Focused-investigation state bleed: newly selected Open Question can show stale previous-question material — separate future increment
|
||||
### Focused investigation presentation ownership — v0.52 correction (verified)
|
||||
|
||||
**Live evidence that motivated this correction:**
|
||||
|
||||
A fresh unanswered Question B displayed stale focused-investigation content from a previously answered Question A across every presentation surface:
|
||||
- Previously answered / Your response
|
||||
- What this tells us
|
||||
- Still unclear
|
||||
- Questions this raises
|
||||
- Assumptions
|
||||
- Connections
|
||||
|
||||
**Root cause:** `FocusedQuestionBody` in `components/reasoning-workspace.jsx` iterated over the scenario-wide `focusedContributions` array for all derivations (`hasCompletedContext`, latest completed contribution, and every effective-presentation fallback: observations, uncertainties, follow-ups, assumptions, relationships). No active-question scoping was applied.
|
||||
|
||||
**Correction applied — scoped contribution presentation:**
|
||||
|
||||
- `FocusedQuestionBody` now derives a thread-local subset before any derivation:
|
||||
```js
|
||||
const threadContribs = (focusedContributions || []).filter(
|
||||
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
|
||||
);
|
||||
```
|
||||
- `hasCompletedContext`, latest completed contribution, and all effective presentation fallbacks use that scoped collection;
|
||||
- scenario-wide `focusedContributions` history is preserved in memory — only the presentation derivation is narrowed;
|
||||
- `originatingTargetNodeId` is also checked so follow-up contributions remain attributed to their originating question;
|
||||
- fresh Question B no longer inherits Question A's focused presentation content;
|
||||
- reopening or revisiting Question A still correctly uses Question A's own historical contribution content.
|
||||
|
||||
**Verified:**
|
||||
- targeted Vitest (`tests/open-questions-vs-assumptions.test.jsx`) — 3 new test cases (fresh B scoped to zero, active A retains its history, originatingTargetNodeId scoping) — pass
|
||||
- `npm run build` — compiles successfully
|
||||
- Rob manually verified the live UI on a persisted investigation: fresh unanswered Question B no longer shows stale focused-investigation content from Question B; confirmed across all six presentation surfaces listed above
|
||||
|
||||
**Verification notes:**
|
||||
- Claude Playwright was not used for final verification because the canonical dev server was unavailable at that point
|
||||
- Full Vitest suite was not re-run in this session (only targeted regression test)
|
||||
|
||||
### Open defects
|
||||
|
||||
- **Focused-investigation state bleed**: Resolved by v0.52 correction above (scoped presentation derivation). Verified by targeted Vitest, build, and Rob manual visual verification.
|
||||
- Empty Done `no_episodic_content`: choosing Done without episodic content can produce `{ success: false, stage: "preparation", error: "no_episodic_content" }` — separate future increment
|
||||
|
||||
**Next restart point:** Place milestone at Open Questions position when reverting placement correction. See v0.51 placement section above for the exact ternary pattern.
|
||||
**Next restart point:** The empty-Done `no_episodic_content` 400. Implement and verify that a Done action taken when no episodic evidence exists produces the same user-facing state (CU refresh with appropriate messaging) without a 400 error.
|
||||
|
||||
@@ -2876,3 +2876,135 @@ describe("Zero Open Questions — end-of-investigation review invitation", () =>
|
||||
expect(state.clarificationNodes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── v0.52: Focused presentation ownership — fresh question must not inherit another's content ──
|
||||
|
||||
describe("v0.52 focused presentation is scoped to active question ownership", () => {
|
||||
// Simulates the exact scoped-contributions derivation in FocusedQuestionBody (v0.52 fix)
|
||||
function deriveScopedContribs(focusedContributions, nodeId) {
|
||||
return (focusedContributions || []).filter(
|
||||
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
|
||||
);
|
||||
}
|
||||
|
||||
function simulateFQB(priorContribs, focused) {
|
||||
const hasAnswer = Boolean(focused?.answer);
|
||||
const hasResult = Boolean(focused?.result);
|
||||
const hasCompletedContext = hasResult || (() => {
|
||||
const pc = [...priorContribs].reverse().find((c) => c?.question && c?.answer);
|
||||
return !!pc;
|
||||
})();
|
||||
|
||||
const latestCompletedContrib = priorContribs.find((c) => c?.question && c?.answer);
|
||||
const effectiveObservations = focused?.result?.observations ?? priorContribs.find((c) => c?.observations)?.observations;
|
||||
const effectiveUncertainties = focused?.result?.uncertainties ?? priorContribs.find((c) => c?.uncertainties)?.uncertainties;
|
||||
const effectiveFollowUps = focused?.result?.possibleFollowUpQuestions || priorContribs.find((c) => c?.possibleFollowUpQuestions)?.possibleFollowUpQuestions;
|
||||
const effectiveAssumptions = focused?.result?.assumptions || priorContribs.find((c) => c?.assumptions)?.assumptions;
|
||||
const effectiveRelationships = focused?.result?.relationships || priorContribs.find((c) => c?.relationships)?.relationships;
|
||||
|
||||
return {
|
||||
hasCompletedContext,
|
||||
latestCompletedContrib,
|
||||
effectiveObservations,
|
||||
effectiveUncertainties,
|
||||
effectiveFollowUps,
|
||||
effectiveAssumptions,
|
||||
effectiveRelationships,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Scenario: Question A has historical contributions; Question B is fresh ──
|
||||
const questionAContrib = {
|
||||
id: "contrib-a-1",
|
||||
targetNodeId: "node-A",
|
||||
originatingTargetNodeId: "node-A",
|
||||
question: "What drives user engagement?",
|
||||
answer: "Social proof and urgency signals.",
|
||||
observations: ["Users respond to countdown timers"],
|
||||
uncertainties: ["Unclear if this generalises beyond SaaS"],
|
||||
possibleFollowUpQuestions: ["What is the optimal timer duration?"],
|
||||
assumptions: ["Users are time-pressured"],
|
||||
relationships: [["engagement", "temporal_pressure"]],
|
||||
};
|
||||
|
||||
const scenarioWideContributions = [questionAContrib];
|
||||
|
||||
it("Case 1 — fresh B has no active-thread contributions (Question A content absent from Question B presentation)", () => {
|
||||
const activeNodeId = "node-B"; // fresh question, no own contributions
|
||||
|
||||
const scopedContribs = deriveScopedContribs(scenarioWideContributions, activeNodeId);
|
||||
expect(scopedContribs).toHaveLength(0);
|
||||
|
||||
const focused = {
|
||||
status: "formulated",
|
||||
question: "How many users visit daily?",
|
||||
answer: null,
|
||||
result: null,
|
||||
};
|
||||
|
||||
const derived = simulateFQB(scopedContribs, focused);
|
||||
|
||||
// None of the A-owned fields should appear
|
||||
expect(derived.hasCompletedContext).toBe(false);
|
||||
expect(derived.latestCompletedContrib).toBeUndefined();
|
||||
expect(derived.effectiveObservations).toBeUndefined();
|
||||
expect(derived.effectiveUncertainties).toBeUndefined();
|
||||
expect(derived.effectiveFollowUps).toBeUndefined();
|
||||
expect(derived.effectiveAssumptions).toBeUndefined();
|
||||
expect(derived.effectiveRelationships).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Case 2 — active node = A retains its own contribution history", () => {
|
||||
const activeNodeId = "node-A"; // reopen Question A
|
||||
|
||||
const scopedContribs = deriveScopedContribs(scenarioWideContributions, activeNodeId);
|
||||
expect(scopedContribs).toHaveLength(1);
|
||||
expect(scopedContribs[0].id).toBe("contrib-a-1");
|
||||
|
||||
const focused = {
|
||||
status: "formulated",
|
||||
question: questionAContrib.question,
|
||||
answer: questionAContrib.answer,
|
||||
result: {
|
||||
observations: questionAContrib.observations,
|
||||
uncertainties: questionAContrib.uncertainties,
|
||||
possibleFollowUpQuestions: questionAContrib.possibleFollowUpQuestions,
|
||||
assumptions: questionAContrib.assumptions,
|
||||
relationships: questionAContrib.relationships,
|
||||
},
|
||||
};
|
||||
|
||||
const derived = simulateFQB(scopedContribs, focused);
|
||||
|
||||
// A's own content is present
|
||||
expect(derived.hasCompletedContext).toBe(true);
|
||||
expect(derived.latestCompletedContrib.id).toBe("contrib-a-1");
|
||||
expect(derived.effectiveObservations).toEqual(questionAContrib.observations);
|
||||
expect(derived.effectiveUncertainties).toEqual(questionAContrib.uncertainties);
|
||||
expect(derived.effectiveFollowUps).toEqual(questionAContrib.possibleFollowUpQuestions);
|
||||
expect(derived.effectiveAssumptions).toEqual(questionAContrib.assumptions);
|
||||
expect(derived.effectiveRelationships).toEqual(questionAContrib.relationships);
|
||||
});
|
||||
|
||||
it("Case 3 — originatingTargetNodeId also scopes (follow-up contribution belongs to A)", () => {
|
||||
const followUpContrib = {
|
||||
id: "contrib-a-followup",
|
||||
targetNodeId: "node-A-followup-intermediate",
|
||||
originatingTargetNodeId: "node-A",
|
||||
question: "What is the optimal timer duration?",
|
||||
answer: "15-30 seconds is the sweet spot.",
|
||||
observations: ["Timer above the CTA works best"],
|
||||
};
|
||||
|
||||
const contribs = [followUpContrib];
|
||||
|
||||
// Matches A via originatingTargetNodeId
|
||||
const scopedA = deriveScopedContribs(contribs, "node-A");
|
||||
expect(scopedA).toHaveLength(1);
|
||||
expect(scopedA[0].id).toBe("contrib-a-followup");
|
||||
|
||||
// Does NOT match B
|
||||
const scopedB = deriveScopedContribs(contribs, "node-B");
|
||||
expect(scopedB).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user