feat(proposals): full parameter-space panel on /proposals/[id] (feat_proposal_full_param_space_view)#446
Conversation
…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>
…ram-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>
…ifted-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>
…dule (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>
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>
…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>
…-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>
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>
…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>
There was a problem hiding this comment.
Code Review
This pull request introduces the 'Proposal Full-Parameter-Space View' feature, adding a new component to the proposal detail page. This panel displays all parameters declared by a template, partitioned into three groups: tuned and changed, tuned but unchanged, and not in the search space. To support this, the extractFromTo and renderValue helpers were promoted to a shared module, and the useTemplate and useStudy queries were refactored to load unconditionally for all proposals. The review feedback highlights two key improvements: first, a critical fix to prevent a potential runtime TypeError by ensuring searchSpaceParams is truthy before using the in operator; second, a layout enhancement suggesting a CSS grid instead of a flex layout to align the columns in the tuned-changed rows for better readability.
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.
| const seen = new Set(Object.keys(configDiff)); | ||
| for (const [key, type] of Object.entries(declaredParams)) { | ||
| if (seen.has(key)) continue; // already in tunedChanged | ||
| if (searchSpaceParams !== undefined && key in searchSpaceParams) { |
There was a problem hiding this comment.
Using the in operator on searchSpaceParams when it is null will throw a TypeError: Cannot use 'in' operator to search for '...' in null. In JavaScript/TypeScript, null !== undefined evaluates to true, so the check searchSpaceParams !== undefined does not guard against null values. Since searchSpaceParams can be null at runtime (e.g., if the API returns null for params or if the search space is empty), we should use a truthiness check instead to prevent potential runtime crashes.
| if (searchSpaceParams !== undefined && key in searchSpaceParams) { | |
| if (searchSpaceParams && key in searchSpaceParams) { |
| function TunedChangedRows({ rows }: { rows: TunedChangedRow[] }) { | ||
| return ( | ||
| <ul className="mt-1 space-y-0.5"> | ||
| {rows.map((row) => ( | ||
| <li | ||
| key={row.name} | ||
| data-testid={`param-space-row-tuned_changed-${row.name}`} | ||
| className="flex items-center gap-2 font-mono text-xs text-gray-700" | ||
| > | ||
| <code>{row.name}</code> | ||
| <span className="text-muted-foreground">{row.type}</span> | ||
| <span>{renderValue(row.from)}</span> | ||
| <span aria-hidden="true">→</span> | ||
| <span>{renderValue(row.to)}</span> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ); | ||
| } |
There was a problem hiding this comment.
The TunedChangedRows component renders the parameter names, types, and from/to values using a simple flex layout with gap-2. Because parameter names and types have varying lengths, the "from" and "to" values will not align vertically across different rows. Using a CSS grid layout (similar to a table) would align these columns perfectly, improving readability and maintaining visual consistency with the <ConfigDiffPanel> component.
| function TunedChangedRows({ rows }: { rows: TunedChangedRow[] }) { | |
| return ( | |
| <ul className="mt-1 space-y-0.5"> | |
| {rows.map((row) => ( | |
| <li | |
| key={row.name} | |
| data-testid={`param-space-row-tuned_changed-${row.name}`} | |
| className="flex items-center gap-2 font-mono text-xs text-gray-700" | |
| > | |
| <code>{row.name}</code> | |
| <span className="text-muted-foreground">{row.type}</span> | |
| <span>{renderValue(row.from)}</span> | |
| <span aria-hidden="true">→</span> | |
| <span>{renderValue(row.to)}</span> | |
| </li> | |
| ))} | |
| </ul> | |
| ); | |
| } | |
| function TunedChangedRows({ rows }: { rows: TunedChangedRow[] }) { | |
| return ( | |
| <ul className="mt-1 space-y-1"> | |
| {rows.map((row) => ( | |
| <li | |
| key={row.name} | |
| data-testid={`param-space-row-tuned_changed-${row.name}`} | |
| className="grid grid-cols-[1.5fr_1fr_1fr_auto_1fr] gap-x-4 items-center font-mono text-xs text-gray-700" | |
| > | |
| <code>{row.name}</code> | |
| <span className="text-muted-foreground justify-self-start">{row.type}</span> | |
| <span className="justify-self-end">{renderValue(row.from)}</span> | |
| <span aria-hidden="true" className="text-muted-foreground text-center">→</span> | |
| <span className="justify-self-start">{renderValue(row.to)}</span> | |
| </li> | |
| ))} | |
| </ul> | |
| ); | |
| } |
…s (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>
Review adjudication (Gemini Code Assist)Commit landing fixes: Gemini Code Assist (2 findings)
Outcomes
All 17 CI checks were green on the pre-fix SHA; the fix commit re-triggers CI. Ready for human review + merge once green. |
…w 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>
… phase3 idea preflight (#447) * docs: finalize feat_proposal_full_param_space_view (PR #446 merged) - Move feature folder planned_features/02_mvp2 → implemented_features/2026_06_04_feat_proposal_full_param_space_view (flat, date-prefixed). - pipeline_status.md: Implementation Complete (PR #446, 3baea3f) + Release: mvp2 marker. - implementation_plan.md: Status → Complete. - state.md: new merge entry (drop #436 to keep last-5), branch/active-feature/in-flight refreshed. - state_history.md: full narrative entry prepended. - Dashboards + public roadmap regenerated (feature moves to the implemented column). No phase*_idea.md files (Caps 2+3 deferred without artifacts per D-14). No tracking issue to close. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * docs(idea): apply preflight edits to feat_overnight_final_solution_phase3/idea.md Lands audit-and-patch edits that were sitting uncommitted in the working tree from a prior /idea-preflight run on the phase3 idea: priority reclassified Backlog → P2 (operator-confirmed MVP2 scope), dependency marked satisfied (Phase 1 merged as PR #440), corrected the proposal-creation citation to backend/workers/orchestrator.py:693-740 (_stop, not _on_study_complete), and linkified sibling references. Dashboards regenerated to reflect the P2 reclassification. Unrelated to feat_proposal_full_param_space_view — committed at operator request to clear the working tree. 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
Adds a Full parameter space panel to the proposal detail page that shows every parameter the proposal's template declares, partitioned into three visually distinct groups so operators can see what the optimizer left on the table, not just what it changed:
config_diff, with the from→to delta.search_space.paramsbut not inconfig_diff(considered, not moved).Now that the overnight autopilot swaps templates and tunes different knobs automatically, the proposal page IS the morning artifact operators read; this panel makes that artifact self-explanatory.
Backend: none. Migration: none. Audit events: none (read-only UI). The feature derives everything client-side from data already on the page (
ProposalDetail.config_diff,ProposalDetail.template.id,StudyDetail.search_space.params,QueryTemplateDetail.declared_params).Stories
extractFromTo+renderValuefromconfig-diff-panel.tsxto a sharedui/src/lib/config-diff.ts(so both panels reuse one canonical normalizer;<ConfigDiffPanel>rendering byte-identical).partitionTemplateParams(ui/src/lib/proposal-param-space.ts) — the partition algorithm, fully unit-tested.<FullParamSpacePanel>component + newproposal.full_param_spaceglossary key.useTemplate(now sourced fromproposal.template.id) ANDuseStudy(drop thehasActionableFollowupgate) so the panel has its data for every proposal shape, with race-aware mount gating.Key decisions
declaredParams ∪ configDiff(D-9);searchSpace-only drift keys are silently dropped (no type-tag to show).config_diffmembership = "tuned" (D-10) — afrom === toanomaly still classifies as tuned, matching<ConfigDiffPanel>'s existing semantics.parentStudyto settle so thetunedUnchangedgroup never flashes a transient mis-classification.phase*_idea.mdartifacts (D-14).Test coverage
config-diff.test.ts(7) +proposal-param-space.test.ts(8) — partition universe, drift,from===to, sort stability, legacy 2-tuple.full-param-space-panel.test.tsx— three groups + counts, empty state, tooltip hover, visual states.tsc+next buildclean.Cross-model review: GPT-5.5 converged at spec (3 cycles) + plan (3 cycles) + phase-gate code review (4 findings accepted, 1 rejected with cited counter-evidence).
Note for reviewers
The FR-3 lift makes
useStudy/useTemplatefire for every study-backed proposal, so the existing 12 page tests now trigger those secondary fetches. They pass deterministically (the queries cancel on unmount before MSW'sonUnhandledRequest: 'error'fires; vitest does not fail on console.error). Verified stable across the full 1143-test suite — flagged here for transparency, not a regression.Test plan
pnpm test(1143 passed)pnpm typecheck+pnpm buildpnpm lint(0 errors)proposals.spec.ts(5 passed, real backend, rebuilt container)🤖 Generated with Claude Code