Feature/product platform foundation v0.62 #1

Merged
robbond merged 683 commits from feature/product-platform-foundation-v0.62 into feature/emergent-unknowns-v0.5 2026-09-09 07:58:20 +01:00
3 changed files with 121 additions and 19 deletions
Showing only changes of commit ae1201bb27 - Show all commits
+31 -19
View File
@@ -274,25 +274,37 @@ function FocusedQuestionBody({
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Questions this raises</h3>
{(focused.result.possibleFollowUpQuestions || []).length > 0 ? (
<div className="space-y-1 mt-1">
{focused.result.possibleFollowUpQuestions.map((q, i) => {
const isCurrentQuestion = q === focused?.question;
return (
<button
key={i}
onClick={(e) => { if (!isCurrentQuestion) { e.stopPropagation(); setFollowUpQuestion(q); } }}
style={{ cursor: isCurrentQuestion ? "default" : "pointer" }}
className={`w-full text-left rounded-lg border px-3 py-2.5 text-sm leading-relaxed transition ${
isCurrentQuestion
? "border-gray-200 bg-gray-100/60 text-gray-400 cursor-default"
: "border-blue-200/60 bg-blue-50/40 text-gray-800 hover:border-blue-300 hover:bg-blue-100/60"
}`}
data-testid="follow-up-question"
>
{q}
{isCurrentQuestion ? " (current question)" : " → pick this question"}
</button>
);
})}
{hasActiveFollowUp
? focused.result.possibleFollowUpQuestions.filter((q) => q !== focused.question).map((q, i) => (
<button
key={i}
onClick={(e) => { e.stopPropagation(); setFollowUpQuestion(q); }}
className="w-full text-left rounded-lg border border-blue-200/60 bg-blue-50/40 px-3 py-2.5 text-sm leading-relaxed text-gray-800 transition hover:border-blue-300 hover:bg-blue-100/60 cursor-pointer"
data-testid="follow-up-question"
>
{q}
{" → pick this question"}
</button>
))
: focused.result.possibleFollowUpQuestions.map((q, i) => {
const isCurrentQuestion = q === focused?.question;
return (
<button
key={i}
onClick={(e) => { if (!isCurrentQuestion) { e.stopPropagation(); setFollowUpQuestion(q); } }}
style={{ cursor: isCurrentQuestion ? "default" : "pointer" }}
className={`w-full text-left rounded-lg border px-3 py-2.5 text-sm leading-relaxed transition ${
isCurrentQuestion
? "border-gray-200 bg-gray-100/60 text-gray-400 cursor-default"
: "border-blue-200/60 bg-blue-50/40 text-gray-800 hover:border-blue-300 hover:bg-blue-100/60"
}`}
data-testid="follow-up-question"
>
{q}
{isCurrentQuestion ? " (current question)" : " → pick this question"}
</button>
);
})}
</div>
) : (
<p className="text-xs text-gray-400">None yet</p>
+36
View File
@@ -2158,3 +2158,39 @@ Those are distinct presentation responsibilities. Separating them exposes a dupl
- Previous Learning single-owner presentation: CLOSED
- Previous Learning newest-first: CLOSED
- Post-answer promotion: PRESERVED
## v0.49 — ACTIVE FOLLOW-UP PRESENTATION SIMPLIFICATION
### Defect resolved
Selected follow-up candidate rendered twice inside "QUESTIONS THIS RAISES": once as a disabled `(current question)` row and again in the active continuation block above the textarea. Both rows contained the identical question text, creating visual redundancy.
### Fix summary
In `components/reasoning-workspace.jsx`, when `hasActiveFollowUp` is true (a follow-up has been selected), unselected candidates are still rendered with their existing selectable form (`→ pick this question`), but the candidate matching `focused.question` is filtered out from the candidate list entirely. The active continuation block already renders the selected question plus textarea + submit — no second rendering needed.
- Selected follow-up candidate now transforms into the active response block (single rendering).
- Duplicate `(current question)` / second-question rendering removed.
- Exactly one selected-question presentation.
- One textarea, Submit response visible.
- Completed Q/A provenance preserved.
- Previous Learning single-owner/newest-first preserved.
- Post-answer promotion preserved.
### Implementation detail
Production file changed: `components/reasoning-workspace.jsx` (candidate render boundary — lines ~275-300). No state added, no identity changes, no submit mechanics altered.
### Vitest config hygiene (from 8bded90)
vitest.config.js change classification: **A** — intentional and necessary (adds `environment: "jsdom"` required for React component testing in this repo).
### Tests
- Command: `npx vitest run tests/open-questions-vs-assumptions.test.jsx`
- Actual tests passed: **117** (up from 116 — one new regression test added)
- New regression assertion: verifies that after selecting a follow-up, the question text appears exactly once and no `(current question)` label is rendered.
### Build
- Result: PASS
@@ -2195,5 +2195,59 @@ describe("v0.49 RENDERED — in-place follow-up context ownership", () => {
const prevLearningHeadings = screen.queryAllByText(/previous learning/i);
expect(prevLearningHeadings).toHaveLength(1);
});
// ── v0.49 REGRESSION: selected follow-up must not render twice ──
it("v0.49 regression: selected follow-up renders exactly once, no '(current question)' duplicate", () => {
const turn1Q = "Completed question?";
const turn1A = "Complete answer.";
// Q3 completed with raised follow-up Q4
const contribs = [
makeContrib(turn1Q, turn1A, 1),
];
// After setFollowUpQuestion: focused.question === "Q4", possibleFollowUpQuestions = ["Q4"], hasActiveFollowUp = true
render(
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<FocusedQuestionBody
nodeId="node-test"
isFocused={true}
hasContent={true}
processingStep="idle"
formulationStep="active"
deconstructMsg=""
focusedAnswer=""
setFocusedAnswer={() => {}}
handleDeconstructSubmit={() => {}}
retryFormulation={() => {}}
setFollowUpQuestion={() => {}}
focused={
{
question: "Q4",
answer: null,
status: "formulated",
result: {
observations: ["finding"],
uncertainties: [],
possibleFollowUpQuestions: ["Q4"],
assumptions: [],
relationships: [],
},
error: null,
}
}
focusedContributions={contribs}
/>
</div>,
);
// The selected follow-up text should appear exactly once (in the active block), not duplicated in candidate row.
const q4Text = screen.getAllByText("Q4");
expect(q4Text).toHaveLength(1);
// After selection, "(current question)" label must NOT be rendered — the active form communicates selection unambiguously.
const currentQuestionLabels = screen.queryAllByText(/\(current question\)/i);
expect(currentQuestionLabels).toHaveLength(0);
});
});
});