From 64c94cfafd3cb3857784e914e748d41fc108923d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 21:47:01 +0000 Subject: [PATCH 1/2] fix(ui): finish confidence-band unification + close estimatePlr audit item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two post-merge follow-ups from the review of bc79247: 1. Confidence-band unification (audit #56 follow-up): the three-level `ConfidenceLevel` enum ("High" | "Moderate" | "Low") and its two hardcoded `>= 0.8 ? : >= 0.5 ?` ternaries in `buildMelodyInsights` were a second source of truth for the canonical band thresholds in `services/sessionMusician/confidenceBand.ts`. The original merge deferred this rollout deliberately ("once producers see the new bands in context"); now that bands ship on chips, badges, and citation pills, the melody Sonic Element row and Melody Patch parameter row migrate too. They now render canonical band labels ("Solid scaffold (78%)", etc.) so the threshold definition lives in one place only. Drops: `ConfidenceLevel` type, `confidenceLabel` field on `MelodyInsightsViewModel`, two ternary literals. Adds: positive test that `averageConfidence: 0.83` lands on the `Solid scaffold` band via `getConfidenceBand`. 2. `estimatePlr` boundary review (audit 2026-05-16 §4): documented why the `truePeak − lufsIntegrated` fallback in `apps/ui/src/services/mixDoctor.ts` is not a competing source of truth. Backend's `analyze_plr` computes the same value byte-for-byte and emits `plr` in both `--fast` and full paths; fallback only fires on legacy snapshots that predate the field, and lands on the same number the backend would have. Inline comment + audit-doc resolution subsection so the next nightly does not re-flag it. Verified: tsc clean, 603/603 vitest pass, build green (4.26s). --- .../src/components/analysisResultsViewModel.ts | 18 +++--------------- apps/ui/src/services/mixDoctor.ts | 10 ++++++++++ .../services/analysisResultsViewModel.test.ts | 4 +++- audits/nightly-2026-05-16.md | 10 ++++++++++ 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/apps/ui/src/components/analysisResultsViewModel.ts b/apps/ui/src/components/analysisResultsViewModel.ts index 31259601..8e4a04c2 100644 --- a/apps/ui/src/components/analysisResultsViewModel.ts +++ b/apps/ui/src/components/analysisResultsViewModel.ts @@ -1,17 +1,10 @@ import { AbletonRecommendation, Phase1Result, Phase2Result } from "../types"; import { type ConfidenceBand, + getConfidenceBand, toConfidenceBand, } from "../services/sessionMusician/confidenceBand"; -// Audit Finding #4: the three-level `ConfidenceLevel` vocabulary stays alive -// for the melody-insights key/value detail rows in Sonic Element cards -// (rendered as plain text like "High (78%)", not as a colored pill). The -// confidence-badge pill path migrated to the canonical four-band ladder -// via `ConfidenceBand` below. Follow-up: migrate the melody rows too once -// producers see the new bands in context. -export type ConfidenceLevel = "High" | "Moderate" | "Low"; - export interface ConfidenceBadgeViewModel { label: string; /** @@ -213,7 +206,6 @@ export interface MelodyInsightsViewModel { dominantNotes: string[]; rangeLabel: string; confidence: number; - confidenceLabel: ConfidenceLevel; isDraft: boolean; source: MelodyInsightsSource; } @@ -473,14 +465,12 @@ export function buildMelodyInsights(phase1: Phase1Result): MelodyInsightsViewMod ? `${transcriptionDetail.pitchRange.minName as string} - ${transcriptionDetail.pitchRange.maxName as string}` : "n/a"; const confidence = Math.max(0, Math.min(1, transcriptionDetail.averageConfidence ?? 0)); - const confidenceLabel: ConfidenceLevel = confidence >= 0.8 ? "High" : confidence >= 0.5 ? "Moderate" : "Low"; return { noteCount, dominantNotes, rangeLabel, confidence, - confidenceLabel, isDraft: confidence < LOW_TRANSCRIPTION_CONFIDENCE_THRESHOLD, source: "transcription", }; @@ -496,14 +486,12 @@ export function buildMelodyInsights(phase1: Phase1Result): MelodyInsightsViewMod ? `${midiToNoteName(detail.pitchRange.min as number)} - ${midiToNoteName(detail.pitchRange.max as number)}` : "n/a"; const confidence = Math.max(0, Math.min(1, detail.pitchConfidence ?? 0)); - const confidenceLabel: ConfidenceLevel = confidence >= 0.8 ? "High" : confidence >= 0.5 ? "Moderate" : "Low"; return { noteCount, dominantNotes, rangeLabel, confidence, - confidenceLabel, isDraft: confidence < LOW_MELODY_CONFIDENCE_THRESHOLD, source: "melody", }; @@ -581,7 +569,7 @@ function getSonicMeasurements( melodyInsights.source === "transcription" ? "Transcription" : "Melody Confidence", - value: `${melodyInsights.confidenceLabel} (${Math.round(melodyInsights.confidence * 100)}%)`, + value: `${getConfidenceBand(melodyInsights.confidence).label} (${Math.round(melodyInsights.confidence * 100)}%)`, }, ] : []), @@ -1109,7 +1097,7 @@ function buildMelodyPatchParameters(insights: MelodyInsightsViewModel): ChainPar { label: noteLabel, value: `${insights.noteCount}` }, { label: "Note Range", value: insights.rangeLabel }, { label: "Dominant Notes", value: insights.dominantNotes.slice(0, 3).join(", ") || "n/a" }, - { label: confidenceLabel, value: `${Math.round(insights.confidence * 100)}% (${insights.confidenceLabel})` }, + { label: confidenceLabel, value: `${Math.round(insights.confidence * 100)}% (${getConfidenceBand(insights.confidence).label})` }, ]; } diff --git a/apps/ui/src/services/mixDoctor.ts b/apps/ui/src/services/mixDoctor.ts index 9e6c1624..41d47add 100644 --- a/apps/ui/src/services/mixDoctor.ts +++ b/apps/ui/src/services/mixDoctor.ts @@ -99,6 +99,16 @@ export function resolveMixDoctorGenreId(phase1: Phase1Result): string { return DEFAULT_GENRE_ID; } +// Audit 2026-05-16 §4 (boundary review): the truePeak − lufsIntegrated branch +// below is *not* a competing source of truth with Phase 1. The backend's +// analyze_plr (apps/backend/analyze_core.py) computes +// `round(true_peak − lufs_integrated, 2)` — byte-identical to the fallback +// here — and emits `plr` in both --fast (analyze.py:1385) and full +// (analyze.py:1775) paths. The fallback only fires when `phase1.plr` is +// null/undefined, which today means: a legacy snapshot replayed from runtime +// SQLite that predates the field. When it fires it lands on the same number +// the backend would have. Result is consumed locally for advisory scoring; +// never written back into the Phase 1 snapshot. function estimatePlr(phase1: Phase1Result): number | null { const explicit = asFiniteNumber(phase1.plr); if (explicit !== null) return roundTo(explicit, 2); diff --git a/apps/ui/tests/services/analysisResultsViewModel.test.ts b/apps/ui/tests/services/analysisResultsViewModel.test.ts index 80c9ad54..65cdff6b 100644 --- a/apps/ui/tests/services/analysisResultsViewModel.test.ts +++ b/apps/ui/tests/services/analysisResultsViewModel.test.ts @@ -9,6 +9,7 @@ import { truncateAtSentenceBoundary, truncateBySentenceCount, } from '../../src/components/analysisResultsViewModel'; +import { getConfidenceBand } from '../../src/services/sessionMusician/confidenceBand'; import { MeasurementResult, Phase2Result, TranscriptionDetail } from '../../src/types'; const measurement: MeasurementResult = { @@ -747,7 +748,8 @@ describe('analysisResultsViewModel helpers', () => { expect(insights?.noteCount).toBe(6); expect(insights?.rangeLabel).toBe('C3 - G5'); expect(insights?.dominantNotes).toEqual(['C3', 'G3']); - expect(insights?.confidenceLabel).toBe('High'); expect(insights?.isDraft).toBe(false); + expect(insights?.confidence).toBeCloseTo(0.83); + expect(getConfidenceBand(insights!.confidence).label).toBe('Solid scaffold'); }); }); diff --git a/audits/nightly-2026-05-16.md b/audits/nightly-2026-05-16.md index 9b352528..d764014a 100644 --- a/audits/nightly-2026-05-16.md +++ b/audits/nightly-2026-05-16.md @@ -160,6 +160,16 @@ and the safer-but-stricter reading would still flag this. emit `plr`, or keep the fallback and document it as an intentional derived display rather than a Phase 1 override. +**Resolution (post-merge follow-up):** keep the fallback, document why. +Verified `analyze_plr` in [apps/backend/analyze_core.py:624-635](../apps/backend/analyze_core.py) computes +`round(true_peak − lufs_integrated, 2)` — byte-identical to the UI fallback — +and that `plr` is emitted in both `--fast` ([analyze.py:1385](../apps/backend/analyze.py)) +and full ([analyze.py:1775](../apps/backend/analyze.py)) paths. The fallback fires only +when `phase1.plr` is null/undefined (legacy snapshots replayed from runtime +SQLite that predate the field) and lands on the same number the backend would +have. Documented inline above `estimatePlr` so the next audit doesn't re-flag +this. See [apps/ui/src/services/mixDoctor.ts:102](../apps/ui/src/services/mixDoctor.ts). + --- ### What was *not* found From 20b263a84b34720591a35762f10e8378072236da Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 21:51:39 +0000 Subject: [PATCH 2/2] test(ui): cover rendered band labels at the two PR call sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #60 review noted that the threshold-drift guard didn't cover the actual string-interpolation sites added by the migration. Adds two focused tests: 1. buildSonicElementCards (transcription path) → asserts that the melodicArp card's "Transcription" measurement renders "Solid scaffold (83%)" — exercises the Sonic Element row template at analysisResultsViewModel.ts:572. 2. buildPatchCards (MIDI Clip Guide synthetic fallback) → asserts that the melody-confidence parameter renders "72% (Workable draft)" — exercises the patch-parameter row template at analysisResultsViewModel.ts:1099. A future regression in either `.label` interpolation now fails a specific test rather than waiting for manual QA. 605/605 pass (was 603). --- .../services/analysisResultsViewModel.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/apps/ui/tests/services/analysisResultsViewModel.test.ts b/apps/ui/tests/services/analysisResultsViewModel.test.ts index 65cdff6b..53da2cc2 100644 --- a/apps/ui/tests/services/analysisResultsViewModel.test.ts +++ b/apps/ui/tests/services/analysisResultsViewModel.test.ts @@ -752,4 +752,39 @@ describe('analysisResultsViewModel helpers', () => { expect(insights?.confidence).toBeCloseTo(0.83); expect(getConfidenceBand(insights!.confidence).label).toBe('Solid scaffold'); }); + + // PR #60: cover the two render sites that interpolate the canonical band + // label, not just the upstream getConfidenceBand mapping. A regression in + // either string-interpolation site would otherwise hide until manual QA. + it('renders the canonical band label inside the Sonic Element melody row (transcription path)', () => { + const sonicCards = buildSonicElementCards( + { ...measurement, transcriptionDetail: pitchNote }, + { + kick: 'Kick.', + bass: 'Bass.', + melodicArp: 'Arp.', + grooveAndTiming: 'Groove.', + effectsAndTexture: 'Fx.', + widthAndStereo: 'Width.', + harmonicContent: 'Harmony.', + }, + ); + const melodic = sonicCards.find((card) => card.id === 'melodicArp'); + const confidenceRow = melodic?.measurements.find((m) => m.label === 'Transcription'); + expect(confidenceRow?.value).toBe('Solid scaffold (83%)'); + }); + + it('renders the canonical band label inside the MIDI Clip Guide patch parameters (melody fallback path)', () => { + // Phase 2 with no MIDI-focused recommendations forces the synthetic + // "MIDI Clip Guide" card, which calls buildMelodyPatchParameters with + // the melodyDetail-derived insights (pitchConfidence: 0.72 → workable). + const phase2 = { + mixAndMasterChain: [], + synthAndDrumPatches: [], + } as unknown as Phase2Result; + const cards = buildPatchCards(measurement, phase2); + const guide = cards.find((c) => c.device === 'MIDI Clip Guide'); + const confidenceRow = guide?.parameters.find((p) => p.label === 'Melody Confidence'); + expect(confidenceRow?.value).toBe('72% (Workable draft)'); + }); });