Skip to content

feat: value-first backport — genre profiles, MixDoctor, broken-pipe fixes - #1

Merged
slittycode merged 11 commits into
mainfrom
claude/elegant-swanson
Mar 19, 2026
Merged

feat: value-first backport — genre profiles, MixDoctor, broken-pipe fixes#1
slittycode merged 11 commits into
mainfrom
claude/elegant-swanson

Conversation

@slittycode

Copy link
Copy Markdown
Owner

Summary

  • Phase A: Fix two broken data pipes — wire dynamicCharacter through server.py so Gemini receives the kick/compressor values it's told to cite; remove the citation instruction that contradicted the STRING-typed response schema
  • Phase B: Add 4 last-mile UI cards for computed-but-invisible data — Genre Classification, Sidechain, Synthesis Character detectors + 6-band Spectral Balance visualization
  • Phase C: Harden genre classifier with abstention logic (empty/sparse input → None, weak scores → None, ambiguous → capped confidence)
  • Phase D: Port all 35 genre profiles from sonic-architect-app + deterministic MixDoctor scoring engine (spectral/dynamics/loudness/stereo evaluation, 0-100 health score) with auto-profile selection UI and export integration

Test plan

  • 131/131 frontend unit tests pass
  • 139/139 backend unit tests pass (including 4 new genre hardening tests)
  • TypeScript type check clean (tsc --noEmit)
  • Smoke tests (npm run test:smoke)
  • Full stack manual test (./scripts/dev.sh → upload audio → verify all new cards render)

🤖 Generated with Claude Code

slittycode and others added 11 commits March 18, 2026 14:09
The file declared its own PHASE2_PROMPT_TEMPLATE, PHASE2_RESPONSE_SCHEMA,
retry logic, and analyze_phase2() — all superseded when Phase 2 moved
server-side in v2.0.0. server.py loads its own prompt from
prompts/phase2_system.txt and manages its own retry logic.

Verified zero imports across the entire repo (Python, tests, docs,
configs) before deleting.

Backend test baseline (post-deletion, warm cache): 129 tests, 128 pass,
1 skipped. UI vitest baseline: 131 tests across 18 files, all pass.

Note: the repo has pre-existing uncommitted work (analyze.py,
JSON_SCHEMA.md, test_analyze.py, types.ts) from in-progress feature
development that is not part of this commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port acidDetail, reverbDetail, vocalDetail, supersawDetail, bassDetail,
and kickDetail from sonic-architect-app reference implementations into
analyze.py. Each detector is wired through server.py HTTP forwarding,
typed in apps/ui/src/types.ts, documented in JSON_SCHEMA.md, and tested
with synthetic fixtures covering bounds, shape, positive/negative cases.

56 analyze tests + 51 server tests + 130 frontend tests all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check off 6 deliverables that have been present since v1.2.0:
- scripts/calibrate_confidence.py
- apps/ui/src/services/phase2Validator.ts
- apps/ui/src/services/fieldAnalytics.ts
- apps/backend/analyze_fast.py
- docs/confidence_calibration_results.md
- docs/field_utilization_report.md

Remaining unchecked (not yet in repo):
- apps/ui/src/components/Phase2ConsistencyReport.tsx
- tests/ground_truth/README.md

Strike through the two HOLD gates (Implementation Order and
Dependencies sections) — the Codex DSP preflight reframe validation
has landed and is superseded by ARCHITECTURE_STRATEGY.md.

No restructuring; only status updated to match repo reality.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…harness

- analyze_reverb_detail: replace hardcoded fallback values (rt60: 0.3/0.5,
  tailEnergyRatio: 0.1/0.2) with rt60: null + measured: false when the
  detector cannot compute real values (short signal / too few transients /
  no RT60 estimates). Avoids misleading downstream consumers with invented numbers.
- Add tests/test_transcription_backends.py: synthetic-audio evaluation harness
  for BasicPitchBackend and TorchcrepeBackend (conditionally skipped if
  torchcrepe is not installed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase B2 (display-component UI type split) is done:
- App.tsx state split into measurementResult + symbolicResult
- AnalysisResults, SessionMusicianPanel, analysisResultsViewModel all
  updated to MeasurementResult + explicit symbolic: TranscriptionDetail | null
- Invariant #4 updated: display components now receive MeasurementResult directly
- All 131 unit tests pass; 40/40 smoke tests pass (2 live skipped)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…al, supersaw, bass, kick)

- reverbDetail: replace fake hardcoded fallbacks (0.3s/0.5s RT60) with
  honest uncertainty schema: rt60=null, tailEnergyRatio=null, measured=false
- server.py: forward all six detectors through _build_phase1()
- types.ts: add typed Phase1Result interfaces for all six detectors
- backendPhase1Client.ts: add parsePhase1Result() parsers for all six detectors
  with parseGrooveType helper; add null-handling test coverage
- fieldAnalytics.ts: register 17 new field paths for Phase 2 citation tracking
- test_analyze.py: update ReverbDetailTests for new nullable schema and
  measured field; add genreDetail to EXPECTED_TOP_LEVEL_KEYS; add import os
  (removed after hook converted setUp to use Path); fix GenreDetailTests
- JSON_SCHEMA.md: add all six detectors + genreDetail to forwarded-fields
  list (was 17, now 24 sections)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renders acid, reverb, vocal, supersaw, bass, and kick detector data
as a new "Detector Analysis" section (Phase 1 badge). Cards are
conditionally shown — section hidden when no detector fields are present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ixes

Phase A: Fix broken data pipes
- Wire dynamicCharacter through server.py → types → client parser so
  Gemini receives logAttackTime/attackTimeStdDev it's told to cite
- Remove citation instruction block from phase2_system.txt that
  contradicted the STRING-typed PHASE2_RESPONSE_SCHEMA

Phase B: Complete last-mile UI for computed-but-invisible data
- Parse genreDetail in backendPhase1Client (was typed but never parsed)
- Add Genre Classification, Sidechain, Synthesis Character detector cards
- Add 6-band Spectral Balance visualization in Phase 1 results

Phase C: Genre detail hardening
- Abstain when fewer than 3 of 7 core features have real values
- Return None when primary_score < 0.25
- Cap confidence at 0.4 when top two genres are within 0.05 score gap
- Add tests for empty input, sparse input, 3-feature threshold, ambiguity

Phase D: Backport genreProfiles + MixDoctor
- Port all 35 genre profiles from sonic-architect-app as JSON
- Deterministic MixDoctor scoring engine (spectral, dynamics, loudness,
  stereo evaluation against genre targets, 0-100 health score)
- MixDoctorPanel UI with auto-profile selection from genreDetail
- Integrate MixDoctor report into Markdown/JSON export

Also fixes stale 20MiB → 100MiB doc reference in CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add MixDoctor scoring engine (spectral balance, dynamics, PLR, loudness, stereo vs genre targets)
- Add MixDoctor panel with profile selector, delta chart, diagnostic cards, band-issue details
- Add genre profiles data for 35 genres with spectral/dynamics/PLR/loudness targets
- Add genre classification, sidechain detection, synthesis character detector cards in UI
- Add spectral balance six-band visualization in Phase 1 results
- Add MixDoctor section to markdown and JSON exports; dynamicCharacter in markdown export
- Harden genreDetail parsing and typing in frontend client
- Add dense techno boundary regression test (145 BPM)
- Align synthesis character labels to three-tier thresholds (clean subtractive / FM-acid / wavetable-noise)
- Add CODEX.md and PURPOSE.md project documentation files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@slittycode
slittycode merged commit d870a7b into main Mar 19, 2026
0 of 2 checks passed
slittycode added a commit that referenced this pull request May 13, 2026
PR #22 shipped a four-band confidence ladder (Solid scaffold / Workable draft
/ Rough sketch / Unreliable) but the PILL_CLASSES map collapsed them into
only two tones — accent for solid + workable, warning for rough + unreliable.
A producer couldn't visually distinguish Solid from Workable, or Rough from
Unreliable, at a glance.

