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:
2026-09-02 12:46:50 +01:00
parent a061428711
commit 2b2096e41d
3 changed files with 181 additions and 5 deletions
@@ -2875,4 +2875,136 @@ describe("Zero Open Questions — end-of-investigation review invitation", () =>
expect(state.showInvitation).toBe(false);
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);
});
});