diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 01a464f1..00000000 --- a/PLAN.md +++ /dev/null @@ -1,127 +0,0 @@ -# Plan: Global Quality System — `feature/global-quality-system` - -## Was bereits implementiert ist (dieser Commit) - -### 1. `core/quality/model.py` — Das Herzstück -Source-agnostisches Datenmodell. **Jede** Quelle mappt ihre Ergebnisse auf `AudioQuality`, der gleiche Ranker läuft für alle. - -``` -AudioQuality(format, bitrate, sample_rate, bit_depth) -QualityTarget(format, bit_depth, min_sample_rate, min_bitrate, label) -filter_and_rank(candidates, targets) ← eine Funktion für alle Quellen -``` - -### 2. Soulseek — echte Werte statt Heuristik -slskd `attributes` werden jetzt ausgelesen: -- `type 4` = sample_rate (Hz) -- `type 5` = bit_depth (bits) - -Vorher: kbps-Schwellwert-Hack (1450 kbps = "wahrscheinlich 24-bit"). Jetzt: echte Werte direkt aus dem Protokoll. - -### 3. Quality Profile v3 -Statt `qualities: {flac: {enabled, priority, bit_depth}}` jetzt eine **geordnete Liste**: -```json -"ranked_targets": [ - {"label": "FLAC 24-bit/192kHz", "format": "flac", "bit_depth": 24, "min_sample_rate": 192000}, - {"label": "FLAC 24-bit/96kHz", "format": "flac", "bit_depth": 24, "min_sample_rate": 96000}, - {"label": "FLAC 24-bit/44.1kHz","format": "flac", "bit_depth": 24, "min_sample_rate": 44100}, - {"label": "FLAC 16-bit", "format": "flac", "bit_depth": 16}, - {"label": "MP3 320kbps", "format": "mp3", "min_bitrate": 320}, - ... -] -``` -v2-Profile werden automatisch migriert. - -### 4. Post-Download Verifikation (alle Quellen) -`probe_audio_quality()` liest die **echte heruntergeladene Datei** mit mutagen: -- FLAC: sample_rate + bit_depth + bitrate -- MP3: bitrate + sample_rate -- M4A/AAC/OGG/OPUS/WAV: bitrate + sample_rate - -`check_quality_target()` prüft gegen die `ranked_targets`: -- Match → akzeptiert -- Kein Match + `fallback_enabled=False` → Quarantäne -- Nach Quarantäne → **Retry mit nächstem Kandidaten** (wie AcoustID) - ---- - -## Bekannte Bugs / offene Fragen - -### BUG: HiFi/Monochrome liefert nur 30s Audio — wird nicht erkannt - -**Symptom:** Jeder Download von Monochrome-Instanzen enthält nur ~30 Sekunden echtes Audio. Der Rest der Datei ist Stille. - -**Warum es durchrutscht:** -- Monochrome liefert einen HLS-Stream, aber nur die ersten ~30s der Segmente haben Audiodaten -- ffmpeg muxed diese Segmente in einen Container mit der **vollen Laufzeit** (z.B. 3:30) — stille Segmente = Stille in der Datei -- `mutagen` liest die Container-Länge (3:30 ✓) → Duration-Check besteht -- `probe_audio_quality()` prüft Format/Bitrate/Sample-Rate → alles OK -- Niemand prüft den **tatsächlichen Audioinhalt** — die Stille bleibt unerkannt - -**Qobuz hat bereits einen Fix** (`duration_s < 35` in `qobuz_client.py:1315`) — aber der greift nur wenn die Datei selbst kurz ist, nicht beim Silence-Padding. - -**Geplanter Fix:** `ffmpeg silencedetect` nach dem Download. Wenn die letzten X% der Datei unter -50dB → Preview → Quarantäne/Retry. - -**Offene Frage (erst klären!):** Ist das ein bekanntes Monochrome-Problem (Preview-Limitation der API), oder ein Bug im HLS-Downloader von SoulSync? - ---- - -## Was noch fehlt — nächste Schritte - -### A) Andere Quellen anbinden (Source-Mapper) -Diese Quellen geben keine echten Werte, haben aber definierte Tier-Strings die wir mappen: - -| Quelle | Tier-String | AudioQuality | -|--------|------------|--------------| -| Tidal | `'hires'` | `flac, 24bit, 96kHz` | -| Tidal | `'lossless'` | `flac, 16bit, 44.1kHz` | -| HiFi | gleich wie Tidal | — | -| Deezer | `'flac'` | `flac, 16bit, 44.1kHz` | - -**Wo**: `core/quality/model.py` — `TIDAL_TIER_MAP`, `DEEZER_TIER_MAP` hinzufügen -**Warum**: Damit `filter_and_rank()` auch für diese Quellen entscheiden kann ob eine Quelle das Ziel erfüllt (z.B. "ich will 24-bit, Tidal lossless reicht nicht") - -**Qobuz ist Sonderfall** — liefert echte `maximum_sampling_rate` + `maximum_bit_depth` aus der API, diese direkt in `AudioQuality` befüllen. - -### B) UI — Ranked Targets als editierbare Liste -Aktuell zeigt das Settings-UI noch die alte `qualities`-Ansicht (FLAC on/off, MP3 320 on/off). - -Neu: Drag & Drop Prioritätsliste in den Settings: -``` -[↕] FLAC 24-bit/192kHz [🗑] -[↕] FLAC 24-bit/96kHz [🗑] -[↕] FLAC 24-bit/44.1kHz [🗑] -[↕] FLAC 16-bit [🗑] -[↕] MP3 320kbps [🗑] -[+ Ziel hinzufügen] -``` - -**Wo**: `webui/static/settings.js` + `webui/index.html` -**API**: `GET/POST /api/quality-profile` existiert bereits, liefert jetzt v3 - -### C) Quarantäne-UI — Grund anzeigen -Wenn eine Datei wegen Quality quarantiniert wird, sollte das UI den Grund klar anzeigen: -- Welches Target wurde gesucht -- Was die Datei tatsächlich hat -- "Approve" Button setzt `_skip_quarantine_check = 'quality'` - -### D) Tests -- `tests/quality/test_model.py` — `AudioQuality.matches_target()`, `filter_and_rank()`, Migration -- `tests/imports/test_quality_guard.py` — `check_quality_target()` mit verschiedenen Szenarien - ---- - -## Branch auf neuem PC holen - -```bash -git clone https://github.com/nick2000713/SoulSync.git -cd SoulSync -git fetch origin -git checkout feature/global-quality-system -``` - -Oder wenn der Fork schon geklont ist: -```bash -git fetch origin -git checkout feature/global-quality-system -``` diff --git a/docs/superpowers/specs/2026-06-14-best-quality-search-mode-design.md b/docs/superpowers/specs/2026-06-14-best-quality-search-mode-design.md deleted file mode 100644 index 3cf65627..00000000 --- a/docs/superpowers/specs/2026-06-14-best-quality-search-mode-design.md +++ /dev/null @@ -1,201 +0,0 @@ -# Best-Quality Search Mode — Design - -**Date:** 2026-06-14 -**Branch:** (new, off current `fix/import-folder-artist-override-optin`) -**Status:** Approved approach (Option A), pending spec review - -## Goal - -Add a user-toggleable download strategy. Today hybrid search is **priority-first**: -`engine.search_with_fallback` walks the source chain in priority order and accepts -the **first** source that meets a quality target — so a passable Soulseek 16-bit FLAC -"wins" even when HiFi/Qobuz/Tidal could deliver a 24-bit version of the same track. - -The new **best-quality** mode instead searches **all** configured sources, pools their -candidates, and works them **best→worst by actual audio quality**. Source priority -becomes only a tiebreaker between equally-good candidates. - -## Hard constraints (from the user) - -1. **Two independent toggles.** The new `search_mode` and the existing - `post_processing.retry_exhaustive` are orthogonal. The feature must behave - correctly in **all four** combinations (priority/best × default/exhaustive budget). -2. **Budget semantics preserved 1:1.** No change to how retries are counted: - - Default mode: single global cap `MAX_QUARANTINE_RETRIES = 5` across all sources. - - Exhaustive mode: per-source budget `query_count × retries_per_query`. - When a source **completely spends** its budget, **all of that source's candidates - are removed from the entire pooled list** (not just skipped once). -3. **Do not touch the query generator.** `matching_engine.generate_download_queries` - and the legacy query-building block in `download_track_worker` stay exactly as-is. -4. **`force_import` never fires on quality** (unchanged existing invariant — only - AcoustID mismatches can force-import). - -## Key realization: the budget-removal mechanism already exists - -When a source spends its budget, `monitor.requeue_quarantined_task_for_retry` -already adds it to the task's **`exhausted_download_sources`** set, and the worker -already passes that set as `exclude_sources` to its searches. Constraint #2's -"remove the source from the whole list" is therefore **not new logic** — best-quality -mode simply consults the *same* set when assembling/filtering its pooled candidate -list. No new budget bookkeeping is introduced. - -## Architecture - -### The one genuinely new piece: quality-dominant candidate ordering - -`attempt_download_with_candidates` (core/downloads/candidates.py:71) sorts candidates -**confidence-first, then `quality_score`**. That is correct for priority mode (never -download a high-quality *wrong* file). For best-quality mode we keep the -"correct-first" guarantee — `get_valid_candidates` already drops candidates below the -match threshold, so **every candidate in the list is correctly matched** — but among -those valid candidates we want **profile quality rank to dominate**, with confidence as -the tiebreaker. - -Add an opt-in parameter: - -```python -attempt_download_with_candidates(task_id, candidates, track, batch_id=None, - deps=None, *, quality_first=False) -``` - -- `quality_first=False` (default): today's sort, byte-for-byte unchanged. -- `quality_first=True`: sort key becomes - `(profile_quality_rank, confidence, upload_speed, -queue_length, free_slots, size)`, - i.e. profile quality dominates, all existing signals become tiebreakers. - -`profile_quality_rank` is derived from the candidate's stamped `AudioQuality` against -the user's `ranked_targets` (reusing `core.quality.model` / `filter_and_rank` ordering). -Candidates with no usable quality info sort last (rank = worst), never dropped. - -### New engine method: `search_all_sources` - -Mirror of `search_with_fallback`, but it does **not** stop at the first satisfying -source. It iterates every configured, non-excluded source in the chain, runs each -source's `search`, and returns the **combined** raw track list (each track already -quality-stamped by its client's `set_quality`, as today). Per-source exceptions are -swallowed exactly like the existing method. Excluded (`exhausted`) sources are skipped. - -```python -async def search_all_sources(self, query, source_chain, timeout=None, - progress_callback=None, exclude_sources=None) - -> Tuple[list, list] # (combined_tracks, combined_albums) -``` - -Quality ranking is **not** done here — the orchestrator/worker owns final ranking, same -division of labour as today (engine returns raw, orchestrator filters/ranks). - -### Orchestrator wiring - -`download_orchestrator.search(...)` gains awareness of `search_mode`: -- `priority` (default) → unchanged: `search_with_fallback`. -- `best_quality` **and** `mode == 'hybrid'` → `search_all_sources`. -- Single-source mode → unchanged regardless of `search_mode` (only one source exists; - its candidates are still quality-ranked downstream, so the toggle is a no-op there). - -A thin helper `load_search_mode() -> str` reads the quality profile (see Config). - -### Worker integration (the per-query loop stays intact) - -In `download_track_worker`'s existing query loop (core/downloads/task_worker.py:398), -when best-quality mode is active: - -1. The loop and the query generator are untouched. -2. `orchestrator.search(query, exclude_sources=_exclude_sources)` now returns - **pooled** results from all non-exhausted sources (via `search_all_sources`). -3. `candidates = get_valid_candidates(pooled, track, query)` — unchanged. -4. `ranked, _ = rank_for_profile(candidates)` — orders best→worst by profile quality. -5. `attempt_download_with_candidates(task_id, ranked, track, batch_id, quality_first=True)`. - -Because the pool already came from every source, the **hybrid-fallback block** -(task_worker.py:529–593, which re-tries `hybrid_order[1:]` individually) is -**redundant in best-quality mode** and is skipped — it would just re-search sources the -pool already covered. In priority mode it runs exactly as today. - -The `exhausted_download_sources` set is already folded into `_exclude_sources` -(task_worker.py:446), so a budget-spent source is excluded from `search_all_sources`'s -pool automatically — satisfying constraint #2 with no new code. A belt-and-suspenders -filter drops any straggler exhausted-source candidates before step 5. - -## Config / persistence - -`search_mode` belongs to the quality profile (it *is* a quality-driven policy). Stored -in the v3 quality-profile JSON (`database.music_database.get_quality_profile`): - -```json -{ "version": 3, "search_mode": "priority", ... } -``` - -- Default: `"priority"` (preserves today's behavior for every existing install). -- Accessor: extend `core/quality/selection.py` with `load_search_mode() -> str` - returning `'priority'` unless the profile says `'best_quality'`. -- No migration needed: a profile lacking the key reads as `'priority'`. - -## UI - -A single toggle in the existing **Quality Profile** tile (webui/index.html + -webui/static/settings.js), e.g. a labelled switch: - -> **Search strategy:** ( ) Source priority — fastest, stops at first good source -> (•) Best quality — searches all sources, picks the highest quality - -With a short note that best-quality searches **all** sources every track (slower / more -API calls). `collectQualityProfileFromUI` emits `search_mode`; `populateQualityProfileUI` -reads it. No change to the ranked-targets editor or the fallback toggle. - -## Behaviour matrix (all four combinations must hold) - -| search_mode | retry_exhaustive | Behaviour | -|---|---|---| -| priority | off | **Unchanged today.** First satisfying source wins; 5 global retries. | -| priority | on | **Unchanged today.** First source wins; per-source budgets. | -| best_quality | off | Pool all sources, best→worst; 5 **global** retries total. A source can't be "removed from the pool" because there's no per-source budget — the global cap fires first. (Documented: combine best-quality with exhaustive for per-source removal.) | -| best_quality | on | Pool all sources, best→worst; each source has `query_count × retries_per_query`; on spend → source removed from the whole pool via `exhausted_download_sources`. **This is the user's target configuration.** | - -## Error handling - -- `search_all_sources`: fail-open per source (swallow exceptions, keep going), identical - to `search_with_fallback`. If the whole pool is empty, the worker falls through to its - existing not-found handling. -- Quality-rank computation: any candidate that can't be ranked sorts last, never crashes - the walk (mirrors the engine's existing fail-open ranking). - -## Testing (TDD) - -New tests under `tests/quality/` and `tests/downloads/`: - -1. **`load_search_mode`** — defaults to `'priority'`; returns `'best_quality'` when set; - unknown value falls back to `'priority'`. -2. **`search_all_sources`** — returns combined candidates from multiple fake sources; - skips excluded/exhausted sources; swallows a raising source; empty when all empty. -3. **quality-first ordering** — `attempt_download_with_candidates(quality_first=True)` - tries a 24-bit candidate before a higher-confidence 16-bit one; `quality_first=False` - reproduces today's confidence-first order exactly (regression lock). -4. **budget-removal in pool** — given an exhausted source, its candidates never appear in - the walked list, while other sources' candidates remain (exhaustive mode). -5. **toggle independence** — orchestrator chooses `search_all_sources` only when - `search_mode == best_quality and mode == hybrid`; single-source and priority paths - call `search_with_fallback` (or single-client search) unchanged. - -Each test written first, watched fail, then minimal code to green. No production code -before a failing test. - -## Files touched - -- `core/quality/selection.py` — add `load_search_mode()`; small quality-rank helper for the sort key (or reuse model). -- `core/download_engine/engine.py` — add `search_all_sources`. -- `core/download_orchestrator.py` — branch `search()` on `search_mode`. -- `core/downloads/candidates.py` — add `quality_first` param + alternate sort key. -- `core/downloads/task_worker.py` — pass pooled results + `quality_first=True`; skip the redundant hybrid-fallback block in best-quality mode. (Query generator block untouched.) -- `database/music_database.py` — include `search_mode` default in the v3 profile. -- `webui/index.html`, `webui/static/settings.js` — the toggle. -- `tests/quality/`, `tests/downloads/` — the tests above. - -## Explicitly out of scope - -- No change to the query generator. -- No change to the budget counters / `MAX_*` constants / `requeue_quarantined_task_for_retry`. -- No change to AcoustID / force_import behavior. -- No cross-query pooling: best-quality pools across **sources** within each query of the - existing loop. The primary query already covers all sources, so this captures the best - candidate without restructuring the loop. Later queries remain per-source fallbacks for - the empty-result case. diff --git a/docs/superpowers/specs/2026-06-14-global-quality-system-design.md b/docs/superpowers/specs/2026-06-14-global-quality-system-design.md deleted file mode 100644 index df7dbbfa..00000000 --- a/docs/superpowers/specs/2026-06-14-global-quality-system-design.md +++ /dev/null @@ -1,180 +0,0 @@ -# Global Quality System — Source Binding, Ranking & UI - -**Branch:** `feature/global-quality-system` -**Date:** 2026-06-14 -**Status:** Approved design — ready for implementation plan - -## Problem - -The quality model (`core/quality/model.py`) is complete and Soulseek already -uses it, but the system has no cross-cutting quality behaviour: - -1. **Streaming sources don't populate real quality.** `SearchResult.audio_quality` - is a derived property over `format/bitrate/sample_rate/bit_depth`, but Tidal, - HiFi, Deezer, Qobuz, Amazon, YouTube never fill `sample_rate`/`bit_depth` (and - sometimes not even the right `format`). Their `audio_quality` therefore falls - back to crude kbps heuristics. -2. **Ranking is inconsistent.** Only the Soulseek path runs - `filter_results_by_quality_preference`. Streaming results are ordered by - match-confidence only — quality is ignored. -3. **No quality-aware source fall-through.** `search_with_fallback` is - "first non-empty source wins": it never escalates to the next source when the - current source can't deliver the wanted quality. -4. **No UI for the v3 ranked-target list.** The profile model is v3 (ordered - target list) but there is no editor for it. - -## Decisions (locked) - -- **Per-Source Population, not cross-source pooling.** Source priority (the - hybrid chain order) stays king. Each source populates an accurate - `audio_quality`; the chain fall-through becomes quality-aware so a source that - cannot meet *any* target is skipped in favour of the next source. -- **Full scope:** source mappers (A) + ranking wiring + streaming-path unify, - UI ranked-target editor (B), quarantine-reason surfacing (C), tests (D). -- **Bitrate is a settable minimum threshold (a range "≥ X"), never an exact - match.** Lossless (FLAC/WAV) is matched on `bit_depth`/`sample_rate`; bitrate is - only a fallback heuristic when those are absent. This is already how - `AudioQuality.matches_target` behaves — the work is to expose it correctly in - the UI and add a small VBR tolerance for lossy presets. - -## Hard constraints (must not regress) - -These are already correct in the codebase and the new work must preserve them: - -- **Retry harmony.** A quality reject already flows through the same retry path as - AcoustID: `check_quality_target` → `move_to_quarantine(trigger='quality')` → - `requeue_quarantined_task_for_retry(..., 'quality')` (pipeline.py:584-620). The - worker walks the next-best candidate using the per-source retry budget. New code - must keep `check_quality_target` returning a reason string that feeds this path. -- **force_import isolation.** `force_imported` status is set ONLY by the AcoustID - version-mismatch fallback (`core/imports/version_mismatch_fallback.py`). The - quality guard sets NO verification status — it quarantines with - `trigger='quality'` and the bypass flag `_skip_quarantine_check='quality'`. A - quality mismatch must NEVER become `force_imported`. force_import stays reserved - for AcoustID mismatches. - -## A — Source mappers - -New module **`core/quality/source_map.py`** centralises each source's tier -knowledge (kept out of `model.py` to avoid bloat). Each download client populates -the four quality fields (`quality`, `bitrate`, `sample_rate`, `bit_depth`) of its -`TrackResult` via these helpers; `audio_quality` then derives automatically. - -Each tier value is a **claim**, verified post-download by `check_quality_target` -reading the real file. Ranking uses the claim to pick; the guard catches lies. - -| Source | Source of values | Mapping | -|--------|------------------|---------| -| **Soulseek** | slskd attrs type 4 (sample_rate) / 5 (bit_depth) — real | already done (`AudioQuality.from_slskd_file`) | -| **Qobuz** | API `maximum_sampling_rate` (kHz) + `maximum_bit_depth` — real | `sample_rate = rate*1000`, `bit_depth`, `format='flac'` | -| **Deezer** | config code `flac`/`mp3_320`/`mp3_128` | flac→16-bit/44.1kHz · mp3_320→320kbps · mp3_128→128kbps | -| **Tidal** | track `audioQuality` tier | HI_RES_LOSSLESS/HI_RES→flac 24/96 · LOSSLESS→flac 16/44.1 · HIGH→aac 320 · LOW→aac 96 | -| **HiFi / Monochrome** | Tidal-backed; config quality key | same map as Tidal | -| **Amazon** | real `sampleRate` when present, else tier | UHD→24/96 · HD→16/44.1; prefer real sampleRate | -| **YouTube / SoundCloud** | yt-dlp / stream `format` + `abr` | lossy: format + bitrate, no bit_depth | - -Helper shape: - -```python -# core/quality/source_map.py -TIDAL_TIER_MAP: dict[str, AudioQuality] -AMAZON_TIER_MAP: dict[str, AudioQuality] - -def quality_from_tidal_tier(tier: str) -> AudioQuality: ... -def quality_from_qobuz(sampling_rate_khz: float, bit_depth: int) -> AudioQuality: ... -def quality_from_deezer(code: str) -> AudioQuality: ... -def quality_from_amazon(stream_info: dict) -> AudioQuality: ... -``` - -Clients copy the result onto the `TrackResult` fields (small helper -`TrackResult.set_quality(aq)` to avoid four-line repetition at each call site). - -## Wiring — quality-aware fall-through - -New helper **`core/quality/selection.py`**: - -```python -def rank_for_profile(candidates) -> tuple[list, bool]: - """Return (ranked_candidates, satisfied_a_target). - - satisfied = filter_and_rank(fallback_enabled=False) produced anything. - """ -``` - -`search_with_fallback` (`core/download_engine/engine.py`) changes from -"first non-empty wins" to: - -``` -best_fallback = [] -for source in chain: - tracks, albums = source.search(query) - if not tracks: continue - ranked, satisfied = rank_for_profile(tracks) - if satisfied: - return ranked, albums # this source meets a target → done - best_fallback = best_fallback or (ranked, albums) -# chain exhausted, nothing satisfied a target -return best_fallback if fallback_enabled else ([], []) -``` - -**Key behavioural note:** with source-priority-king plus a long target list -(down to MP3 192), fall-through only triggers when a source meets *no* target at -all (below the floor) — e.g. user wants only FLAC and a source has only MP3. -Otherwise the first source that returns acceptable results wins, exactly as today -but now quality-ranked within. - -## Streaming-path unification - -In `download_orchestrator.search_and_download_best`, both paths converge on: -**match-filter first (right track), then quality-rank (best version).** The -streaming branch keeps its confidence filter (≥0.55) and then applies -`rank_for_profile` to the survivors; the Soulseek branch keeps its quality ranking -and is unchanged in spirit. No path is left quality-blind. - -## B — Ranked-targets UI - -A draggable, ordered list in the quality settings panel: - -- Each row: drag handle, label, format, the relevant constraint - (bit_depth + min_sample_rate for lossless; **min_bitrate as a settable "≥ X - kbps" field** for lossy), delete button. -- "Add target" control with format/bit_depth/sample_rate/bitrate inputs. -- `fallback_enabled` toggle. -- Persists via the existing `GET/POST /api/quality-profile` (already v3-shaped). - -Bitrate field is explicitly a **minimum threshold**, defaulting to small VBR -headroom for the common presets (e.g. a "320" preset stores `min_bitrate≈315` so -VBR/mono files near 320 still match). Lossless rows hide the bitrate field since -they match on bit_depth/sample_rate. - -## C — Quarantine reason - -Mostly wiring — the UI already carries `quarantineReason` dataset and an approve -flow (`webui/static/downloads.js`), and `check_quality_target` already returns a -"file is X, wanted Y" string. - -- Ensure the quality rejection reason is threaded into the quarantine record's - reason field and rendered in the track-detail modal (what was wanted vs what the - file actually is). -- "Approve anyway" sets `_skip_quarantine_check='quality'` (the bypass already - honoured at pipeline.py:584-585). - -## D — Tests - -- `tests/quality/test_source_map.py` — each mapper produces the expected - `AudioQuality` (Tidal tiers, Qobuz kHz→Hz, Deezer codes, Amazon real-vs-tier, - lossy no bit_depth). -- `tests/quality/test_selection.py` — `rank_for_profile` satisfied/unsatisfied; - fall-through: source A below floor → engine escalates to source B; nothing - matches with `fallback_enabled` on (best returned) vs off (empty). -- Extend `tests/quality/test_model.py` — `matches_target` bitrate-as-minimum, - FLAC matched on bit_depth/sample_rate not bitrate, v2→v3 migration. -- `tests/imports/test_quality_guard.py` — `check_quality_target` quarantine - reason scenarios **plus a regression test asserting a quality mismatch uses - `trigger='quality'` and never sets `force_imported`.** - -## Out of scope - -- Cross-source candidate pooling (rejected in favour of per-source population). -- The Monochrome/HiFi 30-second-silence bug (tracked separately in `PLAN.md`; - needs ffmpeg `silencedetect`, not tier mapping).