Skip to content

feat(studies): morning summary card + strategy line (feat_overnight_final_solution_phase2)#442

Merged
SoundMindsAI merged 9 commits into
mainfrom
feat_overnight_final_solution_phase2
Jun 4, 2026
Merged

feat(studies): morning summary card + strategy line (feat_overnight_final_solution_phase2)#442
SoundMindsAI merged 9 commits into
mainfrom
feat_overnight_final_solution_phase2

Conversation

@SoundMindsAI
Copy link
Copy Markdown
Owner

Summary

  • Adds <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.
  • Adds a read-only "Strategy: …" line inside <LinkedEntitiesRow> for every study whose config.auto_followup_strategy is set, so mid-chain operators see the strategy at a glance without scrolling to the chain panel.
  • Extracts CHAIN_STOP_REASON_PHRASE + formatSignedLift from 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 at 0022_solr_engine_auth_check. Six stories merged in a single PR:

  1. Foundation: extract shared helpers + add overnight_result / auto_followup_strategy_line glossary keys (FR-6 / FR-8).
  2. Pure-domain pathTokenForLink helper with Record<SelectedFollowupKind, …> exhaustiveness (FR-3).
  3. <OvernightResultCard> shell + headline + path + best-config + stop-reason + narrative + truncateNarrative + shouldShowOvernightResultCard (FR-1 / FR-3 / FR-5 / FR-7).
  4. <WinningLinkConvergenceChip> + id="digest" anchor on <DigestPanel> (FR-4 / FR-5 anchor).
  5. <StrategyLine> inside <LinkedEntitiesRow> (FR-2).
  6. Tutorial guide + ui-architecture doc + real-backend E2E spec (FR-9).

Cycle-2 spec restructure: narrative section moved into Story 3 (not Story 4) so digestQ is consumed where it's declared, avoiding an unused-variable lint failure under the invariant hook-order rule (D-19).

Test coverage

Layer File Cases
Vitest unit ui/src/__tests__/lib/chain-stop-reason.test.ts 3
Vitest unit ui/src/__tests__/lib/format-lift.test.ts 5
Vitest unit ui/src/__tests__/lib/chain-path-tokens.test.ts 7
Vitest unit (extends) ui/src/__tests__/lib/glossary.test.ts +3 lock cases
Vitest component ui/src/__tests__/components/studies/overnight-result-card.test.tsx 25 (incl. direct unit tests for shouldShowOvernightResultCard + truncateNarrative)
Vitest component (extends) ui/src/__tests__/components/studies/linked-entities-row.test.tsx +5 strategy-line cases
Playwright E2E ui/tests/e2e/overnight-result-card.spec.ts 2 (negative predicate + positive predicate; AC-12 click-through downgraded per D-17)

Full suite: 1102 / 1102 vitest tests green; pnpm lint 0 errors; pnpm typecheck clean; pnpm build succeeds.

Test plan

  • cd ui && pnpm lint && pnpm typecheck && pnpm test && pnpm build all green (run pre-push).
  • Direct unit tests for shouldShowOvernightResultCard and truncateNarrative (FR-7 / FR-5 / D-15 cycle-2 C2-6).
  • Existing <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.ts against the running local stack (will run in CI; documented best-effort positive case per D-17).
  • Tutorial-guide screenshot (docs/08_guides/images/12-overnight-result-card.png) deferred — the CI demo seed does not exercise the full follow_suggestions card state. Will be captured locally before merge OR tracked as a chore_* follow-up. Prose lands as-is.

🤖 Generated with Claude Code

SoundMindsAI and others added 7 commits June 4, 2026 00:01
…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>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +123 to +128
<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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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>
@SoundMindsAI
Copy link
Copy Markdown
Owner Author

Review adjudication

Commits landing fixes: b1fda0e300302bcc85558d2029c906d251b57f6b

Gemini Code Assist (1 finding)

# Sev Location Verdict Notes
1 Medium ui/src/components/studies/linked-entities-row.tsx:128 Accepted Fixed in b1fda0e300302bcc85558d2029c906d251b57f6b<StrategyLine> now uses inline-flex items-center gap-1 on the outer span; the value lives in its own <span> and InfoTooltip is a direct child (removed the wrapper). Tests updated to \s* instead of \s+ because CSS gap replaces the literal whitespace text node between label and value.

Outcomes

  • Applied fixes (1): linked-entities-row StrategyLine markup cleanup.
  • Rejected (0):
  • Deferred (0):

Pre-push gate green: pnpm lint 0 errors, pnpm typecheck clean, pnpm test 1102/1102 (incl. all 8 updated linked-entities-row cases), pnpm build succeeds.

…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>
@SoundMindsAI
Copy link
Copy Markdown
Owner Author

GPT-5.5 final review adjudication

Commits landing fixes: aad057939f4127d101f0b3fdfdda00ca8ad13430

GPT-5.5 final review (2 findings)

# Sev Location Verdict Notes
1 Blocking tutorial-first-study.md:484 (FR-9 screenshot) Deferred (chore filed) Per the plan's explicit hard-fallback escape hatch (Story 6 Tasks #3): 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 documenting the manual-capture path + optional seed-chain extension. Prose lands in this PR; screenshot tracked as the follow-up.
2 Follow-up-in-PR overnight-result-card.test.tsx (AC-11) Accepted Fixed in aad057939f4127d101f0b3fdfdda00ca8ad13430. The original AC-11 test only verified the hook mock was called (tautological). Strengthened to assert: (a) the hook is called with chain.best_link_id = 'link-c' (the cache-key wiring dedup depends on), and (b) enabled: true when the predicate fires. Added a complementary AC-11 negative test asserting ?? undefined coercion + enabled: false when no winner exists (D-22).

Outcomes

  • Applied fixes (1): AC-11 cache-dedup test strengthened.
  • Rejected (0):
  • Deferred (1): FR-9 screenshot — tracked as chore_overnight_result_card_screenshot per the plan's explicit escape hatch.

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.

@SoundMindsAI SoundMindsAI merged commit 0c4e035 into main Jun 4, 2026
18 checks passed
@SoundMindsAI SoundMindsAI deleted the feat_overnight_final_solution_phase2 branch June 4, 2026 08:29
SoundMindsAI added a commit that referenced this pull request Jun 4, 2026
…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>
SoundMindsAI added a commit that referenced this pull request Jun 4, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant