fix(ui): stop Phase 2 results surface from leaking engine output - #54
Conversation
Closes four orientation gaps in CLAUDE.md: a top-of-Architecture map that flags advisory/, experiments/, docs/, and tests/ground_truth/ as off-path; a Scripts-at-a-glance section covering scripts/ and apps/backend/scripts/; a pointer from the Phase 3 paragraph to SamplePlayback.tsx + sampleGenerationClient.ts; and a curl example plus allowlisted field paths on the csv_export.py module entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Oz <oz-agent@warp.dev>
Removes four classes of broken/placeholder content that made the Phase 2 recommendations read as engine output instead of producer-facing advice (audit finding #1). 1. Mix Chain card role text — `buildRoleSentence` used to fabricate "{stage phrase} by {verb}" by lowercasing the first letter of Gemini's `reason`. Because `reason` is a present-tense clause, every card produced ungrammatical splices like "Controls bass energy by ensures the extreme low-end mono…". Now renders the reason verbatim with a capitalized first letter and trailing period; HIGH-END cue suffix preserved as "(for …)". 2. Patch Framework `patchRole` — was a 7-key category-keyed fallback (`mapPatchRole`) that stamped duplicated placeholders ("Primary tone generator" on every SYNTHESIS card). Removed the field, the fallback, the JSX paragraph, and the contribution to the `inferProcessingGroup` text-concat. The category chip + per-card `whyThisWorks` already carry the bucket and explanation. 3. Patches/Mix Chain overlap — `buildPatchCards` now case-insensitively filters out devices that also appear in `mixAndMasterChain` so the Patches section stops re-listing chain devices. Synthetic fallbacks (Stereo Width, MIDI Clip Guide) are built from Phase 1 only and bypass the filter intentionally. 4. Interpretation Caution raw-JSON dump — the panel rendered `originalValue` verbatim, which for dropped recommendations is a JSON-dumped `AbletonRecommendation` (see `_stringify_warning_value` in `server_phase2.py`). Added `formatDroppedValue` helper that parses JSON-shaped values and renders a compact "device: X · parameter: Y · value: Z" summary. Non-JSON strings pass through; invalid JSON falls back to a truncated raw string. Also resolves the `$Saturator` symptom — Gemini's hallucinated `$`-prefixed device name now surfaces as a readable summary line instead of leaking through a raw JSON dump. 5. System Diagnostics dev-only leak — `validateNewFieldCoverage` emits "Phase 1 field 'X' is present… this warning is benign" coverage signals meant for the engine team, not producers. Added optional `audience?: 'dev' | 'user'` to `ValidationViolation`, marked `NEW_FIELD_UNCITED` as dev, and updated `Phase2ConsistencyReport` to filter dev-audience violations from the rendered table AND from the header counts (so the header doesn't read "5 warnings shown" above an empty table). The underlying `ValidationReport` still carries every violation for tests and offline analysis. Tripwire note: the backend grammar fix `_apply_phase2_grammar_fixes` in `server_phase2.py` only runs on the legacy `/api/phase2` endpoint, not the analysis-runs path the UI uses. After this change it becomes a redundant no-op against the new render shape; left untouched. Tests: 16 new unit cases across viewModel, UI rendering, validator, and consistency report. `npm run verify` passes (lint + 565 unit tests + build + 44 smoke tests). Live UI verified against saved run 855f1376… — all nine text-leak probes (`by ensures`, `by ducks`, `by generates`, `Primary tone generator`, `Texture and movement stage`, `Stereo placement stage`, raw `\$Saturator`, `this warning is benign`, "is present in the measurement payload but no Phase 2…") return false. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
slittycode
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Summary
Frontend-only fix that eliminates five categories of engine-output leakage from the Phase 2 surface: fabricated grammar in Mix Chain cards, duplicated category-keyed placeholder text in Patches, device overlap between the Mix Chain and Patches sections, raw JSON dumps in the Interpretation Caution panel, and dev-coverage signals surfacing in System Diagnostics. All new logic has test coverage. Phase 1 boundary is clean throughout.
Findings
Should fix
docs/LAYER2_EVALUATION.md (new file from commit f7ea7fc) reads as an operational guide but describes tooling that doesn't exist: apps/backend/scripts/import_midi_to_ground_truth.py is not present, evaluate_phase1.py has no --include-transcription flag, tests/fixtures/transcription_tracks/ doesn't exist, and phase1_eval_manifest.json has no transcriptionTracks key. Anyone following the workflow will hit dead ends immediately. Either add a clear "not yet implemented" callout at the top or hold the doc until the implementation lands.
Worth considering
ValidationReport.summary.warningCount stays as the raw count (including dev-audience violations), but Phase2ConsistencyReport now derives header text from the filtered userWarningCount. The PR notes this is intentional for test backward-compat, and the new tests explicitly verify the rendered output. Fine as-is, but a future test asserting report.summary.warningCount === N will pass while the UI renders N-M. Worth a comment on the summary shape noting counts include dev-audience violations.
formatDroppedValue(null) and formatDroppedValue(undefined) return '—' per the guard, but no test cases cover those inputs. Behavior is correct; just untested branches.
Test results
137 passed, 0 failed across the four PR-relevant test files (analysisResultsViewModel, analysisResultsUi, phase2Validator, phase2ConsistencyReport). Three pre-existing failures in decision_gate.*.live.test.ts — these require /tmp/decision_gate_<model>.json snapshots not seeded in this environment and fail identically on main; not caused by this PR.
Phase boundary check
Clean. Nothing in the diff reads from Phase 2 to re-derive or override a Phase 1 value. buildPatchCards deduplication filters Phase 2 recommendations against Phase 2 chain devices — both sides are Phase 2. formatDroppedValue is a pure string formatter for backend-emitted warning metadata. ValidationViolation.audience is an additive optional field that doesn't touch Phase 1 data.
Generated by Claude Code
slittycode
left a comment
There was a problem hiding this comment.
Verdict: APPROVE (self-review — GitHub prevents approve on own PRs)
Summary
Closes five display bugs where Phase 2 cards were leaking engine output — ungrammatical role splices, duplicated placeholder strings, raw JSON dumps, and dev-coverage signals routed to the producer surface. All changes are in the view-model and rendering layer; Phase 1 is untouched. CI (Backend + Frontend) green on head commit.
Findings
Worth considering:
buildRoleSentenceHIGH-END suffix: old form wasfor ${cue}, new form is(for ${cue}).appended to the sentence. When Gemini's reason already mentions the high-end focus, the suffix reads as redundant ("…in the top end (for hi-hats and synth sweeps)."). Not a bug — the test covers it — but watch for producer feedback.formatDroppedValuefallback (entries.slice(0, 2)) can expose non-headline keys when none ofdevice/parameter/value/category/name/fieldare present. For current backend output shapes this is unreachable, but a'(unknown device)'terminal fallback would be less surprising than arbitrary key exposure.
Test results
CI: Backend ✓ Frontend ✓ on a1218f01. Local vitest run tests/services: 616 pass, 1 skip. The 3 decision_gate.*.live.test.ts failures are pre-existing environment fixtures missing from this runner and are excluded from npm run verify's test:unit scope.
Phase boundary check
Clean. pickPhase1Value, pickPhase1Confidence, and all other Phase 1 reads are read-only traversals. No assignment to Phase 1 objects anywhere in the diff. ValidationViolation.audience is a new optional field on a validator type, not a Phase 1 schema field.
Generated by Claude Code
…esh (#57), nightly audit doc (#59) * audits: nightly 2026-05-16 — 5 issues (4 test failures + 1 review item) 3 vitest decision_gate.*.live.test.ts failures (missing /tmp snapshot gating), 1 Playwright upload-estimate-phase1.spec.ts timeout, and one mixDoctor.estimatePlr fallback flagged for human boundary review. No Phase 1 ground-truth mutations found. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(claude): add repo-layout, scripts map, Phase 3 UI entry, CSV recipe Closes four orientation gaps in CLAUDE.md: a top-of-Architecture map that flags advisory/, experiments/, docs/, and tests/ground_truth/ as off-path; a Scripts-at-a-glance section covering scripts/ and apps/backend/scripts/; a pointer from the Phase 3 paragraph to SamplePlayback.tsx + sampleGenerationClient.ts; and a curl example plus allowlisted field paths on the csv_export.py module entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add Layer 2 transcription evaluation guide Co-Authored-By: Oz <oz-agent@warp.dev> * fix(ui): stop Phase 2 results surface from leaking engine output Removes four classes of broken/placeholder content that made the Phase 2 recommendations read as engine output instead of producer-facing advice (audit finding #1). 1. Mix Chain card role text — `buildRoleSentence` used to fabricate "{stage phrase} by {verb}" by lowercasing the first letter of Gemini's `reason`. Because `reason` is a present-tense clause, every card produced ungrammatical splices like "Controls bass energy by ensures the extreme low-end mono…". Now renders the reason verbatim with a capitalized first letter and trailing period; HIGH-END cue suffix preserved as "(for …)". 2. Patch Framework `patchRole` — was a 7-key category-keyed fallback (`mapPatchRole`) that stamped duplicated placeholders ("Primary tone generator" on every SYNTHESIS card). Removed the field, the fallback, the JSX paragraph, and the contribution to the `inferProcessingGroup` text-concat. The category chip + per-card `whyThisWorks` already carry the bucket and explanation. 3. Patches/Mix Chain overlap — `buildPatchCards` now case-insensitively filters out devices that also appear in `mixAndMasterChain` so the Patches section stops re-listing chain devices. Synthetic fallbacks (Stereo Width, MIDI Clip Guide) are built from Phase 1 only and bypass the filter intentionally. 4. Interpretation Caution raw-JSON dump — the panel rendered `originalValue` verbatim, which for dropped recommendations is a JSON-dumped `AbletonRecommendation` (see `_stringify_warning_value` in `server_phase2.py`). Added `formatDroppedValue` helper that parses JSON-shaped values and renders a compact "device: X · parameter: Y · value: Z" summary. Non-JSON strings pass through; invalid JSON falls back to a truncated raw string. Also resolves the `$Saturator` symptom — Gemini's hallucinated `$`-prefixed device name now surfaces as a readable summary line instead of leaking through a raw JSON dump. 5. System Diagnostics dev-only leak — `validateNewFieldCoverage` emits "Phase 1 field 'X' is present… this warning is benign" coverage signals meant for the engine team, not producers. Added optional `audience?: 'dev' | 'user'` to `ValidationViolation`, marked `NEW_FIELD_UNCITED` as dev, and updated `Phase2ConsistencyReport` to filter dev-audience violations from the rendered table AND from the header counts (so the header doesn't read "5 warnings shown" above an empty table). The underlying `ValidationReport` still carries every violation for tests and offline analysis. Tripwire note: the backend grammar fix `_apply_phase2_grammar_fixes` in `server_phase2.py` only runs on the legacy `/api/phase2` endpoint, not the analysis-runs path the UI uses. After this change it becomes a redundant no-op against the new render shape; left untouched. Tests: 16 new unit cases across viewModel, UI rendering, validator, and consistency report. `npm run verify` passes (lint + 565 unit tests + build + 44 smoke tests). Live UI verified against saved run 855f1376… — all nine text-leak probes (`by ensures`, `by ducks`, `by generates`, `Primary tone generator`, `Texture and movement stage`, `Stereo placement stage`, raw `\$Saturator`, `this warning is benign`, "is present in the measurement payload but no Phase 2…") return false. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ui): surface primary citation in collapsed card headers Audit Finding #3 — "the chain-of-custody citation, the product's whole differentiator, renders as a footnote." Adds a one-line `CitationHeadline` primitive inside the collapsed header of every Mix Chain / Patch / Sonic Element card so producers see the measurement evidence at scan-time without expanding. The expanded CitationBlock stays in the body unchanged — this is the collapsed-state companion, not a replacement. Each headline reads `{label} {value} →` with the arrow pointing into the device h4/h3 that follows in the title row, making the implicit "measurement justified device" statement visible — exactly the audit's literal example "Crest factor 8.2 dB → Glue Compressor." Confidence sibling pills (Solid / Workable / Rough / Unreliable, same ladder as ConfidenceBandBadge) ride alongside the value when the primary field has a paired *Confidence sibling. This preserves the chain-of-custody invariant: low-confidence measurements visibly hedge in the collapsed view too, not just after expansion. Implementation: 1. `CitationHeadline` added as a sibling export in `apps/ui/src/components/CitationBlock.tsx`. Shares the file's imports, the `CONFIDENCE_PILL_CLASSES` map, and the audit lineage. Composes existing pure helpers (`pickPhase1Value`, `formatCitedValue`, `humanizeFieldPath`, `pickPhase1Confidence`, `getConfidenceBand`, `formatBandPillLabel`) — no new resolver logic. 2. Mount points in `AnalysisResults.tsx`: - Mix Chain card header (~line 2056): between the title row and the role paragraph. - Patch card header (~line 2215): between the title row and the MetaBadgeList. - Sonic Element card header (~line 1912): between the title row and the summary paragraph. 3. Each mount is guarded by `card.phase1Fields.length > 0`. Cards without cited fields fall back to today's exact layout. Live verification against saved run 855f1376… (the audit reproducer): all 10 Mix Chain cards, 2 Patch cards, and 7 Sonic Element cards render the headline correctly with appropriate confidence pills (PUMPING STRENGTH 47% [ROUGH SKETCH 30%], SUPERSAW DETECTED [WORKABLE DRAFT 78%], ACID BASS DETECTED [SOLID SCAFFOLD 99%], NOTE TRANSCRIPTION 61% [WORKABLE DRAFT 61%], KEY C Minor [SOLID SCAFFOLD 93%], TEMPO 145 BPM [SOLID SCAFFOLD 100%]). Mobile (375px) verified — headlines fit, pills stay aligned, no overflow. Tests: 15 new cases — 10 in `tests/services/citationHeadline.test.ts` (mirrors `citationBlock.test.ts`) covering label/value/arrow render, null fallback, confidence-pill bands, value formatter parity, typography hooks. 5 in `analysisResultsUi.test.ts` covering Mix Chain / Patch / Sonic integration, empty-fields fallback, and the low-confidence Unreliable-band path. `npm run verify` passes: lint + 575 unit tests + build + 44 smoke. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ui): unify five confidence vocabularies onto the canonical band ladder Audit Finding #4 — the producer used to see five competing vocabularies for "how much should I trust this number?" — HIGH/MED/LOW pills on Detected Characteristics cards, High/Moderate/Low chips on Confidence Notes, bare CONF X% text on Key/Character metric cards, SCORE X.XX badges on the BPM card (two render paths), and the canonical four-band ladder used only by the Session Musician panel. The chain-of-custody invariant in PURPOSE.md requires low-confidence measurements to produce visibly hedged advice — five vocabularies fragment that signal so the producer can't build a single mental model of trust. This PR promotes the existing four-band ladder (apps/ui/src/services/sessionMusician/confidenceBand.ts) to the canonical primitive across every confidence surface. Frontend-only; Gemini still emits HIGH/MED/LOW and the UI converts locally via a new `toConfidenceBand` normalizer. What changed: 1. New normalizer `toConfidenceBand(value)` in `confidenceBand.ts` accepts numeric 0-1 floats, 0-100 integers, Gemini string enums (HIGH/MED/LOW + High/Moderate/Low variants), and percent strings ("62%"). Returns the matching `ConfidenceBand` or null for unparseable input. The string enums map to band-mid values (HIGH→0.9, MED→0.6, LOW→0.3) so `formatBandPillLabel` reads as honest hedges. 2. `ConfidenceBandBadge` gains: - `variant: 'full' | 'compact'` — `compact` omits the copy paragraph so the pill can sit inline in card corners and metric-card footers. `full` (default) preserves the Session Musician panel behavior unchanged. - `band?: ConfidenceBand` override prop — when supplied, skips the `getConfidenceBand` round-trip. Useful when the caller pre-converts a string enum. 3. V1: Detected Characteristics cards (AnalysisResults.tsx ~1650) — bespoke HIGH/MED/LOW ternary pill replaced with `<ConfidenceBandBadge variant="compact" band={toConfidenceBand(item.confidence)} />`. `characteristicPillClass` helper kept alive only for the unrelated characteristic-name chips on the Character metric card (line 1000); flagged as a follow-up. 4. V2: Confidence Notes chips (AnalysisResults.tsx ~1187) — `toConfidenceBadges` viewModel return shape changed from `{ label, level }` (3-level legacy enum) to `{ label, band }` (canonical four-band ladder). `confidenceClass` helper deleted. 5. V3: Plain `CONF X%` text (3 sites) — replaced with `<ConfidenceBandBadge variant="compact" confidence={...} />` in: - AnalysisResults.tsx Key card footer - AnalysisResults.tsx Character card footer - MeasurementDashboard.tsx alternate Key card 6. V4: `SCORE X.XX` badges (2 sites) — replaced with band pills in AnalysisResults.tsx Tempo card and MeasurementDashboard.tsx Tempo card. `formatBpmScore` deleted from both files. Cross-Check ✓/✗ StatusBadge in MeasurementDashboard preserved — that's an agreement signal (do multiple BPM detectors agree?), not confidence. 7. Dead code retired: `normalizeConfidenceLevel`, `parseConfidenceScalar`, `confidenceClass`, two copies of `formatBpmScore`. `ConfidenceLevel` type and `MelodyInsightsViewModel.confidenceLabel` kept alive — they drive non-pill key/value text rows in Sonic Element melody insights (flagged for follow-up migration). 3-level → 4-band mismatch documented as an intentional refinement: the old `normalizeConfidenceLevel` mapped scalar 0.5-0.79 AND "medium" both to "Moderate"; the new normalizer maps "medium" to 0.6 → workable (consistent), and scalar 0.25-0.49 to "Rough sketch" (more granular than the old "Low" bucket). Tests: 20 new cases across 4 files — 1. `confidenceBand.test.ts` — 9 cases for `toConfidenceBand` (null/undefined fallback, 0-1 floats, 0-100 integers, all string enum variants, percent strings, NaN/Infinity, negative clamping). 2. `confidenceBandBadge.test.ts` — 4 cases for the compact variant and the `band` override prop (label-only when no confidence, tone override, Gemini-string routing). 3. `analysisResultsViewModel.test.ts` — rewrote the existing `toConfidenceBadges` shape assertion; added a `band: null` fallback case. 4. `analysisResultsUi.test.ts` — 5 integration cases proving each V1-V4 site renders band pill text and not the old vocabularies. Live UI verified against saved run 855f1376… — 36 band pills visible across the page (21 Solid scaffold, 7 Workable draft, 5 Rough sketch, 3 Unreliable). Zero `CONF X%` leakage. Detected Characteristics cards show the chain-of-custody hedge working as designed (Detuned Supersaw renders WORKABLE DRAFT in orange while the other 4 detections render SOLID SCAFFOLD in green). Cross-Check ✓/✗ structurally preserved (the saved run doesn't populate bpmAgreement, so it doesn't render, but the code path is untouched). `npm run verify` passes: lint + ~600 unit + build + 44 smoke. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: refresh stale items against current architecture CHANGELOG.md "Unreleased" was missing nine shipped features (Phase 3 audition samples, URL ingest, admin DELETE bypass, source-audio route, CSV export, publicStatus, reassigned spectrogram, chain-of-custody audit overhaul, BatchedBandpass centralization, hosted runtime foundation, backend monolith split). Added them. CLAUDE.md's analyze.py CLI example only listed four flags; expanded to match the full surface (--standard, --pitch-note-only, --stem-dir, --stem-output-dir, --pitch-note-backend). Expanded the Frontend key service files list from 9 to 16 entries to cover the services that landed during the audit overhaul (httpClient, sampleGenerationClient, appliedRecommendations + userLabels, phase1Picker + phaseLabels, audioFile, fieldAnalytics + diagnosticLogs, sessionMusician helpers). apps/ui/AGENTS.md File Map likewise expanded from 4 to 14 service entries against the same list. docs/SAMPLE_GENERATION.md's "Snapshot integration" section claimed an `AnalysisRunSnapshot.stages.sampleGeneration` shape that does not exist in apps/ui/src/types/backend.ts — the snapshot tracks only measurement, pitchNoteTranslation, and interpretation. Replaced the stale TS block with a description that matches the actual on-demand artifact path (consistent with the doc's own line 47). apps/backend/AGENTS.md and apps/backend/ARCHITECTURE.md now flag symbolic_extract.py as orphaned and broken — it imports a removed BasicPitchBackend symbol from analyze.py, would raise ImportError at load, and is not referenced from any other module. Slated for removal. docs/history/README.md had `library-review-torchfx-2026-05-13.md` listed twice; removed the duplicate. docs/ARCHITECTURE_STRATEGY.md "Last updated" stamp bumped to May 2026 with a note that no shifts to the three-layer thesis or library decisions accompanied the recent feature work. Date stamps in apps/backend/AGENTS.md and apps/ui/AGENTS.md refreshed to 2026-05-17. https://claude.ai/code/session_01QEmFAUJSoRgTqPcFTfcnYW * fix(ui): audit quick-hits bundle (StickyNav, Mix Chain, Mix Doctor, header) Closes six items from the audit's "Quick hits (low ROI, real)" list. Each is independent; bundled into one PR because they're all small surgical fixes. 1. QH1 — StickyNav clipped "MEASUREMENTS" at 1440px. Was overflow-x-auto + min-w-max, which produced an invisible horizontal scroll (macOS hides scrollbars by default; last pill looked clipped with no scroll affordance). Switched to flex-wrap so pills flow onto multiple rows at narrow widths; whitespace-nowrap prevents intra- label wrap. 2. QH2 — Mix Chain card-number order badges removed. Cards are grouped by processing stage AFTER ordering, so the order numbers appeared out-of-sequence within each group ("1, 6, 8, 9 / 2, 4 / 5, 7 / 3 / 10"), which read as a presentation bug. The visual sequence within each group is already meaningful; the badge added confusion without information. Dropped. 3. QH4 — Mix Doctor Band Diagnostics table now shows the target range alongside the optimal. Previously the column rendered only the optimal dB (e.g. "-22.0"), so two bands with similar Delta dB could land in different Issue buckets without the producer being able to see why. The Issue is determined by absolute thresholds (target.minDb / maxDb), not by the diff from optimal. Showing the range makes the verdict legible at a glance — Sub Bass -6.4 norm, range -16 to -8, exceeds the upper bound by 1.6 dB → TOO-LOUD; Low Mids -14.5 norm, range -26 to -14, sits at the boundary → OPTIMAL. Threshold logic itself was correct; this is a display fix. 4. QH5 — "Dense DAW Lab" link removed from the header. A prior audit pass had de-emphasized it to muted text, but the new audit re- flagged it for sitting in the primary flow's header without context for what it is. Route stays accessible via direct URL (the getAppViewHref helper is preserved for future re-introduction behind a settings menu, and main.tsx still routes to the DenseDawConcept component when activeView === 'daw-concept'). 5. QH6 — header model selector visually demoted. Dropped the "Interpretation Model" label and shrank the styling from a bordered dropdown to a discreet text-secondary inline select. The audit flagged the prior styling as foregrounding an AI-model choice an intermediate producer has no basis to make. Selector stays accessible (smoke spec requires it visible at desktop viewport; `responsive-layout.spec.ts` and `upload-estimate-phase1.spec.ts` both guard the `phase2-model-desktop` testid) but now reads as background metadata. Aria-label + title attribute carry the long- form context for assistive tech and hover discoverability. 6. QH7 — Mix Doctor section title renamed from "MixDoctor" to "Mix Doctor". The audit flagged that the section header rendered as "Mixdoctor" with a lowercase 'd', inconsistent with every other section name. Root cause: `formatWord` in `utils/displayText.ts` case-normalizes single-token CamelCase to sentence case, so "MixDoctor" was rendering as "Mixdoctor". Two words ("Mix Doctor") matches every other section name's pattern (Style Profile, Project Setup, Track Layout, Mix & Master Chain, Patch Framework, etc.) and renders cleanly through the existing pipeline. `npm run verify` passes: lint + ~600 unit + build + 44 smoke. Live UI verified against saved run 855f1376… with probes confirming all six fixes: - StickyNav.overflow-x-auto wrapper gone (pills wrap to 2 rows) - Zero Mix Chain order badges in #section-mix-chain - "Target (range)" header present, "Target dB" header gone - "Dense DAW Lab" text absent from page - "Interpretation Model" label absent from page - "Mix Doctor" section title present, "Mixdoctor" absent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Oz <oz-agent@warp.dev>
# Conflicts: # apps/ui/src/components/AnalysisResults.tsx
Summary
Removes four classes of broken/placeholder content from the Phase 2 results surface (audit finding #1 — the highest credibility lever in the recent UI audit). Every leak listed below was making Phase 2 cards read as engine output (broken grammar, repeated placeholders, raw JSON) instead of producer-facing recommendations. Frontend-only; eight files touched.
What changed
Mix Chain "by ensures" grammar —
buildRoleSentenceused to fabricate{stage phrase} by {verb}by lowercasing the first letter of Gemini'sreason. Becausereasonis a present-tense clause, every card printed splices like "Controls bass energy by ensures the extreme low-end mono…". Now renders the reason verbatim with a capitalized first letter and trailing period; HIGH-END cue suffix preserved as(for …).Patches
patchRoleplaceholders —mapPatchRolewas a 7-key category-keyed fallback that stamped duplicated strings ("Primary tone generator" on every SYNTHESIS card). Removed the field fromPatchCardViewModel, the fallback function, the three assignment sites, the<p>paragraph inAnalysisResults.tsx, and the contribution to theinferProcessingGrouptext-concat inbuildPatchGroups. The category chip and per-cardwhyThisWorksalready carry the bucket and the actionable explanation.Patches/Mix Chain overlap —
buildPatchCardsnow case-insensitively filters out devices that also appear inmixAndMasterChainso the Patches section stops re-listing chain devices (the audit captured Glue Compressor, Auto Filter, Wavetable, Utility, Limiter showing up in both). Synthetic fallbacks (Stereo Width, MIDI Clip Guide) are built from Phase 1 only and bypass the filter intentionally.Interpretation Caution raw-JSON dump — the panel rendered
originalValueverbatim, which for dropped recommendations is a JSON-dumpedAbletonRecommendation(see_stringify_warning_valueinserver_phase2.py). AddedformatDroppedValuehelper that parses JSON-shaped values and renders compactdevice: X · parameter: Y · value: Zsummaries (3-key cap, ~80 char ceiling). Non-JSON strings pass through; invalid JSON falls back to a truncated raw string. This also resolves the\$Saturatorsymptom — Gemini's hallucinated\$-prefixed device name now surfaces as a readable summary line instead of leaking through a raw JSON dump.System Diagnostics "field not cited" leak —
validateNewFieldCoverageemits coverage signals ("Phase 1 field 'X' is present… this warning is benign") that are for the engine team, not producers. Added optionalaudience?: 'dev' | 'user'toValidationViolation, taggedNEW_FIELD_UNCITEDasdev, and updatedPhase2ConsistencyReportto filter dev-audience violations from both the rendered table AND the header counts (so the header doesn't read "5 warnings shown" above an empty table). The underlyingValidationReportstill carries every violation for tests and offline analysis.Why
Per
PURPOSE.md, the chain-of-custody from Phase 1 measurement to Phase 2 recommendation is the product. A producer who sees "by ensures the extreme low-end" on a Mix Chain card, "Primary tone generator" five times in Patches, raw{"device":"\$Saturator",…}JSON inside the Caution panel, or 15+ "this warning is benign" lines in System Diagnostics concludes the tool is broken — and no amount of DSP rigor recovers that first impression. None of these leaks were Gemini's fault; every one traced to a view-model that prepended decorative wrappers, fabricated category-keyed boilerplate, rendered JSON-dumped originals, or routed dev-coverage signals to the user surface.Test plan
cd apps/ui && npm run lint— TypeScript clean (catchesPatchCardViewModel.patchRoleremoval).npx vitest run tests/services/analysisResultsViewModel.test.ts— 8 new test cases (3 buildRoleSentence + 2 patchRole removal + 3 Mix Chain overlap dedup).npx vitest run tests/services/analysisResultsUi.test.ts— 3 new test cases forformatDroppedValue+ updated assertion for Mix Chain role text.npx vitest run tests/services/phase2Validator.test.ts— 2 new test cases foraudienceflag.npx vitest run tests/services/phase2ConsistencyReport.test.ts— 2 new test cases for dev-audience suppression.npm run verify— full gate green: lint + 565 unit tests (47 files) + production build + 44 Playwright smoke (+2 skipped: live Gemini, live backend).855f1376-ec1e-4e3e-b145-bbf6eb479296(the audit reproducer) via Playwright intercept of the create-run POST. All nine text-leak probes returnfalse:by ensures,by ducks,by generates,Primary tone generator,Texture and movement stage,Stereo placement stage, raw\$Saturator,this warning is benign, "is present in the measurement payload but no Phase 2…".Reviewer notes
PatchCardViewModel.patchRoleremoval is TS-breaking.npm run lintcatches every consumer; the six call sites in the original tree are all updated. New consumers added in parallel branches will fail typecheck — that's intentional.buildPatchGroupsheuristic.card.patchRolepreviously contributed keywords to the text fed toinferProcessingGroup. The new'buildPatchGroups still routes a synth card into SYNTH / MELODIC after dropping patchRole'test guards the routing regression risk;device + category + whyThisWorkskeeps the keyword surface rich._apply_phase2_grammar_fixesinteraction. That function inserver_phase2.pyonly runs on the legacy/api/phase2route, not the analysis-runs path the UI uses today. After this change it becomes a redundant no-op against the new render shape (the UI no longer prepends a prefix to strip). Left untouched to avoid scope creep.summary.warningCountvs visible rows in the consistency report. Dev violations remain insummary.warningCountso any test that asserts on a specific summary count keeps passing. The header text is now computed fromuserVisibleso the rendered "N error(s), N warning(s)" reflects what's actually in the table.🤖 Generated with Claude Code