fix(confidence-engine): preserve completed-narrative when selecting follow-up + reverse prior-contribs display

Two presentation fixes (no reasoning-engine changes):

A. Follow-up answer continuity — selectFollowUpQuestion clears focused.answer,
   which previously caused the completed-narrative framing ('Previously answered'
   and 'Your response') to disappear mid-investigation. The guard now treats
   a non-null result as sufficient evidence of a completed-context state, so the
   user's verbatim answer and derived findings remain visible while a follow-up is
   being formulated.

B. Prior-contributions chronology — display order in 'Previous learning' panels
   has been reversed at the presentation boundary (newest → oldest). This means
   users see the most recently learned evidence first, without modifying data-order
   anywhere else. Applies to both PriorContributionsSummary and
   SecondaryPreviousLearning.
This commit is contained in:
2026-08-29 19:04:06 +01:00
parent 890a18c5a7
commit 17c6048047
2 changed files with 180 additions and 5 deletions
+166 -2
View File
@@ -1604,9 +1604,173 @@ describe("Narrow textarea rendering condition — v0.49 completed-result repair"
});
});
// ── v0.49 — completed-result provenance narrative ─────────────
// ── v0.50: IN-PLACE FOLLOW-UP CONTINUATION (defect: follow-up selection clears hasAnswer, causing "Question" header to reappear)
describe("v0.49 Case A — completed turn provenance narrative", () => {
// Simulated FQB condition for determining whether to render completed-narrative framing
function shouldRenderCompletedFraming(focused) {
const hasAnswer = Boolean(focused?.answer);
// OLD (buggy): only checks answer
return hasAnswer && Boolean(focused?.question?.trim());
}
// FIXED condition: also treats a non-null result as "completed context exists"
function shouldRenderCompletedFraming_FIXED(focused) {
const hasAnswer = Boolean(focused?.answer);
const hasCompletedContext = Boolean(focused?.result);
return (hasAnswer || hasCompletedContext) && Boolean(focused?.question?.trim());
}
// ── Simulated reverse rendering for Previous Learning chronology ────────
function renderPriorContribs(chronological) {
// Mirrors PriorContributionsSummary / SecondaryPreviousLearning: slice(0, -1) preserves order
const prior = chronological.slice(0, -1);
return prior.map((c, idx) => ({
label: `Turn ${c.sequence || idx + 1}`,
sequence: c.sequence,
id: c.id,
}));
}
function renderPriorContribs_REVERSED(chronological) {
// Reversed at presentation boundary only
const prior = chronological.slice(0, -1);
return [...prior].reverse().map((c, idx) => ({
label: `Turn ${c.sequence || idx + 1}`,
sequence: c.sequence,
id: c.id,
}));
}
describe("v0.50 Case A — selected follow-up continues in place with completed context", () => {
it("OLD: selecting follow-up (answer=null, result exists) does NOT render completed framing → proves defect", () => {
const beforeSelect = {
question: "Turn 2 question",
answer: "Turn 2 answer", // non-null = hasAnswer = true
status: "formulated",
result: { observations: ["Finding A"], uncertainties: [], possibleFollowUpQuestions: ["Should we explore X?"] },
};
expect(shouldRenderCompletedFraming(beforeSelect)).toBe(true);
const afterSelect = {
...beforeSelect,
question: "Should we explore X?", // follow-up question
answer: null, // setFollowUpQuestion clears answer
status: "formulated",
result: beforeSelect.result, // result stays (causal context)
};
// BUG: after selecting a follow-up, completed framing is lost
expect(shouldRenderCompletedFraming(afterSelect)).toBe(false);
});
it("FIXED: after follow-up selection with result, completed framing IS preserved → proves repair", () => {
const afterSelect = {
question: "Should we explore X?",
answer: null,
status: "formulated",
result: { observations: ["Finding A"], uncertainties: [], possibleFollowUpQuestions: ["Another option?"] },
};
expect(shouldRenderCompletedFraming_FIXED(afterSelect)).toBe(true);
});
});
// ── Simulated reverse rendering for Previous Learning ──────────────
function renderPriorContribs(contributions) {
// PriorContributionsSummary and SecondaryPreviousLearning use: slice(0, -1)
const prior = contributions.slice(0, -1);
return prior.map((c, idx) => ({
label: `Turn ${c.sequence || idx + 1}`,
sequence: c.sequence,
id: c.id,
}));
}
function renderPriorContribs_REVERSED(contributions) {
const prior = contributions.slice(0, -1);
// Reverse at presentation boundary only
return [...prior].reverse().map((c, idx) => ({
label: `Turn ${c.sequence || idx + 1}`,
sequence: c.sequence,
id: c.id,
}));
}
describe("v0.50 Prior contribs display order — reversed at presentation boundary", () => {
const threeContributions = [
{ id: "contrib-turn-1", sequence: 1, targetNodeId: "node-A", observations: ["Turn 1 finding"] },
{ id: "contrib-turn-2", sequence: 2, targetNodeId: "node-A", observations: ["Turn 2 finding"] },
{ id: "contrib-turn-3", sequence: 3, targetNodeId: "node-A", observations: ["Turn 3 finding"] }, // latest = excluded as current result
];
it("chronological order: slice(0,-1) preserves input sequence [1, 2]", () => {
const ordered = renderPriorContribs(threeContributions);
expect(ordered.map((c) => c.sequence)).toEqual([1, 2]);
});
it("REVERSED display order: newest prior turn first [2, 1]", () => {
const reversed = renderPriorContribs_REVERSED(threeContributions);
expect(reversed.map((c) => c.sequence)).toEqual([2, 1]);
});
it("reversal puts Turn 2 (newest prior) at top of the list for the user", () => {
const reversed = renderPriorContribs_REVERSED(threeContributions);
expect(reversed[0].label).toBe("Turn 2");
expect(reversed[1].label).toBe("Turn 1");
});
it("data order is unchanged — only the presentation layer reverses", () => {
const ordered = renderPriorContribs(threeContributions);
const reversed = renderPriorContribs_REVERSED(threeContributions);
// The data order (ordered) is unaffected by reversing
expect(ordered.map((c) => c.sequence)).toEqual([1, 2]);
// Reversal produces the opposite visual order for the user
expect(reversed.map((c) => c.sequence)).toEqual([2, 1]);
// Both refer to the same underlying contributions (same IDs)
const orderedIds = new Set(ordered.map((c) => c.id));
const reversedIds = new Set(reversed.map((c) => c.id));
expect(orderedIds).toEqual(reversedIds);
});
it("two-turn thread: reversal still works", () => {
const twoContributions = [
{ id: "contrib-turn-1", sequence: 1, targetNodeId: "node-B" },
{ id: "contrib-turn-2", sequence: 2, targetNodeId: "node-B" }, // excluded as current
];
const reversed = renderPriorContribs_REVERSED(twoContributions);
expect(reversed).toHaveLength(1);
expect(reversed[0].sequence).toBe(1);
});
it("single prior turn: reversal is no-op (one element)", () => {
const single = [
{ id: "contrib-turn-1", sequence: 1, targetNodeId: "node-C" },
{ id: "contrib-turn-2", sequence: 2, targetNodeId: "node-C" }, // excluded as current
];
const reversed = renderPriorContribs_REVERSED(single);
expect(reversed).toHaveLength(1);
expect(reversed[0].sequence).toBe(1);
});
it("reversal with non-sequential sequences preserves correct chronological reverse", () => {
const irregular = [
{ id: "contrib-A", sequence: 5, targetNodeId: "node-D" },
{ id: "contrib-B", sequence: 10, targetNodeId: "node-D" },
{ id: "contrib-C", sequence: 15, targetNodeId: "node-D" }, // excluded as current
];
const reversed = renderPriorContribs_REVERSED(irregular);
expect(reversed.map((c) => c.sequence)).toEqual([10, 5]);
});
});
// ── v0.50 INVESTIGATING cue on normal Open Questions buttons (actual render path) ───
describe("v0.49 Case A — completed turn provenance narrative", () => {
it("completed turn renders distinct user response and derived findings sections", () => {
const focused = {
question: "Completed question",