From 72b7fe260f09015ed87ce02dc0a0acb5e3979f00 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 04:46:40 +0000 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20tighten=20repo=20structure=20?= =?UTF-8?q?=E2=80=94=20clean=20README,=20consolidate=20docs,=20untrack=20r?= =?UTF-8?q?untime=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Goal: move the repo toward a tight, scannable shape (one canonical agent guide, one README, one place for past-tense plans). No code changes; only docs, layout, and .gitignore. Root-level changes: - README.md: rewritten as a tight intro (~80 lines) — what ASA is, layout, quickstart, doc index. Setup detail moved to docs/SETUP.md. - AGENTS.md: reduced to a pointer that defers to CLAUDE.md, so tools that look for AGENTS.md by convention (Codex, OpenHands) still find it. - CODEX.md (root + apps/*/CODEX.md): removed; content is covered by CLAUDE.md + per-app AGENTS.md overlays. - CLAUDE.md: "Companion Agent Docs" section updated to reflect the new precedence chain (PURPOSE.md > CLAUDE.md > per-app AGENTS.md). Doc consolidation: - OPTIMIZATION_PLAN.md -> docs/history/optimization-plan.md - PLAN-phase1-hardening.md -> docs/history/phase1-hardening-plan.md - advisory/phase1_audit/ -> docs/history/phase1-audit/ - docs/archive/ -> docs/history/archive/ - New docs/history/README.md explains the dir as past-tense reference only. - New docs/SETUP.md absorbs the prior README's environment, Phase 2, and upload-limit detail. Untracked binaries / runtime state: - .runtime/analysis_runs.sqlite3 — live runtime state, accidentally committed. - ASA_System_Design.docx — kept locally, gitignored. - docs/PHASE2_TRUTHFULNESS_PHASES_A_B_C.docx — kept locally, gitignored. - .gitignore: add /.runtime/ and the two .docx paths. Deduplication: - apps/backend/LICENSE and apps/ui/LICENSE removed (identical to root LICENSE). Fixed stale internal links (docs/archive → docs/history/archive) in: - CHANGELOG.md - tests/ground_truth/README.md - scripts/calibrate_confidence.py https://claude.ai/code/session_01GvD1K33Jev3vrQXRsX9WAG --- .gitignore | 5 + .runtime/analysis_runs.sqlite3 | 0 AGENTS.md | 515 +----------------- ASA_System_Design.docx | Bin 42054 -> 0 bytes CHANGELOG.md | 2 +- CLAUDE.md | 9 +- CODEX.md | 84 --- README.md | 237 ++------ apps/backend/AGENTS.md | 2 +- apps/backend/CODEX.md | 46 -- apps/backend/LICENSE | 21 - apps/ui/CODEX.md | 50 -- apps/ui/LICENSE | 21 - docs/PHASE2_TRUTHFULNESS_PHASES_A_B_C.docx | Bin 13466 -> 0 bytes docs/SETUP.md | 189 +++++++ docs/history/README.md | 18 + docs/{ => history}/archive/README.md | 0 .../confidence-calibration-results-stubs.md | 0 .../archive/phase1-hardening-plan-alt.md | 0 .../archive/refactor-state-2026-03-18.md | 0 .../stage3-reality-audit-2026-03-18.md | 0 .../history/optimization-plan.md | 0 .../phase1-audit}/.node/package-lock.json | 0 .../history/phase1-audit}/.node/package.json | 0 .../build_phase1_executive_deck.js | 0 .../history/phase1-audit}/ceo_visual_deck.md | 0 .../history/phase1-audit}/deck_build_notes.md | 0 .../history/phase1-audit}/evidence_index.md | 0 .../history/phase1-audit}/phase1_audit.md | 0 .../phase1-audit}/phase1_executive_deck.pptx | Bin .../history/phase1-audit}/phase1_flow.mmd | 0 .../history/phase1-audit}/phase1_flow_doc.md | 0 .../phase1-audit}/phase1_flow_preview.html | 0 .../build_visual_story_v2.js | 0 .../phase1_visual_story_v2/notes.md | 0 .../phase1_executive_deck_v2.pptx | Bin .../phase1_visual_story_v2.html | 0 .../previews/slide-01-core-boundary.svg.png | Bin .../slide-02-truth-vs-compatibility.svg.png | Bin .../slide-03-measurement-engine.svg.png | Bin .../previews/slide-04-duplicate-work.svg.png | Bin .../previews/slide-05-value-density.svg.png | Bin .../slide-06-resource-allocation.svg.png | Bin .../previews/slide-07-roadmap.svg.png | Bin .../slides/slide-01-core-boundary.svg | 0 .../slide-02-truth-vs-compatibility.svg | 0 .../slides/slide-03-measurement-engine.svg | 0 .../slides/slide-04-duplicate-work.svg | 0 .../slides/slide-05-value-density.svg | 0 .../slides/slide-06-resource-allocation.svg | 0 .../slides/slide-07-roadmap.svg | 0 .../work/pptxgenjs_helpers/code.js | 0 .../work/pptxgenjs_helpers/image.js | 0 .../work/pptxgenjs_helpers/index.js | 0 .../work/pptxgenjs_helpers/latex.js | 0 .../work/pptxgenjs_helpers/layout.js | 0 .../work/pptxgenjs_helpers/layout_builders.js | 0 .../work/pptxgenjs_helpers/svg.js | 0 .../work/pptxgenjs_helpers/text.js | 0 .../work/pptxgenjs_helpers/util.js | 0 .../history/phase1-audit}/pipeline_map.md | 0 .../phase1-audit}/resource_priority_matrix.md | 0 .../phase1-audit}/visual_prompt_pack.md | 0 .../history/phase1-hardening-plan.md | 0 scripts/calibrate_confidence.py | 2 +- tests/ground_truth/README.md | 2 +- 66 files changed, 276 insertions(+), 927 deletions(-) delete mode 100644 .runtime/analysis_runs.sqlite3 delete mode 100644 ASA_System_Design.docx delete mode 100644 CODEX.md delete mode 100644 apps/backend/CODEX.md delete mode 100644 apps/backend/LICENSE delete mode 100644 apps/ui/CODEX.md delete mode 100644 apps/ui/LICENSE delete mode 100644 docs/PHASE2_TRUTHFULNESS_PHASES_A_B_C.docx create mode 100644 docs/SETUP.md create mode 100644 docs/history/README.md rename docs/{ => history}/archive/README.md (100%) rename docs/{ => history}/archive/confidence-calibration-results-stubs.md (100%) rename docs/{ => history}/archive/phase1-hardening-plan-alt.md (100%) rename docs/{ => history}/archive/refactor-state-2026-03-18.md (100%) rename docs/{ => history}/archive/stage3-reality-audit-2026-03-18.md (100%) rename OPTIMIZATION_PLAN.md => docs/history/optimization-plan.md (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/.node/package-lock.json (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/.node/package.json (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/build_phase1_executive_deck.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/ceo_visual_deck.md (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/deck_build_notes.md (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/evidence_index.md (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_audit.md (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_executive_deck.pptx (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_flow.mmd (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_flow_doc.md (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_flow_preview.html (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/build_visual_story_v2.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/notes.md (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/phase1_executive_deck_v2.pptx (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/phase1_visual_story_v2.html (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/previews/slide-01-core-boundary.svg.png (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/previews/slide-02-truth-vs-compatibility.svg.png (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/previews/slide-03-measurement-engine.svg.png (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/previews/slide-04-duplicate-work.svg.png (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/previews/slide-05-value-density.svg.png (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/previews/slide-06-resource-allocation.svg.png (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/previews/slide-07-roadmap.svg.png (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/slides/slide-01-core-boundary.svg (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/slides/slide-02-truth-vs-compatibility.svg (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/slides/slide-03-measurement-engine.svg (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/slides/slide-04-duplicate-work.svg (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/slides/slide-05-value-density.svg (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/slides/slide-06-resource-allocation.svg (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/slides/slide-07-roadmap.svg (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/work/pptxgenjs_helpers/code.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/work/pptxgenjs_helpers/image.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/work/pptxgenjs_helpers/index.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/work/pptxgenjs_helpers/latex.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/work/pptxgenjs_helpers/layout.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/work/pptxgenjs_helpers/layout_builders.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/work/pptxgenjs_helpers/svg.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/work/pptxgenjs_helpers/text.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/phase1_visual_story_v2/work/pptxgenjs_helpers/util.js (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/pipeline_map.md (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/resource_priority_matrix.md (100%) rename {advisory/phase1_audit => docs/history/phase1-audit}/visual_prompt_pack.md (100%) rename PLAN-phase1-hardening.md => docs/history/phase1-hardening-plan.md (100%) diff --git a/.gitignore b/.gitignore index f6ae1e5f..987a3013 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,11 @@ apps/backend/.env # Backend runtime state (artifacts, sqlite, reports) apps/backend/.runtime/ +.runtime/ + +# Generated/binary design docs kept locally, not in git +ASA_System_Design.docx +docs/PHASE2_TRUTHFULNESS_PHASES_A_B_C.docx # UI prototypes apps/ui/redesign-concept.html diff --git a/.runtime/analysis_runs.sqlite3 b/.runtime/analysis_runs.sqlite3 deleted file mode 100644 index e69de29b..00000000 diff --git a/AGENTS.md b/AGENTS.md index a224df50..61eb9713 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,512 +1,19 @@ # AGENTS.md -This file provides guidance to AI coding agents working with the ASA (Sonic Analyzer) codebase. +Pointer for AI coding agents that look for `AGENTS.md` by convention (Codex, +OpenHands, and others). -Codex-specific instructions live in `CODEX.md` at the repo root, with app overlays in `apps/ui/CODEX.md` and `apps/backend/CODEX.md`. +**The canonical agent guidance for this repo is [`CLAUDE.md`](CLAUDE.md).** Read it first. -## Project Overview +Order of precedence when guidance conflicts: -ASA is a local audio analysis tool for music producers. It analyzes audio files to extract measurable properties (tempo, key, loudness, spectral characteristics) and provides AI-powered interpretation for arrangement advice and musical descriptions. +1. [`PURPOSE.md`](PURPOSE.md) — why ASA exists and the non-negotiable quality invariants. +2. [`CLAUDE.md`](CLAUDE.md) — canonical agent guide: commands, architecture, tripwires, change map. +3. [`docs/ARCHITECTURE_STRATEGY.md`](docs/ARCHITECTURE_STRATEGY.md) — why the three-layer design is shaped the way it is. -### Core Philosophy +App-local entry points: -The system follows a **three-layer hybrid architecture** that separates deterministic measurement from AI interpretation: +- [`apps/backend/AGENTS.md`](apps/backend/AGENTS.md) +- [`apps/ui/AGENTS.md`](apps/ui/AGENTS.md) -1. **Measurement is authoritative** - DSP results from Essentia are the system's ground truth -2. **Pitch/note translation is best-effort** - Monophonic pitch tracking on separated stems, honest about uncertainty -3. **Interpretation is contextual** - Gemini provides musical insights grounded in measurements, not replacements for them - -This split exists because frontier audio-language models (as of early 2026) still degrade on measurement tasks like BPM estimation and key detection. The hybrid approach leverages the strengths of each layer. - -In plain English: ASA can give you a usable pitch-note sketch and a melody guide, but it does not currently promise reliable full-track polyphonic audio-to-MIDI from dense mixed songs. - -### System Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ LAYER 1 — MEASUREMENT (Essentia/DSP) │ -│ Deterministic, repeatable, authoritative │ -│ BPM, LUFS, key, spectral balance, stereo, dynamics │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ LAYER 2 — PITCH/NOTE TRANSLATION (torchcrepe) │ -│ Best-effort monophonic pitch on Demucs stems │ -│ Bass + Other stems → MIDI notes with confidence │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ LAYER 3 — INTERPRETATION (Gemini) │ -│ Grounded by Layer 1 measurements │ -│ Arrangement advice, device mappings, musical descriptions │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## Technology Stack - -### Frontend (`apps/ui`) - -| Technology | Version | Purpose | -|------------|---------|---------| -| React | 19 | UI framework | -| TypeScript | 5.8 | Type safety | -| Vite | 6 | Build tool and dev server | -| Tailwind CSS | 4.1.14 | Styling with semantic tokens | -| WaveSurfer.js | 7.12.1 | Audio waveform visualization | -| midi-writer-js | 3.2.1 | MIDI file export | -| Vitest | 4.0.18 | Unit testing | -| Playwright | 1.58.2 | E2E/smoke testing | - -### Backend (`apps/backend`) - -| Technology | Version | Purpose | -|------------|---------|---------| -| Python | 3.11.x | Runtime (3.12+ not supported for full setup) | -| FastAPI | 0.135.1 | HTTP API framework | -| Uvicorn | 0.41.0 | ASGI server | -| Essentia | 2.1b6.dev1389 | DSP analysis library | -| Demucs | 4.0.1 | Source separation | -| PyTorch | 2.10.0 | Deep learning backend | -| Google GenAI | 1.14.0+ | Gemini API client | -| SQLite | (builtin) | Run state persistence | - -### Development Tools - -- **Node.js**: 20+ for frontend -- **Python**: 3.11.x specifically for backend -- **Bash**: For orchestration scripts - -## Project Structure - -``` -asa/ -├── apps/ -│ ├── ui/ # React frontend -│ │ ├── src/ -│ │ │ ├── components/ # React components -│ │ │ ├── services/ # API clients, business logic -│ │ │ ├── hooks/ # Custom React hooks -│ │ │ ├── utils/ # Utility functions -│ │ │ ├── types.ts # Shared TypeScript types -│ │ │ └── config.ts # App configuration -│ │ ├── tests/ -│ │ │ ├── services/ # Vitest unit tests -│ │ │ └── smoke/ # Playwright E2E tests -│ │ ├── package.json -│ │ └── tsconfig.json -│ └── backend/ # Python backend -│ ├── analyze.py # CLI entry; coordinates analyze_*.py modules -│ ├── analyze_core.py, analyze_detection.py, analyze_rhythm.py, -│ │ analyze_segments.py, analyze_structure.py, -│ │ analyze_transcription.py, analyze_audio_io.py, -│ │ analyze_estimate.py, analyze_fast.py # Feature modules (split from monolith) -│ ├── server.py + server_phase1.py / server_phase2.py / server_upload.py -│ ├── analysis_runtime.py # SQLite persistence layer -│ ├── worker.py, runtime_profile.py, auth_context.py, artifact_storage.py -│ │ # Hosted-mode foundation -│ ├── upload_limits.py # 100/101 MiB upload contract -│ ├── spectral_viz.py # Spectrogram artifacts (non-critical) -│ ├── polyphonic_evaluation.py # Research-only; not on product path -│ ├── requirements.txt -│ ├── scripts/ -│ │ ├── bootstrap.sh # Environment setup -│ │ └── evaluate_polyphonic.py # Research-only evaluator entry point -│ ├── tests/ # unittest suite -│ └── prompts/ # Gemini system prompts + Live 12 device catalog -├── scripts/ -│ ├── dev.sh # Full-stack dev launcher -│ ├── test-e2e-integration.sh # Local-only integration E2E -│ └── test-e2e.sh # Live Gemini E2E -├── docs/ -│ ├── ARCHITECTURE_STRATEGY.md # Why the architecture is shaped this way -│ ├── PUBLIC_HOSTING_FOUNDATION.md # Hosted-mode boundaries -│ ├── POLYPHONIC_TRANSCRIPTION_SPIKE.md # Research notes -│ └── archive/ # Historical/invalidated docs -└── AGENTS.md # This file -``` - -### Key Frontend Modules - -| File/Directory | Purpose | -|----------------|---------| -| `src/App.tsx` | Main application, upload flow, phase orchestration | -| `src/services/analysisRunsClient.ts` | Canonical transport for `/api/analysis-runs*` | -| `src/services/analyzer.ts` | Orchestration: run creation, polling, display payload projection | -| `src/services/backendPhase1Client.ts` | Legacy multipart transport for `/api/analyze` (compat path) | -| `src/services/phase2Validator.ts` | Runtime chain-of-custody validator (Phase 2 vs Phase 1) | -| `src/types.ts` + `src/types/` | Shared response contracts (barrel re-export of `types/{measurement,backend,interpretation}.ts`) | -| `src/components/AnalysisResults.tsx` | Results display | -| `src/components/SessionMusicianPanel.tsx` | MIDI/piano roll UI | - -### Key Backend Modules - -| File | Purpose | -|------|---------| -| `analyze.py` | DSP pipeline entry point; coordinates the `analyze_*.py` modules below | -| `analyze_core/_detection/_rhythm/_segments/_structure/_transcription/_audio_io/_estimate/_fast.py` | Feature modules (split from the monolith in commit `5c40dd44`) | -| `server.py` (+ `server_phase1.py`, `server_phase2.py`, `server_upload.py`) | HTTP transport, temp file handling, response normalization | -| `analysis_runtime.py` | SQLite persistence, stage queues, artifact metadata | -| `worker.py` / `runtime_profile.py` / `auth_context.py` / `artifact_storage.py` | Hosted-mode runtime | -| `polyphonic_evaluation.py` | **Research-only.** Offline harness, not on product path | -| `tests/test_server.py` | API contract tests | -| `tests/test_analyze.py` | Structural snapshot tests; owns `EXPECTED_TOP_LEVEL_KEYS` | - -## Build and Development Commands - -### Full Stack (Recommended) - -Start both backend and UI with proper synchronization: - -```bash -./scripts/dev.sh -``` - -This script: -1. Starts backend on `127.0.0.1:8100` -2. Waits for OpenAPI contract verification -3. Starts UI on `127.0.0.1:3100` -4. Handles graceful shutdown on Ctrl-C - -### Backend Only - -Setup (first time or after dependency changes): - -```bash -./apps/backend/scripts/bootstrap.sh -``` - -Run the server: - -```bash -./apps/backend/venv/bin/python apps/backend/server.py -# Or with custom port: -SONIC_ANALYZER_PORT=8100 ./apps/backend/venv/bin/python apps/backend/server.py -``` - -Run CLI analyzer directly: - -```bash -./apps/backend/venv/bin/python apps/backend/analyze.py [--separate] [--transcribe] [--yes] -``` - -### Frontend Only - -Setup: - -```bash -cd apps/ui -npm install -``` - -Development server: - -```bash -npm run dev:local # Port 3100, localhost only -npm run dev # Port 3000, host 0.0.0.0 -``` - -Build: - -```bash -npm run build -npm run preview # Preview production build -``` - -### Verification Commands - -Frontend full validation: - -```bash -cd apps/ui -npm run verify # lint + unit tests + build + smoke tests -``` - -Backend tests: - -```bash -cd apps/backend -./venv/bin/python -m unittest discover -s tests -``` - -Syntax checks: - -```bash -cd apps/backend -./venv/bin/python -m py_compile server.py -./venv/bin/python -m py_compile analyze.py -``` - -### Single Test Commands - -Frontend unit test: - -```bash -cd apps/ui -npx vitest run tests/services/backendPhase1Client.test.ts -npx vitest run tests/services/backendPhase1Client.test.ts -t "test name" -``` - -Frontend smoke test: - -```bash -cd apps/ui -npm run test:smoke -- tests/smoke/upload-phase1.spec.ts -``` - -Backend single test: - -```bash -cd apps/backend -./venv/bin/python -m unittest tests.test_server -./venv/bin/python -m unittest tests.test_server.ServerContractTests -./venv/bin/python -m unittest tests.test_server.ServerContractTests.test_analyze_endpoint_combines_separate_and_transcribe_in_subprocess -``` - -## Testing Strategy - -### Frontend Testing - -| Test Type | Tool | Location | Purpose | -|-----------|------|----------|---------| -| Unit | Vitest | `tests/services/` | Test business logic, parsers, clients | -| Smoke | Playwright | `tests/smoke/` | Critical path E2E tests | -| Live | Playwright | `tests/smoke/*-live*.spec.ts` | Tests against real backend/Gemini | - -**Test Environment**: Vitest runs in `node` environment (not `jsdom`). Tests use mocks, fake timers, and payload fixtures. - -**Important**: Playwright boots the app on `127.0.0.1:3100` for smoke tests. - -### Backend Testing - -| Test Type | Tool | Location | Purpose | -|-----------|------|----------|---------| -| Contract | unittest | `tests/test_server.py` | API envelope, error handling | -| Structural | unittest | `tests/test_analyze.py` | Raw analyzer JSON output | - -**Testing Framework**: Uses stdlib `unittest`, not pytest. - -### Test Data - -- Backend tests generate temporary WAV fixtures -- Smoke tests may use `TEST_FLAC_PATH` environment variable for live backend tests -- Gemini live tests require `RUN_GEMINI_LIVE_SMOKE=true` and API key - -## Code Style Guidelines - -### Python (Backend) - -- **Indentation**: 4 spaces -- **Quotes**: Prefer double quotes -- **Type hints**: Use Python 3.10+ style (`str | None`, `dict[str, Any]`) -- **Import order**: stdlib → third-party → local (separated by blank lines) -- **Naming**: `snake_case` functions/variables, `PascalCase` classes, `UPPER_SNAKE_CASE` constants -- **Private helpers**: Prefix with `_` when internal to module - -Example: -```python -import json -from typing import Any - -import numpy as np -from fastapi import FastAPI - -from analysis_runtime import AnalysisRun - - -def _normalize_value(value: float | None) -> float | None: - return round(value, 4) if value is not None else None -``` - -### TypeScript/React (Frontend) - -- Follow the local style of the file you're editing -- **Naming**: `PascalCase` components/interfaces, `camelCase` functions/variables -- **Imports**: External packages first, then local modules -- **Components**: Use function components and hooks -- **Cleanup**: Always clean up timers, object URLs, audio resources in `useEffect` - -### Tailwind CSS - -- Use semantic tokens from `src/index.css` (`bg-bg-panel`, `text-text-secondary`) -- Maintain Ableton-inspired dark visual language -- Keep motion purposeful and lightweight - -## Security Considerations - -### API Keys - -- **Gemini API Key**: Stored in backend environment, NOT exposed to frontend -- Frontend Phase 2 uses backend-mediated Gemini calls -- Never commit API keys to the repository - -### CORS - -Backend allows these origins: -- `http://localhost:3000`, `http://127.0.0.1:3000` -- `http://localhost:3100`, `http://127.0.0.1:3100` -- `http://localhost:5173`, `http://127.0.0.1:5173` - -### File Uploads - -- Temporary files are written to disk during analysis -- Files are cleaned up after processing (success or error) -- Raw audio files at or below 100 MiB go inline to Gemini -- Larger files use Gemini Files API - -### Local Development Only - -Current quality bar is for local development. Do not present as production-ready security until: -- Proper authentication is implemented -- Input validation is hardened -- Database and artifact storage move beyond the local machine - -## Environment Configuration - -### Frontend Environment Variables - -Create `apps/ui/.env` from `.env.example`: - -```bash -# Required for backend connection -VITE_API_BASE_URL="http://127.0.0.1:8100" - -# Enable Phase 2 Gemini features -VITE_ENABLE_PHASE2_GEMINI="true" - -# Disable HMR for testing -DISABLE_HMR="true" -``` - -### Backend Environment Variables - -```bash -# Server port (default: 8100) -SONIC_ANALYZER_PORT=8100 - -# Gemini API key for Phase 2 -GEMINI_API_KEY="your_key_here" -``` - -### Test Environment Variables - -```bash -# Live backend smoke tests -TEST_FLAC_PATH=/path/to/track.flac -VITE_API_BASE_URL=http://127.0.0.1:8100 - -# Live Gemini smoke tests -RUN_GEMINI_LIVE_SMOKE=true -VITE_ENABLE_PHASE2_GEMINI=true -GEMINI_API_KEY=your_key_here -``` - -## Network Configuration - -### Canonical Local Ports - -| Service | Port | URL | -|---------|------|-----| -| UI dev server | 3100 | http://127.0.0.1:3100 | -| Backend API | 8100 | http://127.0.0.1:8100 | - -### API Endpoints - -Backend exposes: - -- `POST /api/analysis-runs/estimate` - Get the canonical runtime estimate -- `POST /api/analysis-runs` - Create a staged analysis run -- `GET /api/analysis-runs/{run_id}` - Poll the canonical run snapshot -- `POST /api/analyze` - Legacy compatibility wrapper for full analysis -- `POST /api/phase2` - Legacy compatibility wrapper for Gemini interpretation -- `GET /openapi.json` - OpenAPI schema -- `GET /docs` - Swagger UI -- `GET /redoc` - ReDoc documentation - -## Deployment - -### Current Status - -The system is designed for **local development** use. Current limitations: - -- SQLite database stored locally (`.runtime/analysis_runs.sqlite3`) -- Artifact storage on local filesystem -- No authentication/authorization layer -- Python 3.11.x requirement for full setup - -### Deployment Considerations - -Before production deployment: - -1. Move to proper database (PostgreSQL) -2. Implement cloud storage for artifacts -3. Add authentication layer -4. Containerize with Docker -5. Implement proper secret management -6. Add rate limiting and resource controls - -## Sub-Project Documentation - -For detailed information specific to each app, see: - -- `apps/ui/AGENTS.md` - Frontend-specific guidance, React patterns, styling rules -- `apps/backend/AGENTS.md` - Backend-specific guidance, DSP pipeline, testing expectations -- `apps/backend/ARCHITECTURE.md` - Backend component responsibilities -- `apps/backend/JSON_SCHEMA.md` - Raw CLI and HTTP schema documentation -- `docs/ARCHITECTURE_STRATEGY.md` - Architecture decisions and roadmap - -## Important Constraints - -1. **Python Version**: Backend requires Python 3.11.x for full-feature local setup. Python 3.12+ is not yet supported because Essentia 2.1b6 wheels are only published for 3.11 on macOS arm64. - -2. **No Repo-Wide Formatting**: No ESLint/Prettier/Ruff baseline is enforced. Follow the style of the surrounding file. - -3. **Contract Boundaries**: - - Measurement result is authoritative - - Pitch/note transcription is injected from pitch/note translation stage (not copied from measurement) - - UI/backend contract is strict and strongly typed - -4. **Before Structural Changes**: Read `docs/ARCHITECTURE_STRATEGY.md` first. It contains the reasoning behind the current design and planned experiments. - -## Common Tasks - -### Adding a New DSP Feature - -1. Add function in `apps/backend/analyze.py` -2. Update raw JSON output schema (document in `JSON_SCHEMA.md`) -3. Update `server.py` normalization if needed -4. Update frontend `types.ts` if new fields exposed via HTTP -5. Add tests in `tests/test_analyze.py` - -### Adding a New API Endpoint - -1. Add route in `apps/backend/server.py` -2. Run contract tests: `./venv/bin/python -m unittest tests.test_server` -3. Update OpenAPI will be automatic -4. Add frontend client method in `src/services/` -5. Update frontend types in `src/types.ts` - -### Debugging Backend Issues - -1. Check logs on stderr (timing, diagnostics) -2. Run CLI directly: `./venv/bin/python analyze.py --yes` -3. Verify JSON output is valid -4. Run contract tests to isolate issue - -## Change Checklist - -Before submitting changes: - -- [ ] If changing API request parsing, run `tests/test_server.py` -- [ ] If changing raw analyzer output, run `tests/test_analyze.py` and update docs -- [ ] If changing timeout or diagnostics, inspect both tests and `ARCHITECTURE.md` -- [ ] If adding a new field, document whether it belongs to raw CLI output, HTTP `phase1`, or both -- [ ] Run narrowest relevant test first, then full suite for broad changes -- [ ] Frontend: run `npm run verify` for app-wide changes -- [ ] Backend: run `./venv/bin/python -m unittest discover -s tests` - -## Getting Help - -- Review `docs/ARCHITECTURE_STRATEGY.md` for architecture reasoning -- Check `apps/backend/JSON_SCHEMA.md` for API contracts -- See `README.md` at repo root and in each app directory -- Review existing tests for usage examples +Historical plan and audit documents live in [`docs/history/`](docs/history/) — past-tense, not living docs. diff --git a/ASA_System_Design.docx b/ASA_System_Design.docx deleted file mode 100644 index ede1fc568be8316d61a9ce41656953f8d50f4323..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42054 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%0DoDHGPwGr< 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 `AGENTS.md` chain > this file. +When information conflicts: `PURPOSE.md` > this file > per-app `AGENTS.md`. diff --git a/CODEX.md b/CODEX.md deleted file mode 100644 index fc833a43..00000000 --- a/CODEX.md +++ /dev/null @@ -1,84 +0,0 @@ -# CODEX.md - -This file provides guidance to Codex when working in this repository. - -## Source Mapping - -- Product mission and quality bar: `PURPOSE.md` and `ASA_System_Design.docx`. -- Repo workflow and policy: `AGENTS.md` at root, then app-local `apps/ui/AGENTS.md` or `apps/backend/AGENTS.md`. -- Command and runtime specifics: `CLAUDE.md`. - -When guidance differs: - -1. Mission and quality invariants from `PURPOSE.md` and `ASA_System_Design.docx` win. -2. Repo workflow and contract rules from the `AGENTS.md` chain win next. -3. `CODEX.md` files provide Codex-tailored execution guidance. - -## Read Order For Codex - -1. `PURPOSE.md` -2. `ASA_System_Design.docx` -3. `AGENTS.md` -4. app-local `CODEX.md` + app-local `AGENTS.md` for the area you edit -5. `CLAUDE.md` for command details and additional guardrails - -## Mission Gate - -Before implementing changes, run this test: - -1. Does this improve measurement accuracy? -2. Does this improve recommendation specificity/quality? -3. Does this improve a producer's ability to act in Ableton? -4. If it is maintenance-only, does it clearly unblock one of the above? -5. If none apply, stop and reconsider. - -## Non-Negotiable Invariants - -- Phase 1 measurements are ground truth; Phase 2 does not override measured values. -- Phase 2 recommendations must cite specific Phase 1 measurements. -- Recommendations must be Ableton Live 12 specific (device, parameter, value). -- Low-confidence measurements must lead to hedged recommendations. -- Reconstruction guidance must cover the full production surface. -- Output must remain usable for intermediate producers without DSP expertise. - -## Architecture Snapshot - -- Layer 1 (`apps/backend/analyze.py`): deterministic DSP measurement engine. -- Layer 2 (`apps/backend/server.py` + `/api/phase2`): interpretation using measured data plus audio. -- Layer 3 (`apps/ui`): upload, estimate, analysis, and reconstruction-facing presentation. -- Contract boundary: `phase1`/`phase2` shapes consumed by UI types must remain aligned with backend responses. - -## Codex Workflow Expectations - -- Treat monorepo root as entrypoint for stack orchestration and release context. -- Prefer surgical edits; avoid broad rewrites unless explicitly requested. -- Preserve `analyze.py` `stdout` JSON vs `stderr` diagnostics behavior. -- Keep frontend/backend contracts in sync when adding/removing fields. -- Read `docs/ARCHITECTURE_STRATEGY.md` before proposing structural architecture or pipeline changes. - -## Canonical Commands - -From repo root: - -```bash -./scripts/dev.sh -``` - -Frontend verification: - -```bash -cd apps/ui -npm run verify -``` - -Backend verification: - -```bash -cd apps/backend -./venv/bin/python -m unittest discover -s tests -``` - -## App Routing - -- UI work: read `apps/ui/CODEX.md` and `apps/ui/AGENTS.md`. -- Backend work: read `apps/backend/CODEX.md` and `apps/backend/AGENTS.md`. diff --git a/README.md b/README.md index b3bfd5b5..b474e501 100644 --- a/README.md +++ b/README.md @@ -1,222 +1,73 @@ -# asa +# Ableton Sonic Analyzer (ASA) -Local/dev monorepo for the Sonic Analyzer project. +A local-first tool that answers **"how do I make something that sounds like +this?"** for intermediate Ableton Live 12 producers. -This repo preserves the history of the existing UI and backend repos and brings -them together under one roof: +ASA runs deterministic DSP measurements on a track (Phase 1) and feeds them to +an AI interpreter (Phase 2) that produces specific, measurement-cited Ableton +device recommendations. The chain of custody from measured number to +recommendation is the product — see [`PURPOSE.md`](PURPOSE.md) for the design +brief and quality invariants. -- `apps/ui` contains the React/Vite frontend -- `apps/backend` contains the Python/FastAPI local DSP backend -- `scripts/dev.sh` starts the full local stack on the canonical ports +## Architecture -Migration note: - -- `apps/ui` and `apps/backend` were imported with history from the former standalone repos. -- The monorepo root is now the source of truth for release notes, local-stack commands, and push workflow. -- App-level changelogs remain imported app history rather than monorepo release history. -- App-specific editing and test guidance lives in `apps/ui/AGENTS.md` and `apps/backend/AGENTS.md`. - -## AI Agent Docs - -- `AGENTS.md`: monorepo policy and workflow rules. -- `CODEX.md`: Codex-tailored guidance derived from `PURPOSE.md`, `ASA_System_Design.docx`, and `CLAUDE.md`. -- App-local overlays: - - `apps/ui/CODEX.md` - - `apps/backend/CODEX.md` - -## Canonical Local Stack - -- UI: `http://127.0.0.1:3100` -- backend: `http://127.0.0.1:8100` - -## Canonical Runtime Flow - -- `POST /api/analysis-runs/estimate` -- `POST /api/analysis-runs` -- `GET /api/analysis-runs/{run_id}` -- `GET /api/analysis-runs/{run_id}/artifacts...` - -Runtime profiles: - -- `local`: current local/dev mode with SQLite + local artifact files + in-process workers. -- `hosted`: hosted-service mode with auth hooks and worker separation boundaries. - -In plain English: the analysis engine is still shared, but the repo now has an explicit split between local mode and hosted mode so public-hosting work does not have to change the local product path. - -Artifact storage now sits behind a backend storage service boundary. In plain English: ASA still writes files locally today, but the code is no longer hard-wired to assume that every stored artifact is just a disk path on the same machine. - -Implementation record: - -- see `docs/PUBLIC_HOSTING_FOUNDATION.md` for the full summary of the hosted-foundation work, the follow-up fixes, the verification that was run, and the remaining work before any true public deployment. - -Legacy `POST /api/analyze`, `POST /api/analyze/estimate`, and `POST /api/phase2` remain available only as temporary compatibility wrappers during the migration window. - -## Local Setup - -Frontend dependencies: - -```bash -cd apps/ui -npm install ``` - -Backend environment: - -```bash -./apps/backend/scripts/bootstrap.sh -``` - -The backend bootstrap path is verified on Python `3.11.x`. The bootstrap -script recreates `apps/backend/venv` from scratch and is the supported recovery -path if the local backend environment becomes stale or broken. - -Manual equivalent: - -```bash -cd apps/backend -python3.11 -m venv venv -./venv/bin/python -m pip install --upgrade pip -./venv/bin/python -m pip install -r requirements.txt -``` - -The backend dependency stack is pinned and validated on Python `3.11.x` for -full-feature local development on macOS arm64. - -Current limitation: Python `3.12+` is not yet supported because Essentia -2.1b6 wheels are only published for 3.11 on macOS arm64. - -Run the full stack from the repo root: - -```bash -./scripts/dev.sh +Layer 1 — MEASUREMENT (Essentia/DSP) → deterministic, authoritative +Layer 2 — PITCH/NOTE TRANSLATION (torchcrepe) → best-effort on separated stems +Layer 3 — INTERPRETATION (Gemini) → grounded in Layer 1 measurements ``` -### Phase 2 Local Setup - -`./scripts/dev.sh` now reads `apps/ui/.env` before starting Vite. This is the -recommended persistent way to enable Gemini Phase 2 locally. - -Persistent `.env` setup: +Phase 2 never overrides Phase 1. See [`docs/ARCHITECTURE_STRATEGY.md`](docs/ARCHITECTURE_STRATEGY.md) for *why* the stack is shaped this way. -```bash -cd apps/ui -cp .env.example .env -``` - -Then set: +## Repo layout -```bash -VITE_API_BASE_URL="http://127.0.0.1:8100" -VITE_ENABLE_PHASE2_GEMINI="true" ``` - -Optional hosted-mode request-header bootstrap for private beta testing: - -```bash -VITE_API_REQUEST_HEADERS_JSON='{"X-ASA-User-Id":"beta-user-123"}' +apps/backend/ Python 3.11 + FastAPI + Essentia DSP pipeline +apps/ui/ React 19 + Vite + TypeScript + Tailwind +scripts/ dev.sh, e2e harnesses +docs/ ARCHITECTURE_STRATEGY, SETUP, topic docs +docs/history/ Completed plans and one-shot audits (reference only) ``` -Supported shell-based overrides: +## Quickstart -```bash -export GEMINI_API_KEY="your_real_key_here" -./scripts/dev.sh -``` +Requires **Python 3.11.x** (Essentia 2.1b6 wheels aren't published for 3.12+) and **Node.js 20+**. ```bash -GEMINI_API_KEY="your_real_key_here" ./scripts/dev.sh -``` +# One-time backend setup +./apps/backend/scripts/bootstrap.sh -This does **not** work because the variable is not exported to the next -command: +# One-time frontend setup +cd apps/ui && npm install && cd - -```bash -GEMINI_API_KEY="your_real_key_here" +# Start the full stack (UI on :3100, backend on :8100) ./scripts/dev.sh ``` -Manual equivalent: - -```bash -cd apps/backend -SONIC_ANALYZER_PORT=8100 ./venv/bin/python server.py -``` - -Hosted worker process: - -```bash -cd apps/backend -SONIC_ANALYZER_RUNTIME_PROFILE=hosted SONIC_ANALYZER_PROCESS_ROLE=worker ./venv/bin/python worker.py -``` - -```bash -cd apps/ui -VITE_API_BASE_URL=http://127.0.0.1:8100 npm run dev:local -``` +Full setup, environment variables, Phase 2 (Gemini) configuration, and +verification commands live in [`docs/SETUP.md`](docs/SETUP.md). ## Verification -Frontend: - ```bash -cd apps/ui -npm run verify +cd apps/ui && npm run verify # lint + unit + build + smoke +cd apps/backend && ./venv/bin/python -m unittest discover -s tests +./scripts/test-e2e-integration.sh # local-only e2e, no Gemini key ``` -Backend: - -```bash -cd apps/backend -./venv/bin/python -m unittest discover -s tests -``` - -Canonical local end-to-end verification is local-only, boots the real backend, drives the UI against the canonical `analysis-runs` routes, and does not require Gemini credentials or a user-provided track: - -```bash -./scripts/test-e2e-integration.sh -``` - -Full live Gemini end-to-end verification stays separate and requires a real audio file plus backend Gemini credentials: - -```bash -TEST_FLAC_PATH=/path/to/track.flac \ -GEMINI_API_KEY=your_real_key_here \ -VITE_ENABLE_PHASE2_GEMINI=true \ -./scripts/test-e2e.sh -``` - -## Upload Limits - -For the backend upload routes, ASA now distinguishes between: - -- raw audio limit: `100 MiB` -- HTTP request envelope limit: `101 MiB` - -In plain English: the audio file itself must stay at or below 100 MiB, but the -whole multipart request is allowed to be slightly larger so filenames and form -wrapping do not cause false `413` errors. - -The canonical operator view is generated from backend code, not maintained by -hand. To see the current edge contract and proxy examples: - -```bash -cd apps/backend -./venv/bin/python scripts/render_upload_limit_contract.py -``` - -If you later put this local stack behind a reverse proxy or load balancer, -mirror the generated `101 MiB` request-body limit there for the protected -upload routes instead of copying stale numbers from old docs. - -## Release Position - -The initial monorepo cut was **local/dev `v1.0.0`**. Current tags: `v1.2.0` (root), `ui-v1.6.0` (frontend). +## Documentation -The current quality bar is met for local development and iterative product work. -It should not be presented as a stronger production/security milestone until -authentication, stronger input hardening, and non-local artifact/database infrastructure are in place. +| Where | What | +|---|---| +| [`PURPOSE.md`](PURPOSE.md) | Why ASA exists; non-negotiable quality invariants. | +| [`CLAUDE.md`](CLAUDE.md) | Canonical guide for AI coding agents and contributors: commands, architecture, tripwires, change map. | +| [`docs/ARCHITECTURE_STRATEGY.md`](docs/ARCHITECTURE_STRATEGY.md) | Why the three-layer design is shaped the way it is. | +| [`docs/SETUP.md`](docs/SETUP.md) | Detailed local setup, env vars, Phase 2 wiring. | +| [`apps/backend/ARCHITECTURE.md`](apps/backend/ARCHITECTURE.md) | Backend HTTP flow and contract. | +| [`apps/backend/JSON_SCHEMA.md`](apps/backend/JSON_SCHEMA.md) | Phase 1 stdout JSON schema. | +| [`BACKLOG.md`](BACKLOG.md) | What's next. | +| [`CHANGELOG.md`](CHANGELOG.md) | What's shipped. | -Keep the backend bootstrap limitation in mind when handing the repo to another machine: +## License -- prefer Python `3.11.x` -- run `./apps/backend/scripts/bootstrap.sh` from the repo root before starting the local stack +[MIT](LICENSE). diff --git a/apps/backend/AGENTS.md b/apps/backend/AGENTS.md index 30cc8922..4ae460d4 100644 --- a/apps/backend/AGENTS.md +++ b/apps/backend/AGENTS.md @@ -3,7 +3,7 @@ ## Scope - This file applies to `apps/backend` inside the `asa` monorepo. -- Codex-specific instructions for this app live in `CODEX.md`. +- Root-level agent guidance lives in `../../CLAUDE.md`; this file is the backend overlay. - The repo is a local Python audio-analysis service with two entry points: - `analyze.py`: raw CLI analyzer - `server.py`: FastAPI wrapper around the CLI diff --git a/apps/backend/CODEX.md b/apps/backend/CODEX.md deleted file mode 100644 index aee27b78..00000000 --- a/apps/backend/CODEX.md +++ /dev/null @@ -1,46 +0,0 @@ -# CODEX.md - -Codex instructions for `apps/backend`. - -## Source Mapping - -- Product intent and quality bar: `../../PURPOSE.md` and `../../ASA_System_Design.docx`. -- Repo and app policy: `../../AGENTS.md` and `./AGENTS.md`. -- Runtime command details and additional guardrails: `../../CLAUDE.md`. - -When guidance differs, keep mission and quality invariants from `PURPOSE.md` and the system design document as the primary decision filter. - -## Backend Mission In This Repo - -- Keep deterministic measurement quality high and trustworthy. -- Preserve the chain of custody from Phase 1 metrics to Phase 2 advice. -- Protect producer-facing reliability over internal abstraction complexity. - -## Contract-Critical Rules - -- `analyze.py` emits machine-readable JSON to `stdout`; diagnostics/logs go to `stderr`. -- `server.py` normalizes raw analyzer output into stable HTTP envelopes for UI consumption. -- Treat backend output shape as contract; update tests/docs with any intentional schema change. -- Keep Phase 1 measurement authority intact; never add behavior that lets Phase 2 override measured values. - -## Canonical Commands - -```bash -./scripts/bootstrap.sh -./venv/bin/python server.py -./venv/bin/python analyze.py [--separate] [--transcribe] [--fast] [--yes] -./venv/bin/python -m unittest discover -s tests -``` - -Preferred synced stack from repo root: - -```bash -./scripts/dev.sh -``` - -## Codex Change Checklist - -- If request parsing, subprocess behavior, or envelopes change: run `tests/test_server.py` or broader. -- If raw analyzer output changes: run `tests/test_analyze.py` and sync docs. -- Preserve bounded diagnostics and structured error responses. -- Keep edits surgical unless an explicit broader refactor is requested. diff --git a/apps/backend/LICENSE b/apps/backend/LICENSE deleted file mode 100644 index 5a1ef31b..00000000 --- a/apps/backend/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Christian Smith - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/apps/ui/CODEX.md b/apps/ui/CODEX.md deleted file mode 100644 index fa54e149..00000000 --- a/apps/ui/CODEX.md +++ /dev/null @@ -1,50 +0,0 @@ -# CODEX.md - -Codex instructions for `apps/ui`. - -## Source Mapping - -- Product intent and quality bar: `../../PURPOSE.md` and `../../ASA_System_Design.docx`. -- Repo and app policy: `../../AGENTS.md` and `./AGENTS.md`. -- Runtime command details and additional guardrails: `../../CLAUDE.md`. - -When guidance differs, keep mission and quality invariants from `PURPOSE.md` and the system design document as the primary decision filter. - -## UI Mission In This Repo - -- Present deterministic Phase 1 measurements clearly and faithfully. -- Present Phase 2 interpretation as measurement-cited Ableton reconstruction guidance. -- Improve producer actionability over visual novelty. - -## Contract-Critical Rules - -- Preserve backend client and shared type contracts in: - - `src/services/backendPhase1Client.ts` - - `src/types.ts` -- Do not silently rename fields expected by backend envelopes. -- Keep diagnostics behavior stable unless intentionally changing contract + tests/docs together. -- Respect the Phase 1 ground-truth model when rendering or explaining results. - -## Canonical Commands - -```bash -npm run dev -npm run dev:local -npm run lint -npm run test:unit -npm run test:smoke -npm run verify -``` - -Preferred synced stack from repo root: - -```bash -./scripts/dev.sh -``` - -## Codex Change Checklist - -- Run focused tests first (single file/spec), then broaden as needed. -- If editing upload/orchestration/rendering flow, run relevant smoke specs. -- If editing shared types or transport parsing, run lint + targeted service tests. -- Avoid style-only churn in mixed-style files. diff --git a/apps/ui/LICENSE b/apps/ui/LICENSE deleted file mode 100644 index 5a1ef31b..00000000 --- a/apps/ui/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Christian Smith - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/PHASE2_TRUTHFULNESS_PHASES_A_B_C.docx b/docs/PHASE2_TRUTHFULNESS_PHASES_A_B_C.docx deleted file mode 100644 index b2316d322ce98c79f9e5e6e80d852357fcb07875..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13466 zcmc(FWmH|uvhKp&B@o;R4nc#vySux)yGwAFpuydPJ0ZBcyG!sOk8Dde=iE2m`*nMa z$y{sDU#;q{>guYmTSgoN6dLfeb&`5&{PE$hKVUC!XKOnHTABZ`3(VhLwCxNn?EmQq z`G+jpZ#k`?Kmb5CC;)){?~Vr6`i_={Rt_{SmKHQWp=I&IX5Dm%0osClT7y!PjPc}O z;(5w@JpqDR&xnT*rt+x`6=ItTG`yEtn~`}bQHEniH$_4hKqCl3_G)M(Bw3$L!Iy3C zwNCM792%BeI?3sl&K`9*p`>efYlUlRBV6bGk=y9d9av32$^&=reJ~2-2!)@^p;8{| z#dSClg@-FR8B>U#dIR%(&TPpp;=*)M%CngPxpU6ohY;E2=#&05BzTs6m%rzPm+i^@&_VEI8eIUco3euf61H|M?-R1(XlvMmrT@&)Gy5^Qy7FWzhert&cnK1~>1r4QRx)U)ttDTsy5Ay!WZ;CVg#^Y$y{F8!c*ObL= zl1_}5S=i6?jO84oUFT1@!`wl^i=aVEyXoY4K z#U;Hk>%Lz!We1vjA*kubHuKkL&!h~dD-T0USUVA+Z^7?#icX|sky)v|&20^Rh<=FY zSR`g_10ldzbEy?QaIuz?Mmd@W&&Dxy{fKyJ=06e1*D7T?xb|t~@-o0GoJ!`Q}d2pq9d28xMM~p zgCdWvcdat^5i3&$FIo#Da;>B)shv4?nNQ`Gq%%6*K5iv~ok8MchK?lOHs|2I3G&ix z`iyL0%dmUkdMfsT#0_fYHn}hS-JG@Vnb`zmE*`wQvmWIqzEujMQabzbA9t4*@tQoR zU|2Pj^35Jy$KO|O2gXM@8JdW;tk^oRKNUgy>w)sAXiV!EUsSv>1T_6axF+)^YZGI^CE zy_2Xjq^GrmrpQO z)$7KXbrf08sMt6U=O?9Zjzkj+17CEKywX=)%tm~dp%p0I6-VN0UAapG<(U&$I=~Db z0}5t=lM@dn2=qh!>F@-%lc!#YA1K>F=;UGZ0{1hhmc4OV8Thjf7!^=_y-QJLS-mk7kc>V*B@^{U zZeEt#J~4@9u2Jb)`=+^qz?d433gxd8-y|OFpGn(KSBU3z76me(KCjqJGK|-7G_N0G zBS%u;>|++jEXNBL6+jvMjSU?s~gztPs1%?hJZn%xIY<7j?FJDzW)ywJUf7 zuOrV7<$_!5imF*?2hT~;y(`CD&T~!fn&vs32_1Dt;R&hXU9v*7@P4TK1C#(}8$=6y zcXf6fSv;~MT#6AiVwp)+zt_K2RiQh*jxN&>~H6qF6<%aRR>^zbmIl*qFbD}GRy*dJS+_FP;s2) z31dRP-f5WC3|o7}2cJ?P>>{?tFzO_O53ffeGzNb^H*&%8Z{_-dmODP4{CUatv+>w% zu1FQmH=a&{CGkxMe-(_7bi@zx9#GySO00V{mPKD_lahp@$; zlxHp)MotqA{u;IpbG@2m?XAC1DMy5%%=g^w3IF`=s<8O^!qr>2#t(nXB}=^S(jLdr zOra{vfl}#Qj$EefBP?pxG*UE#r8z8wq<9u3*+(1&n{Vd8YGf@`x`-ArM1JU6QLUe% z=ES(3WtmoK@8-bgxM{{uKY#&geX><0M1%`N)fZ(eFDDUvwB(CF)0o%vg`)sdgiKTi`?PtN!D58EsMM_iA5$YkYNxI za8f+J8x$gd3aa@!M6_0H7g@^w9&8$#v&d<|NkU?1-nAtnvF&R?qp%fr6+PdY&ZNbdl+VB9?yR_?SM*vj!!hR_&2!PKYi3H^xy$ z1h{+}%XRcrt#X`aP%kXvCe$QClGb;=;+I82F-W=w>GOtdJLC>+0)45IN_FgkD$Ya6 z4Cb_|`&tkWIARvf|cx^ic<6>pBagCXT+zJZfza0(4Fp z?EU)UV!gB9Q~m5Ezw;*U z%o(c4y2(SZHl}xv;o11f@zv_2tzQ~gZcnB+2O?0Jp`K(aexXjVmG*X3d*6LDExm~= zKbYsLyfTirrujkrUsH@^@jZ^qL@CTz??!1(Igi4Xsd zpD|#+F{HGXWEk}aK@U(lDMO*4yPOuGiF~)6mS00*v>^}~Xac*zZU)IIOtk5SZ!>-H z8z62!6+{IpUr-}REC(nepzx$I&5V66|1|oCu%sBhRgj5G;2xK(=PWHFaZ-?x&Hdi= zm|8kDAm499R5d?DR@4Qe#HW?5|GMk~Yh`iTY$5nT2MU@TCk@G7=X*;cm zo7Q1%eX=EBXC!)N{e6Bki$7z(CoSs~Y2=NG_%~B`xwQ$3uhpTVWj;!h*N--;Te9SB zO2j;arZ6&~O+|tSSW`9OOuom-;*?}&U=(#(d14TU3Cz~V){-X(?KBa|(EQO`@O#AD zt|`dxKhexyx8D7Tf3t2+Gp%WJE$mSks$As*CSe6Nerq}~Jt1~DObuSef?yfq;TX~` zUghF%v;740;}M%ek@R!U`?024W3e12H++5$u+e7%k$m9}X0jpi9e1`K2;8o`KwtEG zwT^z?rF}EX7_JS2<{wyyO^G10J#HaMvOW?>WoM4aNbtp|OJ9juE%w+0qEqy6Ba}t7F zSX2>r5_6wx?lnNbU}D#Q53glZ!-9vcyKmsz{JkjOYD{U=b1IIY*=(|$muN{&NEFPj zQJi}cACVl8?H5ZOAXh11h6sQk9njPBIdo}+m3|j~BFjcAZKq^+xwc7RwL5lkK9fYxV04D$g;-s!&!8V#qC+L{%&q)VZx2bpD!6(-!bf9+DOJ%4OYA+a2 z8L4!hXG`kTtFv0MhOk+wtl4^>`kYY#m5&4Y+a+ataUI4FTHBFShNCx)7BH1F;=+P8 zc+kV6j}LctWIftcwjd6SNGVb2ZYka&%bHwbT!MASPWo)w*x zJ71S`85rv=($H{3wl4Rg0bSngqOO|(r?Tv3b8X3(qR@nC;13G`&dZupqA6809OkIz z4%%44-v;gZ8J^+qo!bNd4KUt}W3)W|?@nnxFL zxcb#O%t7hG>hU{=I-VfNm)BbaLTjkbBlVsGtiz3a0{qh+MTb|l1fOXl(!WjlV;AP4 zC669q8B>+(4On6@@4;XN5253O+aD;BPgoszAIkYj`_mZV-FV(H?4Jns55qQ(k_yq% z&5L4gxj4fi<&`Z#l)6tuIOg(PYqu0W331QSrG(pkk}yb75Vr{zVdBri=>DhjlE<_QnBU zN|?0r9IoFZ8+usQ<4>S~)F%93o zPcWY;zeQ{~cT;to!+SaP!@YGGBUfzdTQIBkTsZ_DG`sF6UrI(fIlUu^HlgSv-(jYXWLv%s z;d!gWfR&1H6X&l#xdt2!5QxvpzVmBz^>S*$usKI2u1RQg8fDB= z?t-AlPPHe9^)<9*6{cScX5EHk+Ms$61@4q=Fhpf!8IKb`GmQtR zCqz)|Uf&KCme-KKQxHanDsbN9`$Q!ZdqB#Xg)QjT9Dv2K_t|Avj_#7lF;-O#3w&keW~^NUv}R zZjW8=e?5C^w!Bz0bK@{=P13Dp8NR8+klrp5RFJ7@b=1q^bq(?NGW}0>9qMJ7zRlO4+oaqbWNC-wVJH+^e6!=LNGi3{j&VmFuG6V+w=sF4Fvi6UnVHn6clSBP zJl2FR@|k3EL0lO16Dk#I;k7i)JHf!XLUA{XDw!67bl&x8S@7Ct3U>9_EPoj0=V*ef z*YG%pTl0&4H-p%?<-4hsyf7XjCH79}^P|%+el##Y$Xkgs)=|5ML2#ReRS!DiH+f3y zI_sov@WO7V(~wS@Nsik-eNSvpDlIK=x%uBN(~x~sNWjqGT}*J)Q>JX-s=MAKJpyB$ zbQ4;+mGN-l(loZKMzPNoJyBM9;P>e}zP;ZAPwAKWRZS{eVG=4s`@$VocX$AquQgGHN+#|q;HLLDYDNVK8rAWJ=#W#{6Nabq~{=tnx? zw~TF|?3NP{@A=c@P?gF(PdRsQ7jBxqG~RzrQAW&=eW&N+#&s4VVj$K_1&(KBJtEXG z2#fyCNG=+KN(8)|hW4AtmIq(0<98S2kH&mv9J({v0@=zrzRDD+z@s_gR1}KH{$J=R z{OH7Y0yazgO>+!M3-Xgh;I|#bP=LjY!vGsn zzFt5oT?ucPh19egmgRV!X#T#rvHl2n)^|g&8u%%AnkGWJdRR;9H$Ac+GQ%o@cn4V*v8`Qd zSO+kP@_AcG@USLFt+i9Hl0;j-eRbF_UomBoiBU^akL+|^ix;q`GE$5?LGav#Q68!6 zT0o1EB6|9sy&;QHFN2pZ@Hl%KT#PVQ8F=p{Oe012woV4mX=_KNih2s+gPnbsS6VU7 z%XioZW<~cEw5wgqVy7vjt8Sb2;y`uU4ZO_3wPz0+?nl;fdsm(N`(;UOG30Ob%`D#1 zGN+l_(h9C9SM4?PlMi=lGhyHJ8a(KD1i90X<8+md3pNDsU_WAP&E_PH5gdQ1Vl^5ls;iK-QcQ2!XWE!>(a+I+I~ zx?}1KY4CK0=|0o7k7<4jZN^Oj8BHVVZX!;2A0(NFK5O4x=h*-)E>~P;VeaVkmej~b` ziH7rDTg>HxJ_+ydDf=@9OgKL@SiVey`^$s%^89h!`RAM%Sz9|8+R^`v1Ak19WCSlf zFafM!1z(eM7)=Cy_mCC5&Lsd&2_HUCMvtS7O7p4Z=BdT&yA{_GBbu#EIu!=G_9sz4 z|9($)4IRxLtkbFBY>?pLXwsr(R*bDIbC2l+`p1v!K{Lpq$M| z`9ZiJe4&}M=aY>z0|-!)D6DqY9BEQ$%h2U7>JO+x`F6J3U)@c5Mz zZd$I1!K-3PyC`C+X&Tcq5!3xvNeufO(B5S^QQy9lCE7sQp{*~d9cuoV%^@3wc&d!R z=`c%_)9FcwnNJM>RV9Nu z5)9z)jeSIN5nCbHS_o0|z4jCek^zGgYL;7=?=v^mt;Qx8rbqc$yr!HayV}I<7zvs%{fa4`YzfoxTK|V zDo50{;Hoqg9IYNs*uzvRyYx``7P`p|Z6vv(gHkWY z<_`(v%2SOtJdkzU9h;EYYakl#ac}i146UIbI1eugz*6P1z8G@8XGb{|`nGkjJTs5X z$GDe&XG%Zv-UhnPcAC-Ks|ibUOSxuXGB`Mye!ADR4gVsEPoNG!sqc%6O+*4t89FEf zMLS(m%9H-Ff0r+t$c8aziYQakDl1-6a{lUoqwdpAA8DJj;-}@d@8=X!#l2d+;lbs3 zoVRs5WCKh0k%ZGf&=TIUhbf#Y_ZT{M-OS0Jt5%4)X=9OuhU)EKCb31#ye9{qq9&t> zh+2ik_z1z~ftG(CHBd?Q5%^`s&0s8KBNV-Sjv zn54y-Ui7Mo#_3^>Z#*{>cGfX_dg7|>C-`iHo>Pd<5GuOQ)vHu~T>v8HYs9ZdXPZ7z z5kfH`{T{m00(|btj^dp8Y5q&*fKx8-yS}W{r_%k#&LNxL`HLq|c5uz?r+8B?OGMMGHv`Lpxa7 zM`6>E2Fn@v2y~~*6(%6F=d#G{NPLOFZWVRy6Fq1B5UyNbA}n^GplEEUEP8~n-rqS} z(r`)3R{7WxXWSlqRCN8yEjJ!A7UEu1|GAC)j7I+qLD$CS-}(m}Sr)^@Ow?l89_KV6 z4AjCgz9&EtcXuc9gRr@~UEi#GL`SdU(~~aTt}6ZvsNLd_QsW9*TqguG_8*}N$l8Xd z`cd9W78YF%feywqV>iOaELN34fKe{iCnqGGHxcAk8YrxbhWlp1s{y*68T`~FkzEyA_~@j1hvS3Ekwje5_@|-8o&(zJH!4*F{Y7_!Jr?Fh z=ip|axQ}63uZv}n>Zv-5$sVmGBQw+U2D?{s0^cOEp?4rH#TxRW&+C{3kJcPQSY!4> znsgN7pE(t@R>gXbl$4mBPh1kHqWFvrr%Ih{l8rRzR^0DV%Y(Uwm}v=+V7H|Zeh}I; zBgPkzXAdQGff{L>mh_b(7ccxW5niIVzU)aVqg1+P6~@ugaXavb6!_#|MsPz)%EnL zEr>O#Bdh|OpEl%dv@h+}1^-0Uj-65aHX)PLXde@2A8C9O`=zkuARm`VWjjj}vsYL; z>fZ&FGys~LOWE478J+S622G-Nn5rXgQO(URY|Wk+=oeE}TYOV@qu43|gUO*-KTbMpW}U4zAp3O`B1Qr;b#S6-*Xy!w@rP*8vv{6v+2QWAKJ$*U2BDx zuvIpjxqR!aF9u6`zYlTLt?h%U$#OC80drv42~ZCf9?ajW*Vt}|;I#T4vmmAgnfcl! z_4P*|psi#-P;X0v#pbIr$8#o!P3z?7HNT&XS8jlCgy3w#cN9y(@zDW zh;fNs2*%NVzu0-@cHu#6J-sjX?0ZQv|FNgOwdK#-nSa`IYT0fB9|L?%-Ii53Rz;p= zvX(64_|jz&Cd;Rb@c}k#yjfe0L^LTw%1`)!cr`9*wxfGelrfx$DUnju-y^{nEI|c{ z=7!0|`$y}NU!bE1@)WtKxlKMg2Pao8ni8MP{!tgmgwm}7$WD4Wer>X>?~#~|MZ6+= z!^I6>-J-1o0e#^WGgb8iyk1a5F4-8?orM92cc&#R8kEeZPQs|7D~sX)&0@$VaX*h@ zk%c_bXH|_9QnV_EOwuxa(=<}SintD>JjR5l4Jl0D^k>s+8T{|AW8w(rTelF_jqL94 zDM9=jA0$M!3fy>02Q1ccp#@E9NO@(MSTR&Jcat7H-n2FiX>)f4q*Ol*ey!;Wwqo}# z!+yJPOA}ukVZJz@kHP^vrtwY%R}BXnO|*AsT>+B%}A?yaS={+Kh+eXl!I1 zr9sH%+seAZR$d?`P$FxW(L2FCG^d)c1VL0-;SO4blA{^Dg+#6GmP9QWj${6P){|Pn z`eg~<(UTLN>&uTVb)V?+-uXm%1O3li;HyycyXF74Lyi8i)(p;zYx2JwZ2Xp|Sve@^ z>RJ53gJz5`|Kvd-&Xft3DC<%_nwG9mi95A(0)XoT;0xB*rW)|d>diMkE~6n{Ja2ng z+$h@|^@CRLQMRcWrDImesC$>*-puyvbz=(d*E#G-_F4zsdNs7hGu?|&edbKaqFCvf zOXQ0-gB4NJGxxYJrT2;0$BYCvV9i(zg*dcm3FR1se$wfpgr2EW+`*Z~jEM5o z0^N)a`Sf;iB1=Tz&yb(2)}QSsgy`k@EyI%${~P!x|MU8a`d`SGm&Ct;FGbG3FRZ_o^!mE? zFG*o9F8qfIzsq=W3*PWjCjJ5cb)5GL{GY_%&tru@+t1-^z4@&^`vv=R@UM>< z{ssa7fxpB4K6H2uf6br&f*)f1rSiYn^k4Y@)cPgI{j2@dy5)t6`i4|)|E9^$J@8j^Bm0*ozYF=VioDK Date: Wed, 13 May 2026 04:56:26 +0000 Subject: [PATCH 2/2] fix(test): point upload-limit doc test at docs/SETUP.md instead of root README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit moved operator-facing upload-limit details from the root README into the new docs/SETUP.md (the canonical setup doc that the slimmed README links to). test_docs_reference_generator_and_current_contract_values in test_upload_limits.py asserted those strings still live in the root README, so backend CI failed. The contract the test is actually guarding — "the upload limit values and the generator command are documented somewhere visible and operator- facing" — is preserved; only the file containing them moved. Updated the test to read docs/SETUP.md for the two strings that were relocated: - "./venv/bin/python scripts/render_upload_limit_contract.py" - f"{MAX_UPLOAD_SIZE_BYTES // (1024 * 1024)} MiB" (i.e. "100 MiB") Backend README and apps/backend/ARCHITECTURE.md assertions unchanged (those docs were not touched in the cleanup). https://claude.ai/code/session_01GvD1K33Jev3vrQXRsX9WAG --- apps/backend/tests/test_upload_limits.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/backend/tests/test_upload_limits.py b/apps/backend/tests/test_upload_limits.py index e3941627..84502e36 100644 --- a/apps/backend/tests/test_upload_limits.py +++ b/apps/backend/tests/test_upload_limits.py @@ -63,18 +63,21 @@ def test_generator_outputs_plain_english_and_proxy_snippets(self) -> None: self.assertIn(str(upload_limits.MAX_UPLOAD_REQUEST_BYTES), result.stdout) def test_docs_reference_generator_and_current_contract_values(self) -> None: - root_readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") + # Operator-facing upload-limit detail lives in docs/SETUP.md (the + # canonical setup/operator doc that the root README links to). The + # backend README and ARCHITECTURE.md keep their app-local copies. + setup_doc = (REPO_ROOT / "docs" / "SETUP.md").read_text(encoding="utf-8") backend_readme = (BACKEND_DIR / "README.md").read_text(encoding="utf-8") architecture_doc = (BACKEND_DIR / "ARCHITECTURE.md").read_text(encoding="utf-8") expected_command = "./venv/bin/python scripts/render_upload_limit_contract.py" - self.assertIn(expected_command, root_readme) + self.assertIn(expected_command, setup_doc) self.assertIn(expected_command, backend_readme) self.assertIn("upload limit contract", architecture_doc.lower()) self.assertIn(str(upload_limits.MAX_UPLOAD_REQUEST_BYTES), backend_readme) self.assertIn(f"{upload_limits.MAX_UPLOAD_REQUEST_BYTES:,}".replace(",", ""), backend_readme) - self.assertIn(f"{upload_limits.MAX_UPLOAD_SIZE_BYTES // (1024 * 1024)} MiB", root_readme) + self.assertIn(f"{upload_limits.MAX_UPLOAD_SIZE_BYTES // (1024 * 1024)} MiB", setup_doc) if __name__ == "__main__":