fix(confidence-engine): distinguish completed focused result

This commit is contained in:
2026-08-29 18:09:26 +01:00
parent dac19a3552
commit 88d9768276
3 changed files with 237 additions and 1 deletions
+2 -1
View File
@@ -144,6 +144,7 @@ function FocusedQuestionBody({
}) { }) {
const hasContent = focused?.question?.trim() || formulationStep === "active" || processingStep === "active" || focused?.error; const hasContent = focused?.question?.trim() || formulationStep === "active" || processingStep === "active" || focused?.error;
const hasResult = Boolean(focused?.result); const hasResult = Boolean(focused?.result);
const hasAnswer = Boolean(focused?.answer);
// ── Local correction state (FQB-owned, not propagated upward) ─ // ── Local correction state (FQB-owned, not propagated upward) ─
const [editingFindingId, setEditingFindingId] = useState(null); const [editingFindingId, setEditingFindingId] = useState(null);
@@ -183,7 +184,7 @@ function FocusedQuestionBody({
<p className="text-sm text-blue-600/70">{formulateMsg}</p> <p className="text-sm text-blue-600/70">{formulateMsg}</p>
) : null} ) : null}
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && ( {focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && !hasAnswer && (
<div> <div>
<label htmlFor={`rw-answer-${nodeId}`} className="mb-2 block text-sm font-medium text-gray-700">Your response</label> <label htmlFor={`rw-answer-${nodeId}`} className="mb-2 block text-sm font-medium text-gray-700">Your response</label>
<textarea id={`rw-answer-${nodeId}`} value={focusedAnswer} onChange={(e) => setFocusedAnswer(e.target.value)} rows={4} data-testid="response-textarea" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-60" placeholder="What do you know about this?" /> <textarea id={`rw-answer-${nodeId}`} value={focusedAnswer} onChange={(e) => setFocusedAnswer(e.target.value)} rows={4} data-testid="response-textarea" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-60" placeholder="What do you know about this?" />
+24
View File
@@ -1971,3 +1971,27 @@ Target node: `n58lwnx`
- `Done for now` remains a separate semantic action - `Done for now` remains a separate semantic action
**Next boundary:** FOCUSED WORKSPACE COMPLETED-TURN / ACTIVE-TURN LIFECYCLE **Next boundary:** FOCUSED WORKSPACE COMPLETED-TURN / ACTIVE-TURN LIFECYCLE
## v0.49.7 — NARROW COMPLETED-RESULT REPAIR (answer-affordance contradiction)
- **Status:** CLOSED / CHECKPOINTED
- Previous broader completed-turn lifecycle repair was discarded (restored to checkpoint dac19a3)
- Narrow repair: a reopened completed turn preserves existing current-result presentation but NO longer exposes a response textarea for the already-answered question
- Repair mechanism: added `!hasAnswer` (`Boolean(focused?.answer)`) to the textarea render condition in `FocusedQuestionBody` — line 186 of `reasoning-workspace.jsx`
- Explicit follow-up selection resets `answer` to null via `setFollowUpQuestion()`, creating unanswered state and exposing textarea
- Fresh investigation behaviour preserved (first-turn textarea still appears)
- Latest completed turn intentionally remains as current result (not moved into Previous Learning) — deferred to a later presentation/lifecycle decision
- No persistence changes, no new lifecycle enums, no new state fields added
- No LLM calls during verification or tests
- Pre-fix regression: OLD condition (`shouldShowResponseTextarea_OLD`) incorrectly returned `true` for completed turns (defect proved)
- Post-fix: 97/97 targeted tests PASS
- Build: PASS
- Playwright live reopen: no "Formulating your question…" regression; completed current result shows NO textarea
- Playwright explicit follow-up: selected follow-up becomes current QUESTION, fresh textarea with `What do you know about this?` placeholder appears
- Return to overview: UNCLEAR + INVESTIGATING · 3 learned contributions preserved
- Live fixture: reused existing 3-contribution onboarding investigation (no cold reload)
### Unresolved boundaries (deferred)
- latest-completed-turn → Previous Learning repartition (intentional defer — separate presentation/lifecycle decision)
- workspace control UX wording ("Back to open questions" / "Done for now" / "Close investigation")
@@ -1393,3 +1393,214 @@ describe("INVESTIGATING cue on Open Question buttons (normal render path)", () =
expect(badgeUntouched).toBeNull(); expect(badgeUntouched).toBeNull();
}); });
}); });
// ── NARROW TEXTAREA RENDERING CONDITION (v0.49 completed-result repair) ───
// The response textarea must appear IFF the current displayed turn is NOT already completed.
function shouldShowResponseTextarea(focused, processingStep) {
// Mirrors the exact render condition in FocusedQuestionBody:
// focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated"
// PLUS the repair: must NOT already have a completed answer.
const hasAnswer = Boolean(focused?.answer);
return (
Boolean(focused?.question?.trim()) &&
processingStep !== "active" &&
focused.status === "formulated" &&
!hasAnswer
);
}
function shouldShowResponseTextarea_OLD(focused, processingStep) {
// Mirrors the OLD buggy render condition in FocusedQuestionBody (pre-fix):
// missing !focused.answer check — causes textarea to show for completed turns
return (
Boolean(focused?.question?.trim()) &&
processingStep !== "active" &&
focused.status === "formulated"
);
}
// ── PRE-FIX REGRESSION PROOF: OLD condition incorrectly shows textarea ──
describe("Pre-fix regression: OLD condition erroneously shows textarea for completed turns", () => {
it("OLD condition shows textarea for reopened completed turn (PROVES defect exists)", () => {
const contrib = {
id: "contrib-1",
question: "Onboarding funnel step with highest abandonment?",
answer: "Step 3 — email verification, 42% drop-off.",
status: "formulated",
};
const focused = {
status: "formulated",
question: contrib.question,
answer: contrib.answer,
result: null,
error: null,
};
// OLD condition: all three checks pass → textarea incorrectly shown
// This PROVES the defect exists in dac19a3
expect(shouldShowResponseTextarea_OLD(focused, "idle")).toBe(true);
});
it("FIXED condition does NOT show textarea for reopened completed turn", () => {
const contrib = {
id: "contrib-1",
question: "Onboarding funnel step with highest abandonment?",
answer: "Step 3 — email verification, 42% drop-off.",
status: "formulated",
};
const focused = {
status: "formulated",
question: contrib.question,
answer: contrib.answer,
result: null,
error: null,
};
// FIXED condition: !hasAnswer is false → textarea suppressed
expect(shouldShowResponseTextarea(focused, "idle")).toBe(false);
});
});
describe("Narrow textarea rendering condition — v0.49 completed-result repair", () => {
// ── Simulated fixture: a completed contribution with question + answer ──
function makeCompletedContrib() {
return {
id: "contrib-completed-1",
targetNodeId: "oq-test",
originatingTargetNodeId: "oq-test",
question: "What specific step of the onboarding funnel has the highest abandonment rate?",
answer: "Step 3 — email verification. Data shows 42% drop-off at this gate.",
sequence: 1,
observations: ["Step 3 is the critical drop point"],
possibleFollowUpQuestions: [
"What drives the Step 3 abandonment?",
"Can we reduce friction at Step 3?",
],
};
}
function makeContributions(n) {
return Array.from({ length: n }, (_, i) => ({
id: `contrib-${i}`,
targetNodeId: "oq-test",
originatingTargetNodeId: "oq-test",
question: `Question ${i + 1}`,
answer: `Answer ${i + 1} — completed with data.`,
sequence: i + 1,
observations: [`Observation ${i + 1}`],
possibleFollowUpQuestions: [`Follow-up from turn ${i + 1}`],
}));
}
// ── CASE A — reopened completed result must NOT show textarea ──
describe("Case A — reopened completed result suppresses textarea", () => {
const contrib = makeCompletedContrib();
it("single completed turn: has question + answer, should NOT show textarea", () => {
// Simulated startFocused reopen path: reconstructs latest Contribution
const focused = {
status: "formulated",
question: contrib.question,
answer: contrib.answer, // non-null = completed turn
result: { observations: contrib.observations, possibleFollowUpQuestions: contrib.possibleFollowUpQuestions },
error: null,
};
const processingStep = "idle"; // not processing anything
expect(focused.question?.trim()).toBeTruthy();
expect(focused.status).toBe("formulated");
expect(focused.answer).toBeTruthy();
// The repaired condition: answer exists → no textarea
expect(shouldShowResponseTextarea(focused, processingStep)).toBe(false);
});
it("multiple completed turns (3): latest has answer, should NOT show textarea", () => {
const contributions = makeContributions(3);
const latest = contributions[2];
const focused = {
status: "formulated",
question: latest.question,
answer: latest.answer,
result: { observations: latest.observations, possibleFollowUpQuestions: latest.possibleFollowUpQuestions },
error: null,
};
expect(shouldShowResponseTextarea(focused, "idle")).toBe(false);
});
it("contributions exist but answer is null (data integrity edge): shows textarea", () => {
// Edge case: what if an answer somehow became null?
const contrib = makeCompletedContrib();
const focused = {
status: "formulated",
question: contrib.question,
answer: null, // should not happen normally but let's handle it
result: null,
error: null,
};
// If answer is genuinely missing, textarea appears so user can provide one
expect(shouldShowResponseTextarea(focused, "idle")).toBe(true);
});
});
// ── CASE B — explicit follow-up selection shows textarea ──
describe("Case B — explicit follow-up selection creates unanswered state", () => {
it("after setFollowUpQuestion: answer=null → textarea appears", () => {
const contributions = makeContributions(1);
// Step 1: reopen completed thread (answer is non-null)
const beforeSelect = {
status: "formulated",
question: contributions[0].question,
answer: contributions[0].answer,
result: null,
error: null,
};
expect(shouldShowResponseTextarea(beforeSelect, "idle")).toBe(false);
// Step 2: user selects a follow-up — simulates setFollowUpQuestion()
const focusedAfterSelect = {
...beforeSelect,
question: contributions[0].possibleFollowUpQuestions[0],
answer: null, // setFollowUpQuestion sets answer: null
};
// After explicit selection: textarea should appear
expect(shouldShowResponseTextarea(focusedAfterSelect, "idle")).toBe(true);
});
});
// ── CASE C — fresh investigation preserves textarea ──
describe("Case C — fresh investigation preserves textarea", () => {
it("fresh formulated question with no prior answer: shows textarea", () => {
const focused = {
status: "formulated",
question: "What do you think about this approach?",
answer: null, // no prior completion
result: null,
error: null,
};
expect(shouldShowResponseTextarea(focused, "idle")).toBe(true);
});
it("processing active: does NOT show textarea even with question", () => {
const focused = {
status: "formulated",
question: "Active analysis in progress",
answer: null,
result: null,
error: null,
};
expect(shouldShowResponseTextarea(focused, "active")).toBe(false);
});
});
});