fix(confidence-engine): distinguish completed focused result
This commit is contained in:
@@ -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?" />
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user