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:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user