Skip to content

Add phase1 visual story v2 deck assets and build scripts - #2

Merged
slittycode merged 1 commit into
mainfrom
codex/fix-spectral-detail-and-chord
Mar 19, 2026
Merged

Add phase1 visual story v2 deck assets and build scripts#2
slittycode merged 1 commit into
mainfrom
codex/fix-spectral-detail-and-chord

Conversation

@slittycode

Copy link
Copy Markdown
Owner

@slittycode review

@slittycode
slittycode merged commit 519659c into main Mar 19, 2026
0 of 2 checks passed
@slittycode
slittycode deleted the codex/fix-spectral-detail-and-chord branch March 19, 2026 10:16
@slittycode
slittycode restored the codex/fix-spectral-detail-and-chord branch March 27, 2026 07:12
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
slittycode deleted the codex/fix-spectral-detail-and-chord branch May 20, 2026 07:12
slittycode added a commit that referenced this pull request May 28, 2026
Adds GET /api/analysis-runs/{run_id}/transcription/pianoroll, which
renders the pitch-note translation stage's transcriptionDetail as a
velocity-encoded (pitch, time) uint8 matrix for a UI heatmap surface.

- New symusic==0.6.0 dep (cp311 wheels for darwin-arm64 + manylinux).
- transcription_pianoroll.py: pure module mapping Phase 1 transcription
  notes → symusic.Score → pianoroll matrix. 38 unit tests, ~2 ms total.
- Route handler in server.py with structured error codes: distinguishes
  TRANSCRIPTION_NOT_REQUESTED (404), TRANSCRIPTION_NOT_COMPLETED (409),
  and TRANSCRIPTION_NOT_AVAILABLE (404) so the UI can pick the right
  affordance per state. 10 route tests cover the status mapping +
  validation.
- Derived view; Phase 1 stays authoritative. Response cites bpm +
  timeSignature for chain of custody (PURPOSE invariant #2).
- ARCHITECTURE.md + CLAUDE.md updated; transcriptionDetail location
  note added (stripped from measurement.result, lives in the
  pitchNoteTranslation stage instead).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
slittycode added a commit that referenced this pull request May 29, 2026
Renders the new transcription pianoroll endpoint (eac9bed / PR-A.2) as
a canvas heatmap below the existing Session Musician panel. Only mounts
when transcriptionDetail has at least one note.

- transcriptionPianorollClient.ts: typed transport for
  GET /api/analysis-runs/{run_id}/transcription/pianoroll with
  AbortSignal propagation. 8 vitest cases cover URL construction,
  camelCase query encoding, payload typing, and BackendClientError
  for 4xx/5xx (incl. TRANSCRIPTION_NOT_COMPLETED, NOT_AVAILABLE).
- TranscriptionPianoroll.tsx: canvas heatmap mirroring the
  ChromaHeatmap pattern. Velocity → cyan-to-yellow ramp; lowest pitch
  at the bottom (Ableton convention); octave-C labels on the left.
  Cites mode + note count + Phase 1 BPM + time signature in the
  header so every cell traces back to a measurement (PURPOSE
  invariant #2).
- TranscriptionPianorollBlock.tsx: fetch container with
  loading / error / success states. Surfaces the backend error code
  in the error view so testers can correlate to the server envelope.
- AnalysisResults.tsx: mounts the block inside the existing
  section-musician-suite, gated on apiBaseUrl && runId &&
  transcriptionDetail.noteCount > 0.

693 / 693 vitest UI tests pass (5 s). tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
slittycode added a commit that referenced this pull request May 29, 2026
* refactor(backend): migrate sample_synthesis.write_midi to symusic

Drops pretty_midi from the sample synthesis MIDI-write path; symusic
becomes the single canonical MIDI library on the backend. The .mid file
is a spec-conformant Standard MIDI file that DAWs parse identically, so
the change is non-audible — the existing parity gate in
tests/test_sample_synthesis.WriteMidiTests reads the symusic output back
via pretty_midi and confirms the notes survive the round trip.

Module docstring updated to call out the new writer + the parity
contract for the next maintainer.

919 / 919 backend unittests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(backend): migrate research MIDI parsers off pretty_midi to symusic

Research-only path: polyphonic_evaluation.summarize_midi_file and
scripts/import_midi_to_ground_truth.py now parse MIDI via
symusic.Score(path).to("Second") instead of pretty_midi.PrettyMIDI.
Test fixture writers in test_polyphonic_evaluation.py and
test_phase1_evaluation_transcription.py likewise build their tiny .mid
inputs through symusic.

Drops three pretty_midi importers; only test_sample_synthesis.py keeps
pretty_midi as the deliberate parity reader for write_midi's output.

919 / 919 backend unittests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(backend): drop PR-E (symusic-beat 4th method) — out of scope

The consolidation plan called for adding symusic-beat as an observational
fourth method in beat_evaluation.py. Probing symusic's API shows
get_beats / get_downbeats operate on symbolic Score tempo maps, not on
audio — symusic is not an audio-analysis library and has nothing to
offer this harness, which takes audio paths.

Recording the rationale in the file's header docstring so a future
maintainer doesn't re-do the same investigation. The pre-registered
beat_this vs kick_accent gate (ADOPT_MARGIN = 0.10) is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(backend): add symusic.Synthesizer audio path with env-flag backend select

Sample synthesis now has three render backends — symusic (Prestosynth),
FluidSynth (existing), and the sine fallback — picked by:

- ASA_SAMPLE_SYNTH_BACKEND env var (auto|symusic|fluidsynth|sine), so
  operators have a code-free escape hatch if any backend regresses in
  the wild. Default "auto" prefers symusic when a soundfont is locatable,
  drops to FluidSynth if symusic fails, and falls through to sine when
  no soundfont is reachable. Unknown env values log a warning and treat
  the request as "auto".
- _resolve_backend() captures the precedence in one place. The new
  BackendResolutionTests cover all combinations (8 tests) — pure logic,
  no synthesis, no soundfont required.

Implementation details:

- New _render_with_symusic_synth(plan, sf_path) renders via
  Synthesizer(sf_path).render(score, stereo=False), returning the same
  float32 mono buffer shape as the other two backends.
- _build_score_from_plan extracted from write_midi so the symusic synth
  path and the MIDI emitter share one plan-to-Score translation.
- Backend literal extended: "symusic" | "fluidsynth" | "sine_fallback".
  RenderResult.backend already flows into the citation manifest so
  audition samples carry which synth produced them.
- Refuses to auto-download symusic's built-in MuseScore SF3 — the
  network call is hidden from the request path. Operators who want the
  built-in must point SONIC_ANALYZER_SOUNDFONT at it explicitly.
- prefer_fluidsynth kwarg kept as the test escape hatch (semantics:
  "allow soundfont-based backends") so existing tests pass unchanged.

13/13 sample_synthesis tests pass. The pre-existing test_cleanup
failure on this branch (unrelated MT3 WIP added a 5th startup task
without updating the cleanup test's assertion) is documented in the
working tree but is NOT caused by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(backend): drop pretty_midi + mido, complete symusic consolidation

The campaign began in PR-A (transcription pianoroll module + dep) and now
ends with symusic as the only MIDI library on the backend.

Three load-bearing changes here:

1. analyze_rhythm: the melody MIDI exporter migrates from mido to symusic.
   The old code manually built ticks_per_beat math + note_on/note_off
   events; symusic takes a Score in seconds and dumps a spec-conformant
   .mid. melodyDetail.midiFile (the field consumers read) is unchanged —
   only the bytes inside the file differ, and DAWs parse them identically.

2. test_sample_synthesis.WriteMidiTests: the parity reader was pretty_midi.
   It's now symusic itself — a weaker contract ("symusic reads its own
   output"), but it still verifies the file is a valid Standard MIDI file
   and that pitches, onsets, and durations survive the round trip. Added
   onset/duration assertions to harden the contract slightly.

3. requirements.txt: pretty_midi and mido removed. mido was already
   transitive-only (no direct imports outside this migration's targets),
   and pretty_midi's importers were:
     - sample_synthesis.py        (PR-C)
     - polyphonic_evaluation.py   (PR-D)
     - import_midi_to_ground_truth.py (PR-D)
     - test_polyphonic_evaluation.py  (PR-D)
     - test_phase1_evaluation_transcription.py (PR-D)
     - sample_synthesis::write_midi parity reader (this PR)
   All migrated. ./venv/bin/pip uninstall confirmed nothing else broke.

The pre-existing test_cleanup failure (MT3 WIP added a 5th create_task on
startup without updating the test's assertion) is unrelated to this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(backend): address PR-F gaps surfaced in honesty pass

Issues called out and now fixed:

1. Conservative auto precedence — FluidSynth wins under ASA_SAMPLE_SYNTH_BACKEND=auto
   when both the binding and a soundfont are reachable. Symusic only kicks
   in under auto when FluidSynth is not importable. Cross-backend audio
   parity was never measured, so silently flipping the engine for operators
   with working FluidSynth setups was a regression risk. They opt into the
   faster Prestosynth path explicitly with ASA_SAMPLE_SYNTH_BACKEND=symusic.

2. Kwarg name — render_clip's prefer_fluidsynth had been reinterpreted as
   "allow soundfont-based backends" with the rename of FluidSynth out of
   exclusive control. Renamed to allow_soundfont_backends across the call
   chain (render_clip → generate_samples → generate_and_register_samples)
   plus every test caller. No HTTP route signatures touched — purely
   internal API. Future maintainers reading the kwarg see what it does.

3. Symusic render path coverage — _render_with_symusic_synth was pragma:
   no-cover'd because no soundfont is reachable in CI. Added
   SymusicRenderPathTests:
   - test_render_with_symusic_synth_builds_score_and_returns_mono_float32:
     mocks Synthesizer, verifies the Score is built correctly + the call
     signature matches the documented contract.
   - test_render_with_symusic_synth_reduces_stereo_to_mono: defensive
     coverage for the 2D-buffer fallback.
   - test_symusic_synth_produces_nonzero_audio_with_real_soundfont:
     @skipUnless integration test that actually invokes Prestosynth when
     SONIC_ANALYZER_SOUNDFONT (or a known location) provides one.

4. PR-G parity strengthening — WriteMidiTests now does a layered check:
   library-independent (raw bytes must start with MThd and contain MTrk)
   followed by within-library symusic round-trip. The byte-level check
   catches the "symusic writes something only it can read" failure mode
   that worried us when pretty_midi was dropped as the cross-library
   reader.

5. Dead code — symusic_synth_available() (added in PR-F, never used)
   removed.

6. BackendResolutionTests rewritten to lock down the new conservative
   precedence: tests now patch _FLUIDSYNTH_IMPORTABLE explicitly so the
   assertions don't silently re-pass under whatever happens to be
   installed locally.

936 / 936 backend unittests pass (3 skipped: 2 pre-existing + 1 new
@skipUnless integration test).

PR-E (out of scope — symusic isn't an audio-analysis library) stays
dropped with the rationale in beat_evaluation.py's header docstring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ui): add transcription pianoroll heatmap to Session Musician suite

Renders the new transcription pianoroll endpoint (eac9bed / PR-A.2) as
a canvas heatmap below the existing Session Musician panel. Only mounts
when transcriptionDetail has at least one note.

- transcriptionPianorollClient.ts: typed transport for
  GET /api/analysis-runs/{run_id}/transcription/pianoroll with
  AbortSignal propagation. 8 vitest cases cover URL construction,
  camelCase query encoding, payload typing, and BackendClientError
  for 4xx/5xx (incl. TRANSCRIPTION_NOT_COMPLETED, NOT_AVAILABLE).
- TranscriptionPianoroll.tsx: canvas heatmap mirroring the
  ChromaHeatmap pattern. Velocity → cyan-to-yellow ramp; lowest pitch
  at the bottom (Ableton convention); octave-C labels on the left.
  Cites mode + note count + Phase 1 BPM + time signature in the
  header so every cell traces back to a measurement (PURPOSE
  invariant #2).
- TranscriptionPianorollBlock.tsx: fetch container with
  loading / error / success states. Surfaces the backend error code
  in the error view so testers can correlate to the server envelope.
- AnalysisResults.tsx: mounts the block inside the existing
  section-musician-suite, gated on apiBaseUrl && runId &&
  transcriptionDetail.noteCount > 0.

693 / 693 vitest UI tests pass (5 s). tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(backend): read transcriptionDetail unwrapped in pianoroll route

The pitch-note worker stores the transcriptionDetail dict directly as
the pitchNoteTranslation stage result (complete_pitch_note_attempt(
result=transcription_detail)), but the pianoroll route read
pn_result.get("transcriptionDetail") — a nested key that never exists
in production — so the endpoint returned 404 TRANSCRIPTION_NOT_AVAILABLE
for every real run. The route tests masked the bug by feeding a wrapped
{"transcriptionDetail": {...}} fixture that did not match the worker.

- server.py: read the stage result as the unwrapped transcriptionDetail,
  with defensive tolerance for a legacy wrapper, and require a notes list
  so a null/foreign dict still 404s.
- test_server.py: unwrap _transcription_result so the happy-path test
  exercises the production shape (and would now catch a regression).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(backend): add FluidSynth vs symusic synth parity probe

Off-path maintainer script for PR-F: before anyone flips the auto
sample-synthesis default away from FluidSynth, this renders the same
deterministic C-major triad ClipPlan through both backends on one
SoundFont and compares RMS, peak, and spectral centroid against loose
"audible parity" tolerances (1 dB / 2 dB / 200 Hz).

Skips cleanly (exit 0) when pyfluidsynth or a SoundFont is unavailable,
so it is safe to run anywhere; writes a JSON verdict to
.runtime/parity/synth_parity.json. Passing parity does not itself flip
the default — that stays a deliberate, separate change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ui): add transcription pianoroll smoke coverage (PR-B)

Drives the real upload→run→results flow with a mocked backend and
asserts the Session Musician pianoroll surface end to end:

- Success: the block mounts when the run snapshot's pitchNoteTranslation
  result carries noteCount > 0, fetches the canonical sub-resource
  GET /api/analysis-runs/{run_id}/transcription/pianoroll, and paints the
  canvas heatmap with a citation that echoes Phase 1's BPM + time
  signature (chain of custody on the surface).
- Error: a 409 TRANSCRIPTION_NOT_COMPLETED envelope surfaces visually
  with the structured backend code attached for correlation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
slittycode pushed a commit that referenced this pull request Jun 2, 2026
Addresses the PR #130 review:
- mapSubLayer now routes osc_2_transpose (−1 octave) and osc_2_level through
  applyCitation with spectralBalance.subBass provenance, so every osc_2 value
  in the preset appears in the manifest (invariant #2 completeness) rather than
  only osc_2_on. Test asserts the new citations.
- Removed the redundant header "Download .vital" link from PatchSmithPanel;
  the body primary CTA is the single download affordance.

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

* WS4: patchSmith — downloadable Vital (.vital) presets with cited params

Turns measured synthesis character into a loadable Vital preset where every
parameter cites the Phase 1 measurement behind it (PURPOSE.md invariant #2)
and weak/absent evidence is disclosed rather than guessed (invariant #4).

apps/ui/src/services/patchSmith.ts (pure, deterministic):
- buildPatch(phase1) maps measurements -> Vital controls, each with a
  PatchParameterCitation {label, vitalParam, value, display, phase1Fields,
  rationale, confidence}:
    * supersawDetail.voiceCount/avgDetuneCents -> osc_1 unison voices + detune
    * spectralBalance.subBass (strong)         -> octave-down osc_2 sub layer
    * acidDetail.* + spectralBalance brightness -> resonant filter + cutoff
    * bassDetail.averageDecayMs/type           -> amp envelope decay/sustain
  No measurement -> no bound parameter. Low-confidence/absent sources are
  skipped or defaulted with an explicit hedge; overall confidence = worst band.
- serializeVital(): override-only settings JSON. Confirmed against Vital's
  load_save.cpp that missing wavetables/modulations/lfos arrays load on init
  defaults (no fragile base64 wavetable embedding). Unit mappers encode Vital's
  real ValueDetails (env seconds = value^4; cutoff semitone = 69+12log2(f/440)).

apps/ui/src/components/PatchSmithPanel.tsx: a DeviceRack "Generate Vital patch"
panel (mounted in AnalysisResults) rendering the cited-parameter manifest,
per-param + overall confidence pills, hedges, and the .vital download.

Tests (14): deterministic mapping, citation provenance, hedging on
low-confidence/empty inputs, the unit mappers, and a .vital re-parse asserting
the JSON shape + in-range control values (the machine-verifiable gate).

https://claude.ai/code/session_01Wq1vE1D7o3W9xZKSHXz43Q

* patchSmith review: cite sub-osc values, drop redundant download button

Addresses the PR #130 review:
- mapSubLayer now routes osc_2_transpose (−1 octave) and osc_2_level through
  applyCitation with spectralBalance.subBass provenance, so every osc_2 value
  in the preset appears in the manifest (invariant #2 completeness) rather than
  only osc_2_on. Test asserts the new citations.
- Removed the redundant header "Download .vital" link from PatchSmithPanel;
  the body primary CTA is the single download affordance.

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
…ard (#135)

Two defects surfaced by an independent review of the loudness-v2 program
(#128#133):

patchSmith read spectralBalance bands as "dB above/below the mix average", but
the analyzer emits absolute band energy (10·log10(mean_energy), ~ -100..0). On
real tracks the sub oscillator never engaged (guard subBass<=1.0 against a real
~ -41 dB reading) and the acid cutoff pinned to the 200 Hz floor, while the
cited rationale misstated the measurement (invariant #2). Fix locally: add
meanBandDb() and read each band as a prominence relative to the 7-band mix
average — the analyzer contract is unchanged (mixDoctor/dashboard rely on the
absolute scale). Rationales are now true; tests use realistic absolute dB with
a regression case that fails on the old logic.

phase1Version was emitted but never consumed, so persisted pre-#128 runs
(stored verbatim in analysis_runtime, never migrated) replayed v1-unit values
into v2 consumers: a linear truePeak of 0.98 read as a +0.98 dBTP over (false
TRUE_PEAK_OVER / MISSING_LOUDNESS_ACTION) and a raw bpmConfidence of 3.2
rendered as 320%. Add _migrate_v1_loudness_units() at the top of _build_phase1
(the single chokepoint every persisted snapshot passes through): a payload
lacking "phase1.v2" gets truePeak linear→dBTP, bpmConfidence raw→0-1, plr
dropped and recomputed, then stamped v2. No-op for v2 payloads.

Verified: tsc clean; 750 frontend unit tests; vite build; 1102 backend
unittests (skipped=3). Playwright smoke could not run (chromium download
blocked by the sandbox network allowlist); both changes are pure logic covered
by the suites that ran.

https://claude.ai/code/session_01C2VkJk4JAUGtz3b9BWqVGE

Co-authored-by: Claude <noreply@anthropic.com>
slittycode pushed a commit that referenced this pull request Jun 4, 2026
The initial entry incorrectly said the preset builder uses Phase 2
recommendations. patchSmith.ts imports only Phase1Result — every
parameter cites a Phase 1 field per PURPOSE.md invariant #2. Correct
the description to reflect that.

Caught by review on PR #142.

https://claude.ai/code/session_011v17uVcfV8rM9EQpRn72Ra
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 Jun 5, 2026
* docs: sync AGENTS.md files against post-#141 codebase

Two files drifted when the separation backend landed in #141 (which updated
CLAUDE.md but not the per-app AGENTS.md overlays) and when patchSmith landed
in #130.

apps/backend/AGENTS.md:
- Add separation_backend.py entry (selectable Demucs ↔ MSST backend)
- Add separation_ab.py entry (research-only A/B harness)
- Add tests/test_separation_backend.py entry
- Add msst_separate_runner.py and ab_separation_backends.py to the
  Operator and Research Scripts section

apps/ui/AGENTS.md:
- Add patchSmith.ts entry (Phase 3 Vital preset generation service)

No code changes; CLAUDE.md was already complete after #141.

https://claude.ai/code/session_011v17uVcfV8rM9EQpRn72Ra

* docs: fix patchSmith.ts description in apps/ui/AGENTS.md

The initial entry incorrectly said the preset builder uses Phase 2
recommendations. patchSmith.ts imports only Phase1Result — every
parameter cites a Phase 1 field per PURPOSE.md invariant #2. Correct
the description to reflect that.

Caught by review on PR #142.

https://claude.ai/code/session_011v17uVcfV8rM9EQpRn72Ra

---------

Co-authored-by: Claude <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jun 10, 2026
…bleton + the rec-proof harness (#156)

The ASA -> asa-ableton boundary (and the GOAL.md fixture loop) ran on
hand-extracted interpretation JSON: the result lives embedded in the run
snapshot, the warn-and-keep validationWarnings in a different subtree
(attempt diagnostics), and provenance in a third. asa-ableton's Gate
alpha fixture and every per-fixture phase2.json in the owner plan were
produced by snapshot surgery — an unversioned, fragile handoff.

1. phase2_export.py + GET /api/analysis-runs/{run_id}/export/phase2:
   single self-contained phase2-export.v1 envelope — the stored
   producer_summary interpretation result verbatim (incl. the frozen
   recommendations.v1 projection), the authoritative Phase 1 payload its
   citations resolve against (invariant #2 verifiable offline), the full
   validationWarnings trail, and provenance. Thin lookup-and-serve,
   csv_export.py pattern; 404 RUN_NOT_FOUND / PHASE2_EXPORT_NOT_AVAILABLE.
   Derived and read-only — exports stored state, rewrites nothing.
2. recommendation_evaluation.coerce_phase2_payload: --phase2 (and a
   fixture-dir phase2.json) now accepts either a bare Phase2Result or
   the export envelope, so the downloaded file feeds the scorer as-is.
3. docs/ASA_ABLETON_BOUNDARY.md: the cross-repo contract — file-coupled
   not code-coupled, consumer guidance (prefer recommendations.v1
   entries; dedupe is a consumer concern — Gate alpha's 60.7%-vs-50%
   skip-rate accounting; flagged != invalid), and the v1 freeze policy.

Tests: +15 (10 builder incl. envelope-key freeze, 3 route, 2 unwrap).
tests.test_server 227 OK; adjacent suites 110 OK; full discover matches
the unmodified-tree baseline exactly (env-only matplotlib/torch gaps).

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

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

* feat(backend): phase2-export.v1 — one-call handoff envelope for asa-ableton + the rec-proof harness

The ASA -> asa-ableton boundary (and the GOAL.md fixture loop) ran on
hand-extracted interpretation JSON: the result lives embedded in the run
snapshot, the warn-and-keep validationWarnings in a different subtree
(attempt diagnostics), and provenance in a third. asa-ableton's Gate
alpha fixture and every per-fixture phase2.json in the owner plan were
produced by snapshot surgery — an unversioned, fragile handoff.

1. phase2_export.py + GET /api/analysis-runs/{run_id}/export/phase2:
   single self-contained phase2-export.v1 envelope — the stored
   producer_summary interpretation result verbatim (incl. the frozen
   recommendations.v1 projection), the authoritative Phase 1 payload its
   citations resolve against (invariant #2 verifiable offline), the full
   validationWarnings trail, and provenance. Thin lookup-and-serve,
   csv_export.py pattern; 404 RUN_NOT_FOUND / PHASE2_EXPORT_NOT_AVAILABLE.
   Derived and read-only — exports stored state, rewrites nothing.
2. recommendation_evaluation.coerce_phase2_payload: --phase2 (and a
   fixture-dir phase2.json) now accepts either a bare Phase2Result or
   the export envelope, so the downloaded file feeds the scorer as-is.
3. docs/ASA_ABLETON_BOUNDARY.md: the cross-repo contract — file-coupled
   not code-coupled, consumer guidance (prefer recommendations.v1
   entries; dedupe is a consumer concern — Gate alpha's 60.7%-vs-50%
   skip-rate accounting; flagged != invalid), and the v1 freeze policy.

Tests: +15 (10 builder incl. envelope-key freeze, 3 route, 2 unwrap).
tests.test_server 227 OK; adjacent suites 110 OK; full discover matches
the unmodified-tree baseline exactly (env-only matplotlib/torch gaps).

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* feat(fixtures): re-author techno/dnb ground-truth fixtures as hard techno 145 + UKG 2-step 132

Owner confirmed genre fit for house/melodic-techno/acid but swapped the other
two recommendation-corpus fixtures to genres they actually produce:

- techno_rumble_130 -> hard_techno_rumble_145: distorted sine kick (Drum Buss
  on the kick chain, Boom 45 Hz), rumble reverb return + held sub, 145 BPM.
  Stays the pilot fixture (smallest spec, 9 device entries).
- dnb_reese_174 -> ukg_2step_shuffle_132: 2-step shuffle at 132 BPM, G minor.
  Swing lives in the committed MIDI (57% 16ths), asserted via
  grooveDetail.hihatSwing / perDrumSwing.snare / bassDetail.grooveType.
  Retires the fixture whose proxy fingerprint misread BPM half-time (174->116).

Both new fixtures ship spec-only (phase1Fingerprint: null, no fingerprint
file) — the distrusted _synthetic proxy fingerprints are retired with the old
slugs, and load_fixture tolerates the missing file (citation checks SKIP until
the real Ableton render lands). truePeak intents now use dBTP per ADR 0002
(old manifests carried stale linear targets). New MIDI clips bake correct
tempo metas and were byte-verified (the retired dnb melody encoded ~116 BPM,
not 174). ukg also ships audio_drums.mid so the shuffle answer key is
reproducible without hand-programmed groove.

Docs updated: NEEDS.md (genre confirmation resolved, inventory + build queue),
plans/owner-actions-recommendation-proof-plan.md (pilot + render order),
RECOMMENDATION_VERDICT.md (corpus-composition note; old per-fixture proxy
numbers not comparable).

Verified: evaluate_recommendations.py --fixture <both> --source baseline
(catalog-valid, zero issues), --self-test PASS, 34/34
tests.test_recommendation_evaluation.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* docs: demote abletonDevices.ts to research-only baseline (NEEDS-WIRING decision)

Owner resolved the wire-or-demote choice from NEEDS.md's dead-code finding:
the deterministic recommendation engine stays as the scored free baseline in
the GOAL.md sub-goal 3 three-source comparison but is NOT to be wired into
the product. Score-driven product improvements land on the Phase 2 provider
path instead.

Recorded in: the module header (abletonDevices.ts), the eval bridge header,
NEEDS.md (decision + the now-moot citation-emit candidate), BACKLOG.md,
RECOMMENDATION_VERDICT.md, and CLAUDE.md's Backport Candidates line.

No code changes — comments and docs only. Verified: npm run lint green,
evaluate_recommendations.py --self-test PASS.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

---------

Co-authored-by: Claude <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jun 12, 2026
…ro-Gemini-cost comparison) (#160)

* feat(backend): phase2-export.v1 — one-call handoff envelope for asa-ableton + the rec-proof harness

The ASA -> asa-ableton boundary (and the GOAL.md fixture loop) ran on
hand-extracted interpretation JSON: the result lives embedded in the run
snapshot, the warn-and-keep validationWarnings in a different subtree
(attempt diagnostics), and provenance in a third. asa-ableton's Gate
alpha fixture and every per-fixture phase2.json in the owner plan were
produced by snapshot surgery — an unversioned, fragile handoff.

1. phase2_export.py + GET /api/analysis-runs/{run_id}/export/phase2:
   single self-contained phase2-export.v1 envelope — the stored
   producer_summary interpretation result verbatim (incl. the frozen
   recommendations.v1 projection), the authoritative Phase 1 payload its
   citations resolve against (invariant #2 verifiable offline), the full
   validationWarnings trail, and provenance. Thin lookup-and-serve,
   csv_export.py pattern; 404 RUN_NOT_FOUND / PHASE2_EXPORT_NOT_AVAILABLE.
   Derived and read-only — exports stored state, rewrites nothing.
2. recommendation_evaluation.coerce_phase2_payload: --phase2 (and a
   fixture-dir phase2.json) now accepts either a bare Phase2Result or
   the export envelope, so the downloaded file feeds the scorer as-is.
3. docs/ASA_ABLETON_BOUNDARY.md: the cross-repo contract — file-coupled
   not code-coupled, consumer guidance (prefer recommendations.v1
   entries; dedupe is a consumer concern — Gate alpha's 60.7%-vs-50%
   skip-rate accounting; flagged != invalid), and the v1 freeze policy.

Tests: +15 (10 builder incl. envelope-key freeze, 3 route, 2 unwrap).
tests.test_server 227 OK; adjacent suites 110 OK; full discover matches
the unmodified-tree baseline exactly (env-only matplotlib/torch gaps).

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* feat(fixtures): re-author techno/dnb ground-truth fixtures as hard techno 145 + UKG 2-step 132

Owner confirmed genre fit for house/melodic-techno/acid but swapped the other
two recommendation-corpus fixtures to genres they actually produce:

- techno_rumble_130 -> hard_techno_rumble_145: distorted sine kick (Drum Buss
  on the kick chain, Boom 45 Hz), rumble reverb return + held sub, 145 BPM.
  Stays the pilot fixture (smallest spec, 9 device entries).
- dnb_reese_174 -> ukg_2step_shuffle_132: 2-step shuffle at 132 BPM, G minor.
  Swing lives in the committed MIDI (57% 16ths), asserted via
  grooveDetail.hihatSwing / perDrumSwing.snare / bassDetail.grooveType.
  Retires the fixture whose proxy fingerprint misread BPM half-time (174->116).

Both new fixtures ship spec-only (phase1Fingerprint: null, no fingerprint
file) — the distrusted _synthetic proxy fingerprints are retired with the old
slugs, and load_fixture tolerates the missing file (citation checks SKIP until
the real Ableton render lands). truePeak intents now use dBTP per ADR 0002
(old manifests carried stale linear targets). New MIDI clips bake correct
tempo metas and were byte-verified (the retired dnb melody encoded ~116 BPM,
not 174). ukg also ships audio_drums.mid so the shuffle answer key is
reproducible without hand-programmed groove.

Docs updated: NEEDS.md (genre confirmation resolved, inventory + build queue),
plans/owner-actions-recommendation-proof-plan.md (pilot + render order),
RECOMMENDATION_VERDICT.md (corpus-composition note; old per-fixture proxy
numbers not comparable).

Verified: evaluate_recommendations.py --fixture <both> --source baseline
(catalog-valid, zero issues), --self-test PASS, 34/34
tests.test_recommendation_evaluation.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* docs: demote abletonDevices.ts to research-only baseline (NEEDS-WIRING decision)

Owner resolved the wire-or-demote choice from NEEDS.md's dead-code finding:
the deterministic recommendation engine stays as the scored free baseline in
the GOAL.md sub-goal 3 three-source comparison but is NOT to be wired into
the product. Score-driven product improvements land on the Phase 2 provider
path instead.

Recorded in: the module header (abletonDevices.ts), the eval bridge header,
NEEDS.md (decision + the now-moot citation-emit candidate), BACKLOG.md,
RECOMMENDATION_VERDICT.md, and CLAUDE.md's Backport Candidates line.

No code changes — comments and docs only. Verified: npm run lint green,
evaluate_recommendations.py --self-test PASS.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* feat(eval): add claude source to the recommendation harness + generation script

--source claude in evaluate_recommendations.py scores a stored Phase2Result
from the Claude CLI provider (sibling phase2.claude.json, same ingestion as
the gemini source). scripts/gen_claude_phase2.py produces those files at zero
Gemini cost: it runs each fixture's stored Phase 1 fingerprint through the
exact server path (server._run_interpretation_request) with
ASA_PHASE2_PROVIDER=claude, so output flows the identical parse/citation/
catalogue/recommendations.v1 validation tail as Gemini.

Research-only (mirrors the evaluate_* convention); deleting both restores the
product exactly. Verified: --self-test PASS; --source claude SKIPs cleanly
when no phase2.claude.json exists.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* feat(eval): Claude-provider Phase 2 evidence for acid + house proxy fixtures

Generated via scripts/gen_claude_phase2.py through the exact server path
(ASA_PHASE2_PROVIDER=claude, model sonnet, MAX_THINKING_TOKENS=0): both fully
cited (custody penalty 1.000), zero validation warnings, 31 recommendations.v1
envelope entries each.

Scored with --source claude against the same proxy fingerprints the recorded
Gemini numbers used: acid 0.485 (Gemini 0.172), house 0.424 (Gemini 0.343).
melodic_techno_arp_124 evidence + the verdict write-up follow.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* feat(eval): melodic_techno Claude evidence + provider head-to-head verdict

Completes the zero-Gemini-cost Claude scoring on the proxy corpus:

- melodic_techno_arp_124 phase2.claude.json: 13 rec cards, fully cited, zero
  warnings, aggregate 0.424 — resolving the "Gemini 0 recs" outlier as
  Gemini-side (the fixture's fingerprint is interpretable).
- RECOMMENDATION_VERDICT.md: dated head-to-head section. Claude (sonnet,
  text-only) 0.485/0.424/0.424 vs recorded Gemini 0.172/0.343/0.000 on
  identical fingerprints; mean 0.444 vs 0.172 on the shared subset. Caveats
  (proxy corpus, modality asymmetry, model class) stated inline.
- NEEDS.md sub-goal 3: status updated; source list now includes claude.
- docs/PHASE2_PROVIDER.md: 2026-06-11 addendum — scoring result + operating
  notes (MAX_THINKING_TOKENS=0 for headless calls, model pinning, 300-365s
  measured latency, session-limit failure mode).
- CLAUDE.md: one-line MAX_THINKING_TOKENS note on ASA_CLAUDE_TIMEOUT_SECONDS.
- gen_claude_phase2.py: defaults MAX_THINKING_TOKENS=0 and an 1800s timeout
  (overridable) so the next run doesn't rediscover the thinking-budget trap.

Verified: --self-test PASS; full-corpus --source claude scores 3, SKIPs the
2 spec-only fixtures cleanly.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

---------

Co-authored-by: Claude <noreply@anthropic.com>
slittycode added a commit that referenced this pull request Jun 17, 2026
…d drops

* feat(backend): phase2-export.v1 — one-call handoff envelope for asa-ableton + the rec-proof harness

The ASA -> asa-ableton boundary (and the GOAL.md fixture loop) ran on
hand-extracted interpretation JSON: the result lives embedded in the run
snapshot, the warn-and-keep validationWarnings in a different subtree
(attempt diagnostics), and provenance in a third. asa-ableton's Gate
alpha fixture and every per-fixture phase2.json in the owner plan were
produced by snapshot surgery — an unversioned, fragile handoff.

1. phase2_export.py + GET /api/analysis-runs/{run_id}/export/phase2:
   single self-contained phase2-export.v1 envelope — the stored
   producer_summary interpretation result verbatim (incl. the frozen
   recommendations.v1 projection), the authoritative Phase 1 payload its
   citations resolve against (invariant #2 verifiable offline), the full
   validationWarnings trail, and provenance. Thin lookup-and-serve,
   csv_export.py pattern; 404 RUN_NOT_FOUND / PHASE2_EXPORT_NOT_AVAILABLE.
   Derived and read-only — exports stored state, rewrites nothing.
2. recommendation_evaluation.coerce_phase2_payload: --phase2 (and a
   fixture-dir phase2.json) now accepts either a bare Phase2Result or
   the export envelope, so the downloaded file feeds the scorer as-is.
3. docs/ASA_ABLETON_BOUNDARY.md: the cross-repo contract — file-coupled
   not code-coupled, consumer guidance (prefer recommendations.v1
   entries; dedupe is a consumer concern — Gate alpha's 60.7%-vs-50%
   skip-rate accounting; flagged != invalid), and the v1 freeze policy.

Tests: +15 (10 builder incl. envelope-key freeze, 3 route, 2 unwrap).
tests.test_server 227 OK; adjacent suites 110 OK; full discover matches
the unmodified-tree baseline exactly (env-only matplotlib/torch gaps).

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* feat(fixtures): re-author techno/dnb ground-truth fixtures as hard techno 145 + UKG 2-step 132

Owner confirmed genre fit for house/melodic-techno/acid but swapped the other
two recommendation-corpus fixtures to genres they actually produce:

- techno_rumble_130 -> hard_techno_rumble_145: distorted sine kick (Drum Buss
  on the kick chain, Boom 45 Hz), rumble reverb return + held sub, 145 BPM.
  Stays the pilot fixture (smallest spec, 9 device entries).
- dnb_reese_174 -> ukg_2step_shuffle_132: 2-step shuffle at 132 BPM, G minor.
  Swing lives in the committed MIDI (57% 16ths), asserted via
  grooveDetail.hihatSwing / perDrumSwing.snare / bassDetail.grooveType.
  Retires the fixture whose proxy fingerprint misread BPM half-time (174->116).

Both new fixtures ship spec-only (phase1Fingerprint: null, no fingerprint
file) — the distrusted _synthetic proxy fingerprints are retired with the old
slugs, and load_fixture tolerates the missing file (citation checks SKIP until
the real Ableton render lands). truePeak intents now use dBTP per ADR 0002
(old manifests carried stale linear targets). New MIDI clips bake correct
tempo metas and were byte-verified (the retired dnb melody encoded ~116 BPM,
not 174). ukg also ships audio_drums.mid so the shuffle answer key is
reproducible without hand-programmed groove.

Docs updated: NEEDS.md (genre confirmation resolved, inventory + build queue),
plans/owner-actions-recommendation-proof-plan.md (pilot + render order),
RECOMMENDATION_VERDICT.md (corpus-composition note; old per-fixture proxy
numbers not comparable).

Verified: evaluate_recommendations.py --fixture <both> --source baseline
(catalog-valid, zero issues), --self-test PASS, 34/34
tests.test_recommendation_evaluation.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* docs: demote abletonDevices.ts to research-only baseline (NEEDS-WIRING decision)

Owner resolved the wire-or-demote choice from NEEDS.md's dead-code finding:
the deterministic recommendation engine stays as the scored free baseline in
the GOAL.md sub-goal 3 three-source comparison but is NOT to be wired into
the product. Score-driven product improvements land on the Phase 2 provider
path instead.

Recorded in: the module header (abletonDevices.ts), the eval bridge header,
NEEDS.md (decision + the now-moot citation-emit candidate), BACKLOG.md,
RECOMMENDATION_VERDICT.md, and CLAUDE.md's Backport Candidates line.

No code changes — comments and docs only. Verified: npm run lint green,
evaluate_recommendations.py --self-test PASS.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* feat(eval): add claude source to the recommendation harness + generation script

--source claude in evaluate_recommendations.py scores a stored Phase2Result
from the Claude CLI provider (sibling phase2.claude.json, same ingestion as
the gemini source). scripts/gen_claude_phase2.py produces those files at zero
Gemini cost: it runs each fixture's stored Phase 1 fingerprint through the
exact server path (server._run_interpretation_request) with
ASA_PHASE2_PROVIDER=claude, so output flows the identical parse/citation/
catalogue/recommendations.v1 validation tail as Gemini.

Research-only (mirrors the evaluate_* convention); deleting both restores the
product exactly. Verified: --self-test PASS; --source claude SKIPs cleanly
when no phase2.claude.json exists.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* feat(eval): Claude-provider Phase 2 evidence for acid + house proxy fixtures

Generated via scripts/gen_claude_phase2.py through the exact server path
(ASA_PHASE2_PROVIDER=claude, model sonnet, MAX_THINKING_TOKENS=0): both fully
cited (custody penalty 1.000), zero validation warnings, 31 recommendations.v1
envelope entries each.

Scored with --source claude against the same proxy fingerprints the recorded
Gemini numbers used: acid 0.485 (Gemini 0.172), house 0.424 (Gemini 0.343).
melodic_techno_arp_124 evidence + the verdict write-up follow.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* feat(eval): melodic_techno Claude evidence + provider head-to-head verdict

Completes the zero-Gemini-cost Claude scoring on the proxy corpus:

- melodic_techno_arp_124 phase2.claude.json: 13 rec cards, fully cited, zero
  warnings, aggregate 0.424 — resolving the "Gemini 0 recs" outlier as
  Gemini-side (the fixture's fingerprint is interpretable).
- RECOMMENDATION_VERDICT.md: dated head-to-head section. Claude (sonnet,
  text-only) 0.485/0.424/0.424 vs recorded Gemini 0.172/0.343/0.000 on
  identical fingerprints; mean 0.444 vs 0.172 on the shared subset. Caveats
  (proxy corpus, modality asymmetry, model class) stated inline.
- NEEDS.md sub-goal 3: status updated; source list now includes claude.
- docs/PHASE2_PROVIDER.md: 2026-06-11 addendum — scoring result + operating
  notes (MAX_THINKING_TOKENS=0 for headless calls, model pinning, 300-365s
  measured latency, session-limit failure mode).
- CLAUDE.md: one-line MAX_THINKING_TOKENS note on ASA_CLAUDE_TIMEOUT_SECONDS.
- gen_claude_phase2.py: defaults MAX_THINKING_TOKENS=0 and an 1800s timeout
  (overridable) so the next run doesn't rediscover the thinking-budget trap.

Verified: --self-test PASS; full-corpus --source claude scores 3, SKIPs the
2 spec-only fixtures cleanly.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* test(ui): cross-boundary citation contract guards parser field drops

The Phase 1 contract spans analyze.py output, JSON_SCHEMA.md, and types.ts
with no generated source of truth (tripwire #3). Most of parsePhase1Result
passes detail blocks through verbatim, but ~12 parseOptional* reconstructors
rebuild field-by-field — and a backend field one of them forgets to forward
is silently dropped, breaking valid Phase 2 citations to it. This is the
exact mechanism that dropped reverbDetail.preDelayMs / perBandRt60.* and the
vocalDetail stem proxies.

Add tests/services/phase1CitationContract.test.ts: feed a comprehensive
payload through the real parsePhase1Result, run the real citation walker
(collectPhase1FieldPaths) on the result, and assert every citable path
survives. Each field is populated non-null, so a path is present only if the
reconstructor carried it through — drop a field in any parseOptional* and its
assertion fails by name. Verified teeth: re-injecting the preDelayMs drop
fails exactly that one assertion and no other.

Also point tripwire #3 at the new guard.

Verified: 59 contract assertions green; full tests/services suite 809 green;
npm run lint clean.

https://claude.ai/code/session_01YB5QFBLL4nbqPSdtcnxrLy

* ci: trigger CI run for PR #163

---------

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