Map the four bands to a green → orange → amber → red traffic-light ladder so
the visual hierarchy matches the producer's mental model ("best to worst at
a glance"):

  Solid scaffold   (≥80%)  → success (#00ff9d green)
  Workable draft   (50-79%) → accent  (#ff8800 orange)
  Rough sketch     (25-49%) → warning (#ffb800 amber)
  Unreliable       (<25%)  → error   (#ff3333 red)

All four CSS variables already exist in apps/ui/src/index.css and are the
canonical severity vocabulary across MeasurementPrimitives, DiagnosticLog,
and FileUpload (success = ready, accent = active, warning = caution,
error = critical). No theme additions; no new shades.

Opacity tuning unified at /30 border, /10 bg, text-{color} to match
MeasurementPrimitives.BADGE_TONE_CLASSES. Color carries the differentiation
now — the previous per-band opacity wobble didn't communicate anything.

NoteDraftBlock's overrideTone="rough" path (used for full-mix-fallback and
legacy render states) now renders amber, which is the right tone for "treat
with caution, not unreliable junk." No wiring change needed.

Tests: new tests/services/confidenceBandBadge.test.ts renders all four
confidence values and asserts each emits the expected color tokens. A
pairwise-distinct assertion survives future opacity tweaks. Two override-path
tests lock in the full-mix-fallback / legacy visual (high-confidence input
with overrideTone="rough" produces warning tokens, not green; low-confidence
input stays warning, not red). No existing tests touched — confidenceBand
asserts id/label/percent (not classes); panel + smoke tests assert on text.

Targets PR #22 (claude/thirsty-easley-faeaea) as the base. Plan in
~/.claude/plans/re-evaluate-asa-s-session-musician-sharded-balloon.md
("Follow-up plan #1" section).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
slittycode added a commit that referenced this pull request May 13, 2026
…27)

PR #22 shipped a four-band confidence ladder (Solid scaffold / Workable draft
/ Rough sketch / Unreliable) but the PILL_CLASSES map collapsed them into
only two tones — accent for solid + workable, warning for rough + unreliable.
A producer couldn't visually distinguish Solid from Workable, or Rough from
Unreliable, at a glance.

Map the four bands to a green → orange → amber → red traffic-light ladder so
the visual hierarchy matches the producer's mental model ("best to worst at
a glance"):

  Solid scaffold   (≥80%)  → success (#00ff9d green)
  Workable draft   (50-79%) → accent  (#ff8800 orange)
  Rough sketch     (25-49%) → warning (#ffb800 amber)
  Unreliable       (<25%)  → error   (#ff3333 red)

All four CSS variables already exist in apps/ui/src/index.css and are the
canonical severity vocabulary across MeasurementPrimitives, DiagnosticLog,
and FileUpload (success = ready, accent = active, warning = caution,
error = critical). No theme additions; no new shades.

Opacity tuning unified at /30 border, /10 bg, text-{color} to match
MeasurementPrimitives.BADGE_TONE_CLASSES. Color carries the differentiation
now — the previous per-band opacity wobble didn't communicate anything.

NoteDraftBlock's overrideTone="rough" path (used for full-mix-fallback and
legacy render states) now renders amber, which is the right tone for "treat
with caution, not unreliable junk." No wiring change needed.

Tests: new tests/services/confidenceBandBadge.test.ts renders all four
confidence values and asserts each emits the expected color tokens. A
pairwise-distinct assertion survives future opacity tweaks. Two override-path
tests lock in the full-mix-fallback / legacy visual (high-confidence input
with overrideTone="rough" produces warning tokens, not green; low-confidence
input stays warning, not red). No existing tests touched — confidenceBand
asserts id/label/percent (not classes); panel + smoke tests assert on text.

Targets PR #22 (claude/thirsty-easley-faeaea) as the base. Plan in
~/.claude/plans/re-evaluate-asa-s-session-musician-sharded-balloon.md
("Follow-up plan #1" section).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
slittycode added a commit that referenced this pull request May 13, 2026
…g completion (#18)

* feat: Phase 1.A–1.D depth additions, Track 2 audit, decision-gate v3 + catalog completion

Substantial measurement-depth pass on the ASA Phase 1 pipeline plus a
trustworthy multi-model decision-gate harness. Default Gemini model
(gemini-2.5-flash) now PASSes the stem-aware gate on the Vtss bench
track; Live 12 catalog completion drops UNKNOWN_PARAMETER 16 → 1
across all v3.1 snapshots.

## Depth additions (Phase 1.A – 1.D)

- Phase 1.A cheap wins (5): per-frame short-term LUFS curve, per-frame
  7-band spectral balance time series, instantaneous tempoCurve,
  per-second stereo correlationCurve, 256-point arrangement
  noveltyCurve (up from 64).
- Phase 1.B stem-first refactor: per-stem subtree under
  ``stemAnalysis.{drums,bass,other,vocals}`` with the same shape as
  the full-mix analyzers. Phase 2 can now cite element-specific
  measurements.
- Phase 1.C #0 real time-signature detection (onset-accent
  autocorrelation, replaces the always-4/4 fallback).
- Phase 1.C #1 per-band transient density across the 7
  spectralBalance bands (kick / hi-hat density anchors).
- Phase 1.C #2 per-band stereo correlations (Utility-width-per-band
  recommendations).
- Phase 1.C #3 ``grooveDetail.perDrumSwing.{kick,snare,hihat}`` from
  the mid-band beat-loudness signal.
- Phase 1.C #4 snareDetail / hihatDetail band-limited drum character
  (hits, attack sharpness, body/snap energy ratio, decay).
- Phase 1.C #5 saturationDetail (clipped sample count, peakRatio95to50,
  rmsToPeakRatioDb, saturationLikely).
- Phase 1.C #6 32nd-note sidechain resolution + envelopeShape32
  (legacy envelopeShape preserved via max-pairing downsample);
  pumpingRate now four-valued including thirty_second.
- Phase 1.D #5 RT60 reverb per stem with perBandRt60 (low / lowMids /
  highMids / highs) + preDelayMs via Schroeder-equivalent slope fit.
  Wired through stemAnalysis.{stem}.reverbDetail.

## Track 2 audit pass 1 + measurement-quality fixes

- New ``scripts/audit_pass1.py`` harness — structured 12-section
  markdown audit comparing real-track Phase 1 measurements against
  declared ground truth and sanity rules.
- Bug fix: ``bassDetail.averageDecayMs`` was 0 ms on real bass
  material (Codex PDF finding reproduced). Root cause: decay loop
  searched from onset rather than peak, and checked the raw
  oscillating waveform rather than an envelope. Fixed with a 20 ms
  RMS envelope + peak-anchored search. Synthetic validation: 350 ms
  theoretical → 346 ms measured; 35 ms → 37 ms. Real-track Vtss:
  0 ms (FAIL) → 66–125 ms (PASS).
- Bug fix: ``vocalDetail.hasVocals = true`` false positive on
  instrumental synth content. Added temporal-formant-stability check
  (sustained synth leads have near-static "formants"; real vocals
  shift formants 100+ Hz with syllables); tightened formant tolerance
  200 → 100 Hz; raised threshold 0.45 → 0.55. No-stem run on Vtss now
  correctly reports hasVocals=false at conf 0.48.
- Pre-existing import bugs in ``analyze_segments.py``:
  ``_compute_stereo_metrics`` (from dsp_utils) and ``Counter`` (from
  collections) were undefined. ``segmentStereo`` + ``chordDetail``
  now populate correctly.

## Phase 2 contract + validator + decision gate

- Phase 2 prompt v3: per-stem path enumeration, citation contract
  (``phase1Fields`` required on every recommendation), "Phase-2
  self-reference is not a citation" rule, dedicated sections for
  snare/hi-hat character, saturation, sidechain envelope, reverb
  detail.
- Phase 2 schema (``server_phase2.py``): ``phase1Fields`` array
  required on ``mixAndMasterChain``, ``abletonRecommendations``, and
  ``secretSauce.workflowSteps`` items.
- Frontend validator (``phase2Validator.ts``) overhaul:
    * 8 violation types (NUMERIC_OVERRIDE, GENRE_IGNORES_DSP,
      BOUNDS_VIOLATION, MISSING_CITATION, TRIVIAL_CITATIONS,
      NEW_FIELD_UNCITED, LOW_CONFIDENCE_NOT_HEDGED, RECOMMENDATION_SALVAGED).
    * Bidirectional + wildcard ``pathCoversTracked`` matcher so leaf
      citations satisfy parent tracked paths and ``stemAnalysis.*``
      wildcards work.
    * 19 tracked Phase 1.A + Phase 1.C/D field paths (was 7).
- Decision-gate vitests (4): single-, multi-, real-, and stem-aware
  multi-model comparators that read ``/tmp/decision_gate_*.json``
  snapshots and emit per-model markdown reports.

## Live 12 catalog completion (UNKNOWN_PARAMETER closure)

- Auto Filter: ``parameterAliases`` flat-map
  ``{"Filter Resonance": "Resonance", "Filter Frequency": "Frequency"}``.
  Closes the instrument-side long-form naming bleed.
- Glue Compressor: ``allowedParameters`` expanded by 4
  (Range, Sidechain, Sidechain Gain, Sidechain Dry/Wet).
- ``_validate_phase2_catalog_entry`` resolves aliases before the
  membership check; startup-time loader validates the new
  ``parameterAliases`` shape.
- New ``Phase2CatalogValidationTests`` (11 cases) covers alias
  resolution, expansion, scoping per-device, and negative cases.
- New ``scripts/replay_catalog_validation.py`` — no-Gemini-spend
  harness that re-validates v3.1 snapshots against the live catalog.
  Verdict: UNKNOWN_PARAMETER drops 16 → 1 across all 8 snapshots;
  the remaining hit is ``EQ Eight / "Band 8 Filter Type"`` (separate
  device, v3.2-target).

## Test state

- Backend: 374 tests pass (was 363; +11 ``Phase2CatalogValidationTests``).
- Frontend: 296 tests pass (was 286; +10 validator bidirectional /
  wildcard tests).
- UI lint clean (``tsc --noEmit``).

## Out of scope / follow-ups (documented in SESSION_BLITZ + plan)

- Pro-model citation-depth iteration (3-pro and 3.1-pro still cite
  far less Phase 1.C/D than 2.5-flash).
- gemini-3.1-pro-preview ``DROPPED_INVALID_ARRAY_ITEM`` salvage
  regression seen in earlier v3 runs.
- Compressor "Sustain", "Ableton Project Settings", "Mixer" device
  hits — v3.2 prompt-fix targets.
- Phase 1.D #1 madmom integration (prototype proven, install path
  documented at ``.runtime/reports/madmom_install_prototype_2026-05-12.md``).
- Phase 1.D #2 chord progression deeper labelling.
- ``JSON_SCHEMA.md`` documentation of the catalog schema
  (``parameterAliases`` field).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(backend): add coverage for Phase 1.C analyzers, per-stem orchestrator, and dsp_utils (#19)

Closes the test-coverage gaps flagged in the PR review.

Backend:
- test_analyze.py: new BandDrumDetailTests, SnareDetailTests, HihatDetailTests,
  TransientDensityDetailTests, SaturationDetailTests, RunPerStemAnalysesTests.
  Each covers null/short/silent input guards, schema-field presence, and at
  least one behavioral assertion against a synthetic signal that should trip
  the detector. RunPerStemAnalysesTests covers stems=None, empty dict, all
  loads failing, mono+stereo success, partial-analyzer failure, and the
  stereo-load-skipped path.
- BassDetailTests: regression test for averageDecayMs > 30 ms on a synthetic
  decaying-pulse signal — guards the envelope-based fix from sliding back to
  the pre-fix sub-millisecond range.
- test_dsp_utils.py (new): direct tests for _pearson_corr,
  _downsample_lufs_array, _downsample_band_energies_curve,
  _compute_tempo_curve_from_ticks, and _compute_stereo_correlation_curve,
  with closed-form expected values for perfect/anti/orthogonal correlation,
  constant-input NaN handling, tempo-change detection, and the silent-sub
  None policy.

Docs / comments:
- JSON_SCHEMA.md: flag bassDetail.fundamentalHz as a ZCR approximation that
  biases upward on harmonic-rich basses; steer Phase 2 to prefer
  pitchDetail when stems are available and avoid narrow filter-Q moves
  within ±15 Hz of the value.
- phase2Validator.ts: rewrite the pathCoversTracked docstring so it
  matches what the code actually does (a wildcard token covers all longer
  citation paths under the same prefix; "stemAnalysis.*" does match
  "stemAnalysis.drums.spectralBalance").

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
slittycode added a commit that referenced this pull request May 16, 2026
…branch, store prune (#51)

* audit: chain-of-custody, recommendations-first IA, applied tracker, grammar post-process

Implements the design audit's prescription end-to-end (findings #1#15 + N1/N2/N7/N9/N10
+ follow-ups). The product's chain-of-custody promise — every Phase 2 recommendation
traces back to the Phase 1 measurement that justifies it — is now visually first-class
on every card, not a 9px monospace footnote.

What ships:

* Chain-of-custody (findings #2 + #3). New CitationBlock primitive renders a structured
  "GROUNDED IN" block above every Mix Chain / Patches / Sonic Element card with
  humanized labels (FIELD_LABELS map, ~50 entries + humanizeFieldPath fallback) and a
  ConfidenceBandBadge pill computed from the worst confidence among cited fields.
  Also retires GroundingBadgeList at the Track Layout site; segmentIndexes ride
  through a new extraRows prop.

* Recommendations-first IA (#1). MeasurementDashboard moved to the bottom of the
  results scroll; StickyNav's 9 measurement pills collapse to one trailing
  "Measurements" entry. Producers hit Style → Sonic → Mix Chain → Patches → Session
  before the measurement evidence.

* Header polish (#7, #9, #11). CPU meter removed (browser-tab CPU is misleading
  during backend analysis), "Local DSP Engine v1.6.0" eyebrow removed (resolves
  mobile 3-line wrap), Dense DAW Lab demoted from accent chip to quiet text link.

* Engineering vocab cleanup (#8 + N3/N4/N5/N8). New userLabels.ts service translates
  field paths to producer-readable labels at every render. Button labels renamed
  (Download data / Download report). FAMILY: NATIVE chip dropped from meta-badge rows.
  workflowStage prettified at the view-model layer ("Sound design" not "SOUND_DESIGN").
  AI Interpretation gated copy reworded ("AI interpretation isn't configured…" not
  "Developer kill-switch is off").

* Applied-recommendations tracker (#14 + #15). Per-card checkbox affordance + section-
  header "N of M applied" chip + localStorage persistence keyed by audio content
  SHA256. Producer can rename their file without losing their progress.

* Idle value-prop panel (#5). Replaces the 200px "NO SIGNAL DETECTED" canvas with
  a producer-readable explanation of what ASA does, with honest pacing copy
  (4–5 min Phase 2 wait, not 2–5 min).

* Patches group structure. Mirrors Mix Chain's emoji-eyebrow grouping (Drums / Bass /
  Synth / Master) so producers can jump to the bass patch without scanning 8 cards.

* Input Source collapse (N9). Post-analysis, the Input Source panel collapses to a
  compact summary with filename + duration + "Analyze new file" + "Adjust settings".
  Frees the top of the page for the results the user came for.

* AnalysisStatusPanel primary readout (#6). Stage diagnostic message promoted from
  9px footnote to the visual focus of the progress card. Pre-existing tone-aware
  fill (running / success / failed) preserved.

* Phase 2 failure mode (N1). Header subtitle derives from interpretation stage status
  (no more "PHASE COMPLETE" while INTERPRET still RUNNING/FAILED). StickyNav Phase 2
  pills render disabled with hover-reason when sections didn't populate. Retry button
  gated on error.retryable; non-retryable failures surface the error code inline.

* Misc audit follow-ups: BPM reconciled across exec card + Core Metrics tile (N2);
  Signal Monitor STANDBY canvas hidden when audio isn't playing, freeing ~160px (N7);
  StickyNav label "Device Chain" → "Sections" (N10, less ambiguous with Ableton's own
  effects-routing meaning); BASS group icon swapped from 🫧 → Lucide AudioWaveform
  (#13); toggle helper paragraphs switched from all-caps mono walls to sans-serif
  sentence case (#4 revised).

* Phase 2 grammar post-process (audit final round). The prompt instruction added
  earlier didn't take — Gemini still emits "by recreates / by absorbs / by shapes"
  3rd-person singular forms after "by" in role/reason text. Server-side
  _apply_phase2_grammar_fixes rewrites these to gerunds in-place on
  mixAndMasterChain[].reason, abletonRecommendations[].{reason,advancedTip}, and
  secretSauce.workflowSteps[].{instruction,measurementJustification}. Conservative
  regex (\bby \w{4,}s\b) + denylist guards against plural-noun false positives.

Test coverage:

* UI: 46 test files / 540 tests pass (was 39/422 before the audit). New service tests:
  userLabels (21), phase1Picker (25), citationBlock (12), appliedRecommendations (16),
  formatTrackDuration (12), interpretationSubtitle (10), workflowStagePrettifier (8),
  analysisStatusProgress (6), idleValuePropPanel + phase2NavReason. New DOM tests in
  analysisResultsUi.test.ts cover Track Layout citation, applied-checkbox flow, mix-
  chain citation rendering.

* Backend: 16 new unit tests in test_phase2_grammar_fix.py cover _to_gerund,
  _fix_by_gerund_in_text, _apply_phase2_grammar_fixes including the actual
  screenshot corpus (recreates → recreating, shapes → shaping, matches → matching,
  etc.). Full suite: 463 of 463 ASA tests pass; 1 unrelated pre-existing failure
  in tests.test_url_ingest predates this branch.

Visual verification: Playwright capture pass against a real 126s track confirmed
every surface (15 screenshots in /tmp/asa-shots-audit-final/). Phase 2 returned in
220s; localStorage round-trip verified on applied-checkbox toggles.

Documented limitations:

* The gerund rule is algorithmic — verbs requiring consonant doubling (control →
  controlling, submit → submitting) degrade to "controling" / "submiting". Still
  better than "by controls". Drop a hand-mapped exception into the module if
  observed in real output.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix smoke tests for retired/renamed surfaces

CI surfaced 7 smoke test failures from this branch's audit changes that
hadn't been propagated to the smoke spec assertions:

* `tests/smoke/ui-details.spec.ts` × 3
  - `NO SIGNAL DETECTED` → `IdleValuePropPanel` (audit #5)
  - `JSON_DATA` / `REPORT_MD` button labels → `Download data` / `Download report`

* `tests/smoke/responsive-layout.spec.ts` × 4
  - `NO SIGNAL DETECTED` (×2) → `IdleValuePropPanel`
  - `CPU` text-presence checks removed; the two viewport-shape tests now
    assert just the model-selector responsive behavior (audit #11 retired
    the CPU meter; there's no element to assert)

* `tests/smoke/file-validation.spec.ts` × 1
  - `re-upload after results resets to file-selected state`: the test
    used `Remove File` (FileUpload component's affordance) to clear after
    results were visible. Post-N9 collapse, the Input Source panel
    replaces FileUpload with a compact summary card whose "↺ Analyze new
    file" button calls the same handleFileClear. Switched the test to
    target the new affordance.

Also updated the e2e exports spec for label consistency (not in the
failing CI job, but the same renames apply):

* `tests/e2e/phase1-exports.spec.ts`
  - `downloadTextArtifact(page, /JSON_DATA/i)` → `/Download data/i`
  - `downloadTextArtifact(page, /REPORT_MD/i)` → `/Download report/i`

Verified locally against the live stack: 45 of 46 smoke tests pass, 1
skipped (was unrelated). The previously-failing 7 are all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* audit: nightly Phase-1 invariant + test guardrail (2026-05-14)

3 unit tests failing — all fixture-dependent live decision-gate
comparators that assert instead of skipping when Gemini snapshots
are absent. No Phase-boundary violations found.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: address review findings — decision-gate skip, gerund exceptions, dead branch, store prune

- Skip decision_gate.{multi,real,stems}.live.test.ts when no /tmp snapshots
  are present (matches the existing decision_gate.live.test.ts pattern).
  Unblocks npm test on CI.
- _to_gerund: add _GERUND_IRREGULARS map for consonant-doubling verbs
  (controls→controlling, submits→submitting, runs→running, etc.) — English
  doubling is stress-conditional, not worth implementing algorithmically.
- _fix_grammar_in_record: drop the always-true conditional return and the
  unused `updated` flag.
- appliedRecommendations: bound the localStorage store to MAX_TRACKED_FILES=50
  (least-recently-updated wins eviction) so it can't grow without bound.

* fix: formatTrackDuration carries seconds boundary; drop dead IdleSignalMonitor

- formatTrackDuration: round seconds to total first, then derive mins/secs.
  Previously Math.round(seconds % 60) could yield 60, producing "0:60" for
  inputs like 59.5. New tests cover 59.5, 59.9, 119.5, 3599.7.
- Delete IdleSignalMonitor.tsx (replaced by IdleValuePropPanel; not imported
  anywhere). Strip the "kept for future use" comments per CLAUDE.md.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
slittycode added a commit that referenced this pull request May 17, 2026
…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>
slittycode added a commit that referenced this pull request May 17, 2026
* 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>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Oz <oz-agent@warp.dev>
slittycode added a commit that referenced this pull request May 17, 2026
* 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>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Oz <oz-agent@warp.dev>
slittycode added a commit that referenced this pull request May 17, 2026
…ladder (#56)

* 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>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Oz <oz-agent@warp.dev>
slittycode added a commit that referenced this pull request May 17, 2026
…eader) (#58)

* 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>

* 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 (1M context) <noreply@anthropic.com>
Co-authored-by: Oz <oz-agent@warp.dev>
@slittycode slittycode mentioned this pull request May 18, 2026
4 tasks
slittycode added a commit that referenced this pull request May 18, 2026
* 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(eval): Layer 2 transcription harness + polyphonic scoring workflow

Wires Layer 2 (torchcrepe) transcription into the Phase 1 evaluation
harness behind --include-transcription, with a stepped-sine self-test
that runs even when the corpus is empty and a transcriptionTracks slot
in the manifest fixture. Adds new interactive tooling for the
polyphonic-transcription spike: score_polyphonic_clip.py walks each
clip/candidate pair and writes scorecards back into the report JSON,
import_midi_to_ground_truth.py seeds ground-truth references, and
summarize_midi_file emits richer diagnostic flags (note_clutter,
octave_junk, dense_chords_unusable, sparse_likely_undertranscribed)
to steer reviewer attention without bypassing the manual usefulness
gates. Documents the corpus + scoring workflow and the distinction
between the polyphonic_tracks/ research corpus and the
transcription_tracks/ Layer 2 bench.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Oz <oz-agent@warp.dev>
slittycode pushed a commit that referenced this pull request May 18, 2026
PR 6 of 16 in the UI consistency overhaul. Promotes the AnalysisResults
page header to <SectionHeader> and wraps the top TEMPO/KEY/METER/
CHARACTER metric strip in a <DeviceRack name="Measurement Summary">
with <MetricTile> cards inside.

Stacked on PR #69 (D.4 FileUpload). Will rebase onto main after #69
merges.

Header (AnalysisResults.tsx:835-880 → :835-878):
- <h1> + Download buttons row → <SectionHeader size="lg"
  variant="inline" eyebrow="ASA Results" titleRole="page-title">
  with both Download buttons in the action slot.
- The Activity icon prefix on the h1 disappears — the SectionHeader
  LED dot already carries the "results are live" semantic.
- The subtitle (sourceFileName · interpretationSubtitle) stays as a
  separate <p> sibling, preserving data-testid="analysis-results-
  subtitle" and the existing text-role-meta styling. Indented pl-4
  to align under the title after the LED.
- Download data: <button> → <Button variant="secondary"
  leadingIcon=FileJson>. Preserved data-testid="analysis-export-json".
- Download report: <button> → <Button variant="primary"
  leadingIcon=FileText>. Preserved
  data-testid="analysis-export-markdown".

Metric strip (AnalysisResults.tsx:884-1012 → :882-1010):
- Outer <div className="grid grid-cols-2 md:grid-cols-4 gap-3"> wrapped
  in <DeviceRack name="Measurement Summary" density="dense"
  status="success">. The grid layout stays as-is inside the rack body
  so the responsive 2-col / 4-col break still works.
- 4 × AccentMetricCard → 4 × <MetricTile size="lg" accent="accent">.
  API parity drop-in:
  • label (was a flex-span with icon+text) → icon prop + label string.
    Cleaner separation; the MetricTile primitive places the icon in
    the eyebrow row.
  • value / unit / headerRight / footer pass through unchanged.
  • The hand-rolled text-[1.5rem] override on the CHARACTER value
    disappears — MetricTile size="lg" already renders text-2xl which
    is the same 1.5rem.

Imports:
- AccentMetricCard stays imported from ./MeasurementPrimitives.
  Other usages of it remain at :1249-1258 and :1444-1448 (Patches /
  Sonic Elements sections — migrated in later PRs D.5b/c/d).
- Added Button / DeviceRack / MetricTile / SectionHeader from ./ui.

Preserved verbatim:
- All data-testid attributes (analysis-results-root, analysis-results-
  subtitle, analysis-export-json, analysis-export-markdown).
- All visible text strings: "Analysis Results", "Download data",
  "Download report", "TEMPO", "BPM", "KEY SIG", "METER", "CHARACTER",
  "SCANNING...".
- The PhaseSourceBadge + ConfidenceBandBadge + MetricBar + StatusBadge
  + TokenBadgeList rendering inside each tile is untouched.
- Audit Finding #4 commentary preserved (canonical band-pill
  vocabulary).
- Audit Finding #1 ordering comment preserved (MeasurementDashboard
  rendering at the bottom of the scroll).
- The lowConfidenceIndicator + characteristicPills logic untouched.

Verified: lint clean, 628/628 unit tests passing, build clean
(AnalysisResults chunk 255 KB / 63 KB gz; index chunk 323 KB / 97 KB
gz — both stable vs PR 5).

https://claude.ai/code/session_01WNtYcWXwn4mVnnjxqT7uAe
slittycode added a commit that referenced this pull request May 18, 2026
PR 6 of 16 in the UI consistency overhaul. Promotes the AnalysisResults
page header to <SectionHeader> and wraps the top TEMPO/KEY/METER/
CHARACTER metric strip in a <DeviceRack name="Measurement Summary">
with <MetricTile> cards inside.

Stacked on PR #69 (D.4 FileUpload). Will rebase onto main after #69
merges.

Header (AnalysisResults.tsx:835-880 → :835-878):
- <h1> + Download buttons row → <SectionHeader size="lg"
  variant="inline" eyebrow="ASA Results" titleRole="page-title">
  with both Download buttons in the action slot.
- The Activity icon prefix on the h1 disappears — the SectionHeader
  LED dot already carries the "results are live" semantic.
- The subtitle (sourceFileName · interpretationSubtitle) stays as a
  separate <p> sibling, preserving data-testid="analysis-results-
  subtitle" and the existing text-role-meta styling. Indented pl-4
  to align under the title after the LED.
- Download data: <button> → <Button variant="secondary"
  leadingIcon=FileJson>. Preserved data-testid="analysis-export-json".
- Download report: <button> → <Button variant="primary"
  leadingIcon=FileText>. Preserved
  data-testid="analysis-export-markdown".

Metric strip (AnalysisResults.tsx:884-1012 → :882-1010):
- Outer <div className="grid grid-cols-2 md:grid-cols-4 gap-3"> wrapped
  in <DeviceRack name="Measurement Summary" density="dense"
  status="success">. The grid layout stays as-is inside the rack body
  so the responsive 2-col / 4-col break still works.
- 4 × AccentMetricCard → 4 × <MetricTile size="lg" accent="accent">.
  API parity drop-in:
  • label (was a flex-span with icon+text) → icon prop + label string.
    Cleaner separation; the MetricTile primitive places the icon in
    the eyebrow row.
  • value / unit / headerRight / footer pass through unchanged.
  • The hand-rolled text-[1.5rem] override on the CHARACTER value
    disappears — MetricTile size="lg" already renders text-2xl which
    is the same 1.5rem.

Imports:
- AccentMetricCard stays imported from ./MeasurementPrimitives.
  Other usages of it remain at :1249-1258 and :1444-1448 (Patches /
  Sonic Elements sections — migrated in later PRs D.5b/c/d).
- Added Button / DeviceRack / MetricTile / SectionHeader from ./ui.

Preserved verbatim:
- All data-testid attributes (analysis-results-root, analysis-results-
  subtitle, analysis-export-json, analysis-export-markdown).
- All visible text strings: "Analysis Results", "Download data",
  "Download report", "TEMPO", "BPM", "KEY SIG", "METER", "CHARACTER",
  "SCANNING...".
- The PhaseSourceBadge + ConfidenceBandBadge + MetricBar + StatusBadge
  + TokenBadgeList rendering inside each tile is untouched.
- Audit Finding #4 commentary preserved (canonical band-pill
  vocabulary).
- Audit Finding #1 ordering comment preserved (MeasurementDashboard
  rendering at the bottom of the scroll).
- The lowConfidenceIndicator + characteristicPills logic untouched.

Verified: lint clean, 628/628 unit tests passing, build clean
(AnalysisResults chunk 255 KB / 63 KB gz; index chunk 323 KB / 97 KB
gz — both stable vs PR 5).

https://claude.ai/code/session_01WNtYcWXwn4mVnnjxqT7uAe

Co-authored-by: Claude <noreply@anthropic.com>
slittycode added a commit that referenced this pull request May 30, 2026
…ounding gate (#122)

Adds MT3 (Magenta Multi-Task Multitrack) as a flag-gated, additive polyphonic-transcription stage emitting per-instrument MIDI, and gates interpretation on in-flight MT3/pitch-note stages so Gemini Phase 2 reliably sees the additive grounding. Additive only — Phase 1 stays authoritative (PURPOSE.md #1); camelCase contract and types.ts untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
slittycode pushed a commit that referenced this pull request Jun 2, 2026
Phase 1 v2 emits truePeak: null for digital silence (no defined dBTP),
but the frontend type declared it `number` and several display sites
called .toFixed unguarded — a latent crash on silent input. Worse, the
HTTP envelope coerced null -> 0.0, falsely reporting a full-scale peak
for a silent track (violating measurement-honesty invariants #1/#4).

Backend:
- server_phase1._build_phase1 now preserves null via _coerce_nullable_number
  instead of defaulting to 0.0; PLR fallback already guards None.
- Tests: truePeak passthrough/null + PLR-null-when-peak-null.

Frontend:
- Phase1Result.truePeak typed `number | null`.
- analysisResultsViewModel: formatTruePeak (renders "—" for null, matching
  the house formatNumber convention) and masterCeilingDb (falls back to the
  -0.3 dB cap when there is no peak to duck below); applied at all 6 sites.
- MeasurementDashboard: omit the true-peak marker, PLR gap fill, and PLR
  annotation for silence (LUFS marker still renders).
- backendPhase1Client: accept an explicit null truePeak (legacy path) while
  still rejecting a malformed/missing field.
- exportUtils markdown renders "—" for a null peak.
- Tests: null-silence card renders "—" without crashing; helper unit cases.

Docs: JSON_SCHEMA truePeak note now states null == digital silence.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q
slittycode added a commit that referenced this pull request Jun 2, 2026
…t PLR (#128)

* fix(loudness): Phase 1 v2 - truePeak to dBTP, bpmConfidence to 0-1, coherent PLR

Phase 1 schema v2 migration for the loudness fields, motivated by a consumer
audit: ~12 UI/recommendation consumers already assumed `truePeak` was dBTP
(several were latent bugs), while only the loudness guardrail treated it as the
linear amplitude proxy v1 actually emitted.

- truePeak now emitted in dBTP (0.0 == full scale, >0 == inter-sample over);
  null for silence. bpmConfidence normalized to 0-1 (raw Essentia ~0-5.32 / 5).
- PLR is now a coherent dB-domain subtraction (truePeak_dBTP - lufsIntegrated),
  fixing the prior linear-minus-dB unit mismatch, across analyze_core, the HTTP
  fallback, and the phase1 eval consistency gate.
- Add top-level `phase1Version` ("phase1.v2") so consumers can detect the
  generation; loudness guardrail over-check moves to dBTP (>0); the validator
  drops its special-case bpmConfidence threshold (now shares the 0.4 hedge).
- Re-baseline the golden (truePeak 0.0, plr 5.6); add dedicated analyze_plr and
  dBTP true-peak unit tests; update fixture expectations, types, JSON_SCHEMA.
- ADR 0002 records the v2 bump, superseding ADR 0001 for these two fields.

Verified: backend unittest suite green; frontend tsc + 716 unit tests + build green.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

* chore: ignore Python bytecode repo-wide

The .gitignore only covered apps/backend/__pycache__/, so bytecode generated
outside the backend (e.g. scripts/__pycache__/ when build_live12_catalogue.py
is imported) showed up as untracked. Generalize the __pycache__/ and *.py[cod]
rules to the whole repo.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

* fix(loudness): v2 follow-ups - phase1 eval manifest targets + dBTP labels

- Backend CI fix: phase1_eval_manifest.json truePeak/plr thresholds still
  encoded v1 linear values (click_120 truePeak 0.9 / plr 24.8; sine_220 0.5 /
  7.4). Migrated to the v2 dBTP measurements (click -0.9 / 23.2; sine -6.0 /
  1.0). The plr:consistency gate already passes; only the manifest's expected
  targets were stale.
- Review (#128): three analysisResultsViewModel.ts display sites labeled the
  now-correct truePeak value as "dB"; corrected to "dBTP".

Verified: tests.test_phase1_evaluation green; frontend tsc + 716 unit green.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

* Harden nullable truePeak across the v2 contract

Phase 1 v2 emits truePeak: null for digital silence (no defined dBTP),
but the frontend type declared it `number` and several display sites
called .toFixed unguarded — a latent crash on silent input. Worse, the
HTTP envelope coerced null -> 0.0, falsely reporting a full-scale peak
for a silent track (violating measurement-honesty invariants #1/#4).

Backend:
- server_phase1._build_phase1 now preserves null via _coerce_nullable_number
  instead of defaulting to 0.0; PLR fallback already guards None.
- Tests: truePeak passthrough/null + PLR-null-when-peak-null.

Frontend:
- Phase1Result.truePeak typed `number | null`.
- analysisResultsViewModel: formatTruePeak (renders "—" for null, matching
  the house formatNumber convention) and masterCeilingDb (falls back to the
  -0.3 dB cap when there is no peak to duck below); applied at all 6 sites.
- MeasurementDashboard: omit the true-peak marker, PLR gap fill, and PLR
  annotation for silence (LUFS marker still renders).
- backendPhase1Client: accept an explicit null truePeak (legacy path) while
  still rejecting a malformed/missing field.
- exportUtils markdown renders "—" for a null peak.
- Tests: null-silence card renders "—" without crashing; helper unit cases.

Docs: JSON_SCHEMA truePeak note now states null == digital silence.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

---------

Co-authored-by: Claude <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jun 2, 2026
* WS3a: asa-dsp ↔ Essentia loudness parity harness + report

Before the loudness-spectro-wasm core (asa-dsp, openmeters-derived BS.1770)
can back any ASA product path (WS3b backend, WS3c browser readout), it has
to agree with Essentia — ASA's authoritative Phase 1 loudness source, since
Phase 1 measurements are ground truth (PURPOSE.md invariant #1).

scripts/essentia_parity.py runs a deterministic synthetic corpus (tones,
sweep, white/pink noise, decorrelated stereo) through BOTH paths at each
file's native rate (no resampling):
- asa-dsp via the native measure-cli binary (source-identical to the WASM
  core, so WS3a needs no wasm toolchain), and
- Essentia via AudioLoader -> LoudnessEBUR128, mirroring
  analyze_core.analyze_loudness, plus a dBTP true peak from TruePeakDetector.

Primary gate: integrated LUFS within +/-0.1 LU on every file.

Result (docs/essentia-parity-report.md): 7/8 signals agree within +/-0.02 LU
(most within +/-0.002) — essentially perfect BS.1770 parity on tones and
noise. The sole breach is the 20 Hz->20 kHz log sweep at -0.102 LU (0.002
over the gate), a pathological non-program signal. Verdict is reported as the
harness computed it (FAIL by that single borderline case); the realistic-signal
agreement is the substantive finding. Surfaced at the WS3a hard checkpoint.

README documents the harness as Validation helper #4.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

* WS3a review: simplify run(), flag true-peak divergence for WS3c

Addresses the PR #129 review:
- run() no longer calls build_report() and discards the text; it returns
  rows only. build_report() now runs exactly once, in main(). Pure refactor —
  the harness reproduces identical numbers.
- Report caveats now explicitly flag the broadband true-peak divergence
  (white_noise ~0.6 dBTP gap): WS3c must not expose a browser true-peak
  readout without its own gate. Integrated loudness is unaffected.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

---------

Co-authored-by: Claude <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jun 2, 2026
…default-off) (#132)

* WS3b: optional asa-dsp loudness backend behind ASA_LOUDNESS_BACKEND

Wires the loudness-spectro-wasm core (asa-dsp) into the backend as a default-off
alternative loudness source for the LUFS scalars, selected by
ASA_LOUDNESS_BACKEND=essentia|wasm (default essentia).

Architecture (deviates from the plan's pyo3 sketch, deliberately):
- Invokes the already-built native measure-cli binary on a temp WAV rather than
  binding via pyo3. This sidesteps the workspace's `panic = "abort"` profile
  (which blocks pyo3), needs no separate Cargo workspace, and — being pure
  Python on this side — cannot red the loudness-wasm CI job. The backend has
  already decoded the audio, so the temp WAV makes it format-agnostic.
- Scope is the four LUFS scalars (integrated/range/momentary-max/short-term-max)
  — the EBU loudness the WS3a parity harness validated at ±0.1 LU. truePeak stays
  Essentia (asa-dsp's true peak diverges on broadband per #129) and lufsCurve
  stays Essentia (the CLI emits scalars, not per-frame curves). So this is a
  scalar override, not a full engine replacement.
- Default stays Essentia (PURPOSE.md invariant #1); any failure (missing binary,
  non-zero exit, unparseable/null output) degrades gracefully back to Essentia.

apps/backend/loudness_backend.py + a call after analyze_loudness in analyze.py.
Tests mock the subprocess (the binary isn't built in the Python CI job): backend
selection, scalar override + curve passthrough, and every fall-back path. The
default path is unchanged — test_analyze + the golden snapshot still pass.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

* WS3b review: fix repo-root path off-by-one; don't clobber Essentia with null

Addresses the PR #132 REQUEST CHANGES:
- BLOCKING: _REPO_ROOT used parents[1] (= apps/), so the default measure-cli
  path was apps/packages/loudness-spectro-wasm/... which doesn't exist — the
  feature silently degraded to Essentia unless ASA_MEASURE_CLI was set. Fixed
  to parents[2] (repo root). Regression test asserts the default path resolves
  to the real <repo>/packages/loudness-spectro-wasm dir.
- Should-fix: only override a LUFS scalar when the CLI value is non-null, so a
  null lra (undefined for short content where integrated is still valid) can't
  clobber Essentia's reading. Regression test covers it.

12/12 loudness_backend tests pass; the warn path now correctly points at
<repo>/packages/... (no apps/ prefix).

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

---------

Co-authored-by: Claude <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jun 2, 2026
* WS3c: in-browser loudness parity readout (flag-gated, WAV-only)

Adds an additive browser-side loudness readout that measures the uploaded WAV
with the asa-dsp (WASM core) and compares the LUFS scalars against the
authoritative Phase 1 Essentia values. Phase 1 stays the source of truth
(PURPOSE.md invariant #1); this is a diagnostic, default-off behind
VITE_ENABLE_BROWSER_LOUDNESS.

Services (pure, fully unit-tested):
- wavDecoder.ts: decodes uncompressed WAV (PCM 16/24/32 + float32, incl.
  WAVE_FORMAT_EXTENSIBLE) to interleaved f32 at the file's NATIVE rate — no
  AudioContext.decodeAudioData resampling, which would break ±0.1 LU EBU
  conformance. WAV only; FLAC/MP3 report "WAV only" rather than guessing.
- parity.ts: computeLoudnessParity → per-field deltas vs Phase 1, gated at
  ±0.1 LU. True peak is deliberately excluded (asa-dsp diverges on broadband
  per #129).
- loader.ts: guarded dynamic import of the WASM module from a runtime URL
  (VITE_BROWSER_LOUDNESS_WASM_URL) with @vite-ignore so the build never tries
  to resolve the (unbuilt, gitignored) package; returns null → "unavailable"
  on any failure. The result adapter (NaN→null, free()) is unit-tested.

UI: BrowserLoudnessPanel (flag-gated mount in AnalysisResults) reuses the
retained App `audioFile`; graceful states for non-WAV, wasm-unavailable, error.

Verification limits (honest): the WAV decoder + parity math + result adapter are
unit-tested; the actual WASM run and browser behavior are NOT verified here (no
browser, and building web-wasm needs wasm-bindgen-cli). Activation requires
building packages/loudness-spectro-wasm and serving pkg/ — documented in the
unavailable state. 729 unit tests pass; build clean (no static wasm import).

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

* WS3c review: fix EXTENSIBLE guard off-by-2, drop redundant import.meta cast

- wavDecoder: WAVE_FORMAT_EXTENSIBLE guard needs chunkSize >= 26 (not 24) to
  safely read the 2-byte SubFormat tag at body+24; add a test that decodes a
  real 40-byte EXTENSIBLE fmt chunk (the branch was previously untested).
- loader: VITE_BROWSER_LOUDNESS_WASM_URL is declared in vite-env.d.ts, so the
  import.meta cast is unnecessary — read import.meta.env directly.

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

---------

Co-authored-by: Claude <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jun 5, 2026
…#143)

Phase 2 emits device recommendations across three free-shaped arrays
(abletonRecommendations, mixAndMasterChain, secretSauce.workflowSteps) with
free-text values and no machine-checkable freeze. This adds a normalized,
versioned, schema-validated contract for that surface.

- schemas/recommendations.v1.schema.json: Draft 2020-12 schema. Envelope
  {version, recommendations[]}; each entry {device, parameter, value, unit,
  range, cited_measurements[]}, additionalProperties:false, cited_measurements
  minItems:1.
- recommendations_contract.py: deterministic projection of the three Phase 2
  card arrays into the normalized shape, validated against the literal schema
  file via jsonschema (not a hand-rolled mirror — avoids the drift ADR 0001
  warned about). Derived/additive: never overrides Phase 1 (invariant #1);
  admits only cited cards (invariant #2); unit/range are nullable best-effort
  since the static Live 12 catalogue carries no min/max (invariant #4).
- server.py: attaches the validated envelope to the producer_summary
  interpretation result; it travels into GET /api/analysis-runs/{run_id} under
  stages.interpretation.result.recommendations (degrades to absent on error).
- interpretation.ts: TS mirror (RecommendationsContract).
- ADR 0003 + CLAUDE.md index document the contract and its semver/compat policy.

Tests: test_recommendations_contract.py (schema validity, projection-validates,
round-trip, freeze), a server wiring test, and a runtime snapshot-survival test.
Full backend suite green (1153), tsc --noEmit green. jsonschema==4.26.0 pinned.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jul 4, 2026
#204)

Behavior-neutral: analyze_time_signature computed per-candidate accent
evidence (dominance + per-bar-position onset means for bar lengths 3-7)
and threw it away. Now it rides the payload as an additive, full-mode-only
top-level field, strongest candidate first, empty on every fallback branch:

  timeSignatureCandidates: [{timeSignature, dominance, positionMeans[]}]

fundamentalsQuality.domains.meter.evidence gains {bestCandidate, margin,
candidateCount} derived from it, so the trust layer can say HOW ambiguous
the meter read was rather than just that it was ambiguous. This is the
evidence surface the meter improvement track (accuracy program Phases
B/C — the measured weak layer) builds on; downstream must never treat a
candidate as an override of timeSignature (invariant #1).

Contract mirrors (tripwire #4): JSON_SCHEMA.md inventory + full-only list
+ field table; server_phase1._build_phase1 forwarding (caught by the
golden's forwarding guard); src/types/measurement.ts +
backendPhase1Client.ts reconstructor (drops malformed entries);
phase1FullPayload fixture; golden re-baselined (new key, structural).

Tests: meter-evidence summary (backend), parser filtering (Vitest),
full-mode key set. Backend suite 1295 OK; fundamentals gate 15/15;
frontend verify green (877+ unit, 51 smoke).


Claude-Session: https://claude.ai/code/session_01T4wfz87k6kzJqkKLKZE7YL

Co-authored-by: Claude <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jul 13, 2026
…acy PR-G3)

Adds bpmOctaveEvidence (full-mode only): simple-ratio candidates
(1:2, 2:3, 1:1, 3:2, 2:1 of the shipped bpm, clamped 40-220) scored
against two inter-onset streams — low-band kick articulation (integer
multiples, fit divided by the median multiple) plus the full-band
hat/snare grid (admits the 8th-note 0.5 multiple), which pins the
notated tempo when the kick only marks bars. Never overrides bpm
(invariant #1); abstains (null) on sparse/beatless material.

Measured on the corpus: recovers all three broken octave cases
(174 -> 173.8, 190 -> 189.8, halftime-174 -> 175.5 via 3:2) with zero
false alarms on the 10 correctly-shipped rhythm clips; the beatless
ambient clip abstains. Promoted as an active tempoOctave:preferredBpm
gate on 14 rhythm clips (13 pass; grid_7_8_140 stays informational —
its bpm is meter-smeared, not octave-wrong: PR-G4 territory).

Both sides landed: JSON_SCHEMA.md + full-only docstring, measurement.ts
types, backendPhase1Client reconstructor, phase1FullPayload fixture;
golden deliberately re-baselined (bpmOctaveEvidence key + one
rounding-step lufsShortTermMax nudge within designed tolerance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jul 13, 2026
…acy PR-G3)

Adds bpmOctaveEvidence (full-mode only): simple-ratio candidates
(1:2, 2:3, 1:1, 3:2, 2:1 of the shipped bpm, clamped 40-220) scored
against two inter-onset streams — low-band kick articulation (integer
multiples, fit divided by the median multiple) plus the full-band
hat/snare grid (admits the 8th-note 0.5 multiple), which pins the
notated tempo when the kick only marks bars. Never overrides bpm
(invariant #1); abstains (null) on sparse/beatless material.

Measured on the corpus: recovers all three broken octave cases
(174 -> 173.8, 190 -> 189.8, halftime-174 -> 175.5 via 3:2) with zero
false alarms on the 10 correctly-shipped rhythm clips; the beatless
ambient clip abstains. Promoted as an active tempoOctave:preferredBpm
gate on 14 rhythm clips (13 pass; grid_7_8_140 stays informational —
its bpm is meter-smeared, not octave-wrong: PR-G4 territory).

Both sides landed: JSON_SCHEMA.md + full-only docstring, measurement.ts
types, backendPhase1Client reconstructor, phase1FullPayload fixture;
golden deliberately re-baselined (bpmOctaveEvidence key + one
rounding-step lufsShortTermMax nudge within designed tolerance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jul 13, 2026
…(PR-G4) (#213)

* feat(rhythm): surfacing-only tempo-octave evidence at extremes (accuracy PR-G3)

Adds bpmOctaveEvidence (full-mode only): simple-ratio candidates
(1:2, 2:3, 1:1, 3:2, 2:1 of the shipped bpm, clamped 40-220) scored
against two inter-onset streams — low-band kick articulation (integer
multiples, fit divided by the median multiple) plus the full-band
hat/snare grid (admits the 8th-note 0.5 multiple), which pins the
notated tempo when the kick only marks bars. Never overrides bpm
(invariant #1); abstains (null) on sparse/beatless material.

Measured on the corpus: recovers all three broken octave cases
(174 -> 173.8, 190 -> 189.8, halftime-174 -> 175.5 via 3:2) with zero
false alarms on the 10 correctly-shipped rhythm clips; the beatless
ambient clip abstains. Promoted as an active tempoOctave:preferredBpm
gate on 14 rhythm clips (13 pass; grid_7_8_140 stays informational —
its bpm is meter-smeared, not octave-wrong: PR-G4 territory).

Both sides landed: JSON_SCHEMA.md + full-only docstring, measurement.ts
types, backendPhase1Client reconstructor, phase1FullPayload fixture;
golden deliberately re-baselined (bpmOctaveEvidence key + one
rounding-step lufsShortTermMax nudge within designed tolerance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): wire bpmOctaveEvidence into server_phase1 forwarder and audio-fixture key set

Two omissions from PR-G3 that CI caught on the rebased branch:
- server_phase1._build_phase1 did not forward bpmOctaveEvidence to the
  frontend (test_phase1_golden.BuildPhase1KeySupersetTests caught it)
- EXPECTED_TOP_LEVEL_KEYS in test_audio_fixture.py did not include it
  (AudioFixtureSmokeTest.test_output_contains_all_expected_keys caught it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCtxwnhn8nzHBjzeehPtf8

* feat(meter): loudness-accent stream for meter + downbeats on non-4/4 (accuracy PR-G4)

The onset-count accent stream is loudness-blind: a kick on every beat
yields identical counts at every bar position regardless of which beat
is accented — measured on the odd-meter corpus clips as 3/4 edging 4/4
by 0.8%, shipping 4/4 at confidence 0.03. analyze_time_signature now
folds the per-beat low-band loudness (beat_data, hoisted above the
meter call) at each candidate bar length as a second accent stream:
max-over-phase dominance, clamped at 10, neutral under 4 bars, harmonic
folds collapsed by echo PROMINENCE (0.262 true-3/4 vs 0.000 true-6/8
measured; raw echo/peak is ~0.72 for both). Decision runs on the
count x loudness product with a 15% override margin when loudness
contributed; count-only callers (including --fast) keep the 20% margin
byte-identical.

Burn-down: 3/4, 6/8, 7/8 meter AND downbeats (F1 0.875 each) flip from
knownGaps to gating checks; all ten 4/4 rhythm clips (incl. broken-kick
2-step/halftime/breakbeat) unchanged. Remaining gaps are all
tempo-family. timeSignatureCandidates entries gain an additive
loudnessDominance key, landed on both sides; golden passes without
re-baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jul 13, 2026
…R-G5) (#214)

* feat(rhythm): surfacing-only tempo-octave evidence at extremes (accuracy PR-G3)

Adds bpmOctaveEvidence (full-mode only): simple-ratio candidates
(1:2, 2:3, 1:1, 3:2, 2:1 of the shipped bpm, clamped 40-220) scored
against two inter-onset streams — low-band kick articulation (integer
multiples, fit divided by the median multiple) plus the full-band
hat/snare grid (admits the 8th-note 0.5 multiple), which pins the
notated tempo when the kick only marks bars. Never overrides bpm
(invariant #1); abstains (null) on sparse/beatless material.

Measured on the corpus: recovers all three broken octave cases
(174 -> 173.8, 190 -> 189.8, halftime-174 -> 175.5 via 3:2) with zero
false alarms on the 10 correctly-shipped rhythm clips; the beatless
ambient clip abstains. Promoted as an active tempoOctave:preferredBpm
gate on 14 rhythm clips (13 pass; grid_7_8_140 stays informational —
its bpm is meter-smeared, not octave-wrong: PR-G4 territory).

Both sides landed: JSON_SCHEMA.md + full-only docstring, measurement.ts
types, backendPhase1Client reconstructor, phase1FullPayload fixture;
golden deliberately re-baselined (bpmOctaveEvidence key + one
rounding-step lufsShortTermMax nudge within designed tolerance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): wire bpmOctaveEvidence into server_phase1 forwarder and audio-fixture key set

Two omissions from PR-G3 that CI caught on the rebased branch:
- server_phase1._build_phase1 did not forward bpmOctaveEvidence to the
  frontend (test_phase1_golden.BuildPhase1KeySupersetTests caught it)
- EXPECTED_TOP_LEVEL_KEYS in test_audio_fixture.py did not include it
  (AudioFixtureSmokeTest.test_output_contains_all_expected_keys caught it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCtxwnhn8nzHBjzeehPtf8

* feat(meter): loudness-accent stream for meter + downbeats on non-4/4 (accuracy PR-G4)

The onset-count accent stream is loudness-blind: a kick on every beat
yields identical counts at every bar position regardless of which beat
is accented — measured on the odd-meter corpus clips as 3/4 edging 4/4
by 0.8%, shipping 4/4 at confidence 0.03. analyze_time_signature now
folds the per-beat low-band loudness (beat_data, hoisted above the
meter call) at each candidate bar length as a second accent stream:
max-over-phase dominance, clamped at 10, neutral under 4 bars, harmonic
folds collapsed by echo PROMINENCE (0.262 true-3/4 vs 0.000 true-6/8
measured; raw echo/peak is ~0.72 for both). Decision runs on the
count x loudness product with a 15% override margin when loudness
contributed; count-only callers (including --fast) keep the 20% margin
byte-identical.

Burn-down: 3/4, 6/8, 7/8 meter AND downbeats (F1 0.875 each) flip from
knownGaps to gating checks; all ten 4/4 rhythm clips (incl. broken-kick
2-step/halftime/breakbeat) unchanged. Remaining gaps are all
tempo-family. timeSignatureCandidates entries gain an additive
loudnessDominance key, landed on both sides; golden passes without
re-baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(rhythm): detect 16th-grid shuffle in the swing measurement (accuracy PR-G5)

compute_swing_detail probes the 16th grid when the 8th grid reads
straight or absent: same long/short-split logic at half scale (straight
center 0.25, thresholds 0.265/0.235). UKG/2-step shuffle lives on the
16ths and was previously invisible. 8th-swung material keeps priority
and is byte-identical; straight material cannot fabricate a shuffle (no
split at 0.25); sparse material still abstains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(eval): gate 16th-shuffle DETECTION; document the ratio-value frame-resolution gap (PR-G5)

New swing:gridResolution harness check (expected.swingGrid) promotes
shuffle detection to an active gate on shuffle16_130_62 — swingDetail
now reads swung/16th/conf 0.992 where it read None. The ratio value
stays a knownGap with its measured diagnosis: onset detection
quantizes to 11.6ms hops with a one-frame-per-side inward bias,
compressing 62% shuffle to a 55% read (verified not a fixture
artifact: a 50ms closed hat renders byte-identical medians). Value
correction without a gate would repeat the confidently-wrong pattern;
it stays a future, separately gated onset-timing refinement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <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