feat(studies): morning summary card + strategy line (feat_overnight_final_solution_phase2)#442
Conversation
…tion_phase2 - idea.md preflight patches (5 edits, applied 2026-06-03): - Fix stale convergence-indicator folder path (2026_05_31 → 2026_06_01). - Cap 1 visibility predicate cites CHAIN_STOP_REASONS source-of-truth + frames it as Q2. - Cap 2 sibling-coordination surfaces sibling's locked cookie-only + /chains/recent decisions. - Cap 3 typo fix: "narrow" / None / "narrow" → "narrow" / None (default narrow). - Cap 4 adds implementation note: convergence_verdict is on StudyDetail.convergence, NOT StudyChainLink. - feature_spec.md drafted + GPT-5.5 cross-model reviewed (3 cycles, 22 D-entries logged). - implementation_plan.md drafted + GPT-5.5 cross-model reviewed (3 cycles). - MVP dashboards regenerated by pre-commit hook. Single-PR Phase 2 — frontend-only. No backend change, no migration, head stays 0022. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
FR-8 — extract CHAIN_STOP_REASON_PHRASE + formatSignedLift into shared modules (ui/src/lib/chain-stop-reason.ts + ui/src/lib/format-lift.ts) so the new <OvernightResultCard> can consume identical formatting. The existing <AutoFollowupChainPanel> imports the shared modules; behavior is byte- identical (existing tests pass unchanged). FR-6 — add two new glossary keys: overnight_result (the morning card's InfoTooltip) and auto_followup_strategy_line (the read-only strategy line's InfoTooltip). Landed in Story 1 (not Story 6) per cycle-1 finding C1-1 because <InfoTooltip>'s glossaryKey is type-locked to ShortGlossaryKey (ui/src/components/common/info-tooltip.tsx:14-16; glossary.ts:987); stories 3 + 5 that use the new keys must typecheck against the live glossary. Tests: - ui/src/__tests__/lib/chain-stop-reason.test.ts (new) — 3 cases - ui/src/__tests__/lib/format-lift.test.ts (new) — 5 cases - ui/src/__tests__/lib/glossary.test.ts (extended) — 3 new lock cases for overnight_result + auto_followup_strategy_line Verification gate: pnpm lint + pnpm typecheck + pnpm test + pnpm build all green (1065 tests, 0 lint errors). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
FR-3 — add ui/src/lib/chain-path-tokens.ts exporting pathTokenForLink,
the pure data→data helper that maps a chain link's selected_followup_kind
to a short token for the morning card's "Explored: …" line.
Implementation pattern per cycle-1 finding C1-7: TOKEN_RENDERERS is typed
as Record<SelectedFollowupKind, ...>, so adding a new value to
SELECTED_FOLLOWUP_KIND_VALUES upstream breaks the build until the renderer
map is extended. Mirrors the Phase 1 CHAIN_STOP_REASON_PHRASE pattern;
satisfies the spec's "no hardcoded kind literals" anti-pattern via the
exhaustive Record type.
Truncation contract:
- Short template names → "swap to {name}".
- Names > 24 chars → "swap to {first 24}…".
- Null templateName → "swap to {first 6 of template_id}" graceful-degrade.
Callers MUST filter null-token links BEFORE rendering child components per
cycle-1 finding C1-3 — rendering null-token children would emit dangling
" → " separators.
Tests: ui/src/__tests__/lib/chain-path-tokens.test.ts — 7 cases.
Verification gate: pnpm typecheck + lint + targeted test all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
…e (Story 3)
FR-1 + FR-3 + FR-5 + FR-7 — add ui/src/components/studies/overnight-result-card.tsx
mounted between <StudyHeaderWithSyntheticChip> and <LinkedEntitiesRow> on
/studies/{id} via ui/src/app/studies/[id]/page.tsx. Renders headline +
explored-path tokens + best-config CTA + stop-reason line (with conditional
InfoTooltip for depth_exhausted/budget) + narrative excerpt with
"View full digest →" link. Returns null when the predicate fails.
Hook-order invariance per spec D-19: useStudyChain → useStudyDigest → derive
predicate → early return. The card also exports shouldShowOvernightResultCard
(FR-7 predicate) and truncateNarrative (FR-5 helper) so they can be unit-tested
directly without rendering React.
FR-3 child-component pattern (D-11): per-link useTemplate calls live in
<PathTokenChip> child. Parent filters null-token links (cycle-1 C1-3) BEFORE
mounting children so isLast is correct and no dangling " → " can render.
Best-config three-case render matrix per D-13. Narrative section consumes
digestQ.data via the hook declared at top per D-22. truncateNarrative walks
sentence → whitespace → hard-fallback (D-15).
Story 4 will mount <WinningLinkConvergenceChip> in <CardTitle> (commented
placeholder); Story 4 also adds id="digest" to <DigestPanel>.
Tests — 21 cases in overnight-result-card.test.tsx:
- 4 direct unit tests for shouldShowOvernightResultCard.
- 4 direct unit tests for truncateNarrative.
- AC-1 partial, AC-2, AC-3, AC-4, AC-5, AC-11.
- 3 best-config matrix cases.
- 1 mixed-token-chain filter case.
- 3 stop-reason tooltip cases.
Verification gate green: lint + typecheck + test + build all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
…ory 4)
FR-4 — add WinningLinkConvergenceChip child component to OvernightResultCard.
Parent-gates-mount per spec D-18: the parent only renders the chip when
chain.best_link_id !== null. Inside the child,
useStudy(linkId, { enabled: linkId !== viewedStudy.id }) fires unconditionally;
the enabled gate skips the cross-study fetch when the operator is on the
winner's own page (verdict reads from viewedStudy.convergence directly per
AC-10). Cycle-1 finding C1-4: uses <Badge variant="secondary"> with
VERDICT_LABEL Record exhaustiveness so unknown verdicts break the build.
FR-5 / D-22 — add id="digest" to <DigestPanel>'s outer <Card> so the
card's "View full digest →" link (Story 3) anchors correctly. No behavior
change to DigestPanel.
Tests — 4 new cases:
- AC-1 chip portion: cross-study useStudy → "Converged" badge.
- AC-6: null convergence → chip hidden.
- AC-10: viewed study IS the winner → chip reads viewedStudy.convergence.
- chain.best_link_id null → chip not mounted at parent gate.
Verification gate: lint + typecheck + 25/25 tests + build all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
FR-2 — extend ui/src/components/studies/linked-entities-row.tsx with a read-only "Strategy: …" line as a fifth item after the four existing FK chips. Renders only when study.config.auto_followup_strategy matches one of the values in OVERNIGHT_STRATEGY_VALUES (the type-locked mirror of backend AUTO_FOLLOWUP_STRATEGY_VALUES). Hidden for null / missing / unknown values (defensive — Phase 1 D-13 lets bad JSONB through the backend in principle). Display mapping per spec FR-2 via Record<OvernightStrategy, string> exhaustiveness: "narrow" → "Refine same knobs" "follow_suggestions" → "Try suggested follow-ups" Includes the auto_followup_strategy_line InfoTooltip (glossary key added in Story 1). Source-of-truth comment above STRATEGY_DISPLAY cites ui/src/lib/enums.ts OVERNIGHT_STRATEGY_VALUES per CLAUDE.md "Enumerated Value Contract Discipline". Tests — 5 new cases extending linked-entities-row.test.tsx (existing 3 cases pass unchanged): - AC-7: follow_suggestions → "Try suggested follow-ups" rendered. - AC-9: narrow → "Refine same knobs" rendered. - AC-8: config missing the key → line hidden. - config === null (legacy) → line hidden. - Defensive unknown wire value → line hidden (allowlist check). Wrapped TooltipProvider in the test wrapper since the new line carries an InfoTooltip. Verification gate: pnpm lint (0 errors) + pnpm typecheck + pnpm test full suite (1102/1102 green) + pnpm build all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
FR-9 — extend docs/08_guides/tutorial-first-study.md Step 12 with a new sub-section "In the morning — read the overnight result card" describing the card's mount point + rendered sections (headline, convergence chip, explored path, best-config CTA, stop reason, narrative summary). Strategy line is also documented at the end of the sub-section. ui/public/docs/ regenerated via node scripts/copy-docs.mjs to keep the in-app guide freshness gate green. Architecture — add a new "Study detail page — vertical stack" section to docs/01_architecture/ui-architecture.md describing the morning card mount point, the shared chain-stop-reason + format-lift modules introduced by Story 1 (FR-8), and the strategy line inside LinkedEntitiesRow. Cited file paths for every new module. E2E coverage — ui/tests/e2e/overnight-result-card.spec.ts (new). Two tests using the existing seedAutoFollowupChain real-backend helper: - Negative predicate: in-flight chain → card hidden (the FR-7 stop-rule case that's most likely to break if a future refactor weakens the predicate). - Positive predicate: terminated 3-link chain (depth=2) with all links completed → card visible; path line absent because the test seeder creates chain links with NULL selected_followup_kind (legacy narrow pattern per Phase 1 D-12); best-config falls back per FR-1 / D-13 matrix because the seeder doesn't create proposals; stop-reason line renders with a non-empty phrase. The AC-12 click-through to a winning proposal is BEST-EFFORT against the demo seed per spec D-17 — full click-through coverage lives in the vitest suite (real component + mocked hooks). Documented inline. Note: screenshot deliverable (FR-9) deferred — the demo seed does not produce a chain that exercises the path / convergence / narrative card state. Operator will need to run `pnpm capture-guides` after manually seeding a follow_suggestions chain via the wizard. A follow-up issue will track the screenshot regen if needed; the prose lands as-is. Verification gate: pnpm lint (0 errors) + pnpm typecheck + pnpm build all green. Playwright `--list` confirms the new spec parses (2 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
There was a problem hiding this comment.
Code Review
This pull request implements Phase 2 of the overnight autopilot final solution, introducing a top-of-page morning summary card () and a read-only strategy line () on the study detail page. It extracts shared formatting helpers, adds new glossary keys, updates documentation, and introduces comprehensive unit and E2E tests. The reviewer suggests improving the layout of the strategy line by rendering it as an inline-flex container with items-center and a consistent gap to ensure perfect vertical alignment and simplify the markup.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| <span data-testid="study-strategy-line"> | ||
| <span className="text-muted-foreground">Strategy:</span> {STRATEGY_DISPLAY[strategy]} | ||
| <span className="ml-1 inline-flex"> | ||
| <InfoTooltip glossaryKey="auto_followup_strategy_line" /> | ||
| </span> | ||
| </span> |
There was a problem hiding this comment.
To ensure visual consistency and perfect vertical alignment with the other <Entry> items in the row, we should render the strategy line as an inline-flex container with items-center and a consistent gap. This also simplifies the markup by removing the extra wrapper span around the InfoTooltip.
<span data-testid="study-strategy-line" className="inline-flex items-center gap-1">
<span className="text-muted-foreground">Strategy:</span>
<span>{STRATEGY_DISPLAY[strategy]}</span>
<InfoTooltip glossaryKey="auto_followup_strategy_line" />
</span>
Gemini review on PR #442 (medium): use inline-flex + items-center + gap-1 on the outer <span> and split the value into its own <span>, removing the extra wrapper around InfoTooltip. Simpler markup + better vertical alignment with the surrounding row. Tests updated: \s+ → \s* in the Strategy text-content regex because CSS gap replaces the literal text-node whitespace between label and value. Vitest: 8/8 linked-entities-row cases pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
Review adjudicationCommits landing fixes: Gemini Code Assist (1 finding)
Outcomes
Pre-push gate green: |
…t chore GPT-5.5 final review found two material issues: 1. AC-11 test was tautological (only checked the hook mock was called, not what args it was called with). Strengthened to assert useStudyDigest is called with chain.best_link_id (the wiring dedup depends on) AND with enabled=true when the predicate fires. Added a complementary AC-11 negative test asserting the D-22 ?? undefined coercion + enabled=false path when no winner exists. 2. FR-9 screenshot deferred per the plan's hard-fallback escape hatch (the demo seed cannot produce a follow_suggestions chain with a winning digest/proposal that exercises the card's full rendered state). Filed chore_overnight_result_card_screenshot/idea.md documenting the manual-capture path + optional seed-chain extension. Dashboards regenerated by pre-commit hook. Vitest: 26/26 overnight-result-card cases pass (added 1 new AC-11 negative). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
GPT-5.5 final review adjudicationCommits landing fixes: GPT-5.5 final review (2 findings)
Outcomes
Pre-push gate green again on the new SHA: lint + typecheck + 26/26 overnight-result-card cases + build all pass. Ready for human review + merge. |
…443) - Move feature folder planned_features/02_mvp2/ → implemented_features/2026_06_04_feat_overnight_final_solution_phase2/. - pipeline_status.md: Implementation Complete + Release: mvp2 marker. - implementation_plan.md: status → Complete (PR #442, merged 2026-06-04). - state.md: new merge one-liner (drop oldest to keep Last 5), branch + in-flight + Last-updated refreshed. - state_history.md: full feature-merge narrative prepended. - Dashboards regenerated by pre-commit hook. Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…proposal_full_param_space_view) (#446) * docs(idea): apply preflight-2026-06-04 patches to feat_proposal_full_param_space_view Lands the audit-and-patch edits from a prior /idea-preflight run that had been sitting in the working tree, plus the dashboard regen that follows. - Header: "preflight-refreshed 2026-06-04" + linkified sibling references to the now-implemented overnight trio (PR #440 / #442 / #444). - Cap 1 grounds the panel target on <ConfigDiffPanel> at ui/src/components/proposals/config-diff-panel.tsx:63 (replacing the "Recommended config" placeholder which doesn't match the live tree). - declared_params shape correction: flat Record<string, type-tag>, NOT per-param bounds/defaults — bounds live on study.search_space. - Cap 2 coordination note pointing at the existing parent-vs-swap-target diff in <SuggestedFollowupsPanel> (suggested-followups-panel.tsx:250-291) so the new panel reuses the established visual grouping. - Scope signals: "backend: none required" — confirmed the proposal page already pays for useTemplate(parentStudy.template_id) at ui/src/app/proposals/[id]/page.tsx:183; chain-link proposals inherit template_id from parent (backend/workers/auto_followup.py), so the template's declared_params is on-page for free. - Q2 + Q3 rewritten against the corrected data shapes. - MVP2_DASHBOARD.md status cell picks up the linkified feat_overnight_final_solution reference. No new content; this is the preflight audit-and-patch result. Spec generation runs next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * docs(spec): feat_proposal_full_param_space_view — three-state full-param-space panel on proposals Generates the feature specification + pipeline_status.md from the preflight-refreshed idea.md. Converged across 3 GPT-5.5 cycles + 1 Opus internal verification pass. The spec adds a new <FullParamSpacePanel> on /proposals/[id] that renders every parameter the proposal's template declares, partitioned into three visually distinct groups derived client-side from data already on the page: 1. Tuned (changed by this proposal) — appears in proposal.config_diff, rendered with from→to delta via the existing extractFromTo helper (promoted to ui/src/lib/config-diff.ts per FR-5). 2. Tuned (unchanged) — in study.search_space.params but not in config_diff (the optimizer considered it but the digest's recommended_config didn't include it). 3. Not in search space — declared on the template but absent from this study's tuning surface. The pure helper partitionTemplateParams in ui/src/lib/proposal-param-space.ts owns the partition algorithm; <FullParamSpacePanel> is a thin renderer. Both are unit-testable without DOM. Backend: NONE. Migration: NONE. The feature consumes existing endpoints (proposals/{id}, studies/{id}, query-templates/{id}) only. Cross-model review highlights (3 cycles, 19 findings total — 18 accepted+applied, 1 rejected with cited counter-evidence): - Cycle 3 F1 (High, accepted): caught a real correctness bug — the earlier FR-3 only lifted useTemplate's gate, not useStudy's, so study proposals with text-only digests would have mounted the panel with searchSpaceParams undefined and mis-classified every search-space param as 'untuned' instead of 'tunedUnchanged'. FR-3 + D-13 now require lifting BOTH fetches. - Cycle 2 F2 (High, accepted): FR-4 race-aware gating contradicted the section 11 narrative; aligned (panel waits for both parentTemplate + parentStudy.isPending=false before mounting for study-backed cases). - Cycle 1 F8 (Medium, rejected): GPT-5.5 worried the lifted useTemplate would change <SuggestedFollowupsPanel>'s rendering for previously-disabled cases. Rejected with counter-evidence at suggested-followups-panel.tsx:90-95+119-130 — parentTemplate is structurally consumed only by <SwapTemplateCard>, so non- swap-template proposals are indifferent to the prop. Caps 2 + 3 from the idea (cross-panel hover linking + study-detail mount) are explicitly deferred WITHOUT phase*_idea.md artifacts (D-8 + D-14) — reopen only if specific operator feedback surfaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * docs(plan): feat_proposal_full_param_space_view — 4-story plan with lifted-fetch race-aware mount Generates the implementation plan from the approved spec. Converged across 3 GPT-5.5 cycles + 1 Opus internal verification pass — 19 findings total, 18 accepted+applied, 1 rejected with cited counter- evidence. Plan structure: 1 epic, 4 stories, dependency-ordered (story number IS execution order): Story 1.1 — Promote extractFromTo + renderValue to ui/src/lib/config-diff.ts (shared helper extraction, 7 unit cases, AC-9 byte-identical preservation of ConfigDiffPanel rendering). Story 1.2 — Pure helper partitionTemplateParams (the FR-1 partition algorithm — 8 unit tests covering AC-1/2/3/5/6, D-9 search- space drift drop, D-10 from===to anomaly, sort stability). Story 1.3 — <FullParamSpacePanel> component + new proposal.full_param_space glossary key (7 component vitest tests, AC-1/2/5/6/7/8). Story 1.4 — Page-level integration: lift BOTH useTemplate AND useStudy gates (drop hasActionableFollowup); race-aware conditional mount; 6 page-level vitest tests + 1 real-backend Playwright E2E test. Cross-model review highlights: - Cycle 1 F7 (Low, rejected): GPT-5.5 worried seedManualProposal wasn't a real helper. Counter-evidence: it's defined locally at proposals.spec.ts:21-36 as a 3-helper composition. - Cycle 3 F1 (High, accepted): caught a TypeScript build-breaker — Object.keys + indexed access on Record<string, string> with the project's noUncheckedIndexedAccess gate yields string | undefined. Algorithm now uses Object.entries (type-narrowed) + ?? '(unknown)' fallback for the AC-6 drift path. - Cycle 3 F6 (Medium, accepted): missing dedicated test for FR-7 edge case A (source-study fetch error). Added as page-level Test 6. - Cycle 1 F2 (High, accepted): the cycle-3 F1 regression guard was bundled into the happy-path test which uses swap_template (already actionable, so wouldn't catch the bug). Split into dedicated Test 5 with empty/text-only digest. - Cycle 3 F5 (Medium, accepted): Test 4 race-gating used single deferred resolver — could pass vacuously if template was also pending. Switched to dual-deferred + qc.getQueryState confirmation. No backend changes, no migrations, no audit events. Plan is fully frontend, single-phase, no phase*_idea.md artifacts per D-14. Total test coverage: 7 (config-diff unit) + 8 (partition unit) + 7 (panel component) + 6 (page-level vitest) + 1 (real-backend Playwright E2E) = 29 new tests. Existing tests stay byte-identical (AC-9 + AC-10 enforce this). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * refactor(proposals): promote extractFromTo + renderValue to shared module (Story 1.1) Extract the two config_diff value-rendering helpers from config-diff-panel.tsx into ui/src/lib/config-diff.ts so the new <FullParamSpacePanel> (Story 1.3) can reuse the same canonical {from, to}-vs-2-tuple normalization without duplication. - New ui/src/lib/config-diff.ts exports extractFromTo + renderValue. - config-diff-panel.tsx re-imports both; rendering byte-identical (AC-9). - New config-diff.test.ts: 7 cases (3 extractFromTo + 4 renderValue). - Existing config-diff-panel.test.tsx passes unchanged (6 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * feat(proposals): pure partitionTemplateParams helper (Story 1.2) The FR-1 partition algorithm — partitions a template's declared params into tunedChanged / tunedUnchanged / untuned given the proposal's config_diff + the source study's search_space.params. - Partition universe is declaredParams union configDiff (D-9); search-space- only drift keys are silently dropped. - config_diff membership is the operational definition of "tuned" (D-10: a from===to anomaly still classifies as tunedChanged). - Drift keys (in config_diff, not in declared_params) render type '(unknown)' (AC-6). - Uses Object.entries (type-narrowed) to satisfy noUncheckedIndexedAccess. - 8 unit tests covering AC-1/2/3/5/6, D-9, D-10, sort stability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * feat(proposals): FullParamSpacePanel component + glossary key (Story 1.3) A new <FullParamSpacePanel> renders the three-state partition (tuned-changed / tuned-unchanged / not-in-search-space) for a proposal's template, consuming the pure partitionTemplateParams helper. - Card shell + InfoTooltip matching the existing proposal-panel pattern. - Three labeled groups, each omitted when empty; full-universe-empty shows the param-space-empty state (declaredParams AND config_diff both empty — AC-6 drift path takes precedence otherwise). - tunedChanged rows show from→to; tunedUnchanged "(no change)"; untuned italic — reusing <DeclaredParamsColumn>'s typography. - New proposal.full_param_space glossary key (FR-6). - 7 component tests (AC-1/2/5/6/7/8 + full-empty defensive); glossary AC-12 audience-language check passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * feat(proposals): mount FullParamSpacePanel with lifted fetches + race-aware gating (Story 1.4) Page-level integration of the full-parameter-space panel on /proposals/[id]. - Lift BOTH useTemplate (now sourced from proposal.template.id, null-safe) AND useStudy (drop the `&& hasActionableFollowup` gate) so the panel has declared_params + search_space.params for EVERY proposal shape (FR-3 / D-13). Removed the now-dead hasActionableFollowup variable. - Mount the panel below ConfigDiffPanel with race-aware gating: wait for parentTemplate.data AND, for study-backed proposals, parentStudy settled (FR-4) so the tunedUnchanged group never flashes a transient mis-classification. - 6 new page-level vitest tests: happy path, manual proposal, template 404, race-gating (dual-deferred resolver), FR-3 regression guard (no-actionable-followups digest), FR-7 edge A (study fetch error). - 1 new real-backend Playwright E2E asserting the panel renders for a seeded manual proposal. All 18 page tests pass (12 existing + 6 new). All 5 proposals E2E pass (verified against a rebuilt production container). tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * test(proposals): strengthen tests per GPT-5.5 phase-gate review Phase-gate cross-model review findings (4 accepted, 1 rejected): - F1 (accepted): D-9 search-space-drift unit test now uses searchSpaceParams={phantom} only, asserting `foo` (declared, not in search space) → untuned AND phantom dropped — covers the classification the prior version skipped by including foo in search space. - F2 (accepted): component AC-1 test now asserts the group-header count text ("2 parameters" / "1 parameter") per FR-2, not just the testids. - F3 (accepted): page template-404 test now asserts <PrPanel> (open-pr-button) stays visible alongside ConfigDiffPanel + metric-delta. - F4 (accepted): race-gating test converted to the documented dual-deferred resolver pattern (both template + study deferred) to eliminate any vacuous-pass window. - F5 (rejected): GPT wanted an exhaustive switch(group)/never default; counter-evidence — GROUP_LABELS: Record<ParamSpaceGroup, string> in full-param-space-panel.tsx already gives compile-time exhaustiveness (a 4th variant is a type error at the literal). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * docs(guides): regenerate guide 02 screenshots for the full-parameter-space panel The new <FullParamSpacePanel> mounts on /proposals/[id] between the config-diff and metric-delta panels, so guide 02's proposal-detail screenshot (03-proposal-detail.png) now shows it. Regenerated the full guide-02 screenshot set against the running stack so the walkthrough reflects the shipped UI. 03-proposal-detail.png confirms the panel renders correctly end-to-end: config_diff drift keys (description.boost / title.boost, type "(unknown)" since the seeded template declares only `boost`) under "Tuned (changed by this proposal)", and `boost` under "Not in search space". The other four PNGs changed only from re-seeded demo data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * fix(proposals): null-safe search-space guard + grid-aligned tuned rows (Gemini review) Gemini Code Assist adjudication (both accepted): - G1 (High): `searchSpaceParams !== undefined && key in searchSpaceParams` throws TypeError when searchSpaceParams is null (null !== undefined is true, and `key in null` throws). JSONB study.search_space.params can be null at runtime. Fixed with a truthiness guard + widened the PartitionInput/prop type to Record | null | undefined to be honest about the runtime contract. Added a null-search-space regression unit test. - G2 (Medium): tunedChanged rows now use a CSS grid so name/type/from/→/to align vertically across rows (matching ConfigDiffPanel's table columns). Tests are layout-agnostic and stay green. 34 lib+component+page tests pass; tsc + build + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * test(proposals): close race-gating coverage hole (GPT-5.5 final review FF1) The AC-11 race-gating test asserted that tuned_unchanged + empty were absent during the template-ready/study-pending window — but BOTH are absent even on a premature mount (config_diff empty + no search space → foo classifies as `untuned`, not tuned_unchanged or empty). So the guard would have passed even if the panel mounted too early. Add assertions that param-space-group-untuned AND param-space-row-untuned-foo are also absent during the race window — these WOULD render on a premature mount, so the test now genuinely catches the race bug FR-4's gating defends against. (GPT-5.5 final review FF2 — "ACTIONABLE_FOLLOWUP_KINDS unused" — rejected: still consumed at page.tsx:196 in the prefillValues useMemo; tsc + lint clean.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> --------- Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
<OvernightResultCard>above<LinkedEntitiesRow>on/studies/{id}— mounts when the auto-followup chain has terminated AND has at least 2 links. Renders headline lift + convergence chip + explored-path tokens + best-config CTA + stop-reason phrase + winning-link narrative excerpt with a "View full digest →" link.<LinkedEntitiesRow>for every study whoseconfig.auto_followup_strategyis set, so mid-chain operators see the strategy at a glance without scrolling to the chain panel.CHAIN_STOP_REASON_PHRASE+formatSignedLiftfrom the existing chain panel into shared modules (ui/src/lib/chain-stop-reason.ts,ui/src/lib/format-lift.ts) so the same number / phrase never renders in two different formats on the same page (Phase 2 D-12).Phase 2 of
feat_overnight_final_solution. Frontend-only — no backend code, no migration, Alembic head stays at0022_solr_engine_auth_check. Six stories merged in a single PR:overnight_result/auto_followup_strategy_lineglossary keys (FR-6 / FR-8).pathTokenForLinkhelper withRecord<SelectedFollowupKind, …>exhaustiveness (FR-3).<OvernightResultCard>shell + headline + path + best-config + stop-reason + narrative +truncateNarrative+shouldShowOvernightResultCard(FR-1 / FR-3 / FR-5 / FR-7).<WinningLinkConvergenceChip>+id="digest"anchor on<DigestPanel>(FR-4 / FR-5 anchor).<StrategyLine>inside<LinkedEntitiesRow>(FR-2).Cycle-2 spec restructure: narrative section moved into Story 3 (not Story 4) so
digestQis consumed where it's declared, avoiding an unused-variable lint failure under the invariant hook-order rule (D-19).Test coverage
ui/src/__tests__/lib/chain-stop-reason.test.tsui/src/__tests__/lib/format-lift.test.tsui/src/__tests__/lib/chain-path-tokens.test.tsui/src/__tests__/lib/glossary.test.tsui/src/__tests__/components/studies/overnight-result-card.test.tsxshouldShowOvernightResultCard+truncateNarrative)ui/src/__tests__/components/studies/linked-entities-row.test.tsxui/tests/e2e/overnight-result-card.spec.tsFull suite: 1102 / 1102 vitest tests green;
pnpm lint0 errors;pnpm typecheckclean;pnpm buildsucceeds.Test plan
cd ui && pnpm lint && pnpm typecheck && pnpm test && pnpm buildall green (run pre-push).shouldShowOvernightResultCardandtruncateNarrative(FR-7 / FR-5 / D-15 cycle-2 C2-6).<AutoFollowupChainPanel>+<DigestPanel>+<LinkedEntitiesRow>tests pass unchanged (proves Story 1 + Story 4 + Story 5 refactors are mechanical).pnpm test:e2e ui/tests/e2e/overnight-result-card.spec.tsagainst the running local stack (will run in CI; documented best-effort positive case per D-17).docs/08_guides/images/12-overnight-result-card.png) deferred — the CI demo seed does not exercise the fullfollow_suggestionscard state. Will be captured locally before merge OR tracked as achore_*follow-up. Prose lands as-is.🤖 Generated with Claude Code