fix: handoff audit — enforce config validation, repair broken provider paths, add guardrails - #53
Merged
Merged
Conversation
…r paths, add guardrails Phase-3/4 output of the 2026-07 principal-architect handoff audit. Every fix is locked by tests/test_handoff_invariants.py (19 tests, invariants I1-I8) and documented in docs/ARCHITECTURE.md / docs/BACKLOG.md. Correctness (production path): - Config validation rules were declared but NEVER executed: BaseConfig._validate() had an empty body. It now runs every registered rule against the dataclass fields, plus a new cross-field rule (overlap_duration < segment_length). - split_audio: guard against non-positive step (segment_length <= overlap_duration previously hung in an infinite while-loop; config is mutable post-load so the config-time check alone is insufficient). - ACRCloud provider was unusable end-to-end: the factory constructed it with no credentials (TypeError) and identify_track took raw bytes while the pipeline passes AudioSegment. Factory now reads TRACKLISTIFY_ACR_ACCESS_KEY/_SECRET/_HOST (ConfigError with actionable message when missing); identify_track accepts AudioSegment or bytes. - Provider fallback was a complete no-op: fallback_enabled, fallback_providers and --no-fallback were read by nothing. IdentificationManager now walks a primary+fallback provider chain per segment. - Circuit breaker never received request outcomes; new public RateLimiter.record_result() is called on every provider success/failure. - Cache TTL was disabled (set() stored ttl=None which shadowed the strategy default -> is_valid always True) and the cache index was only persisted from cleanup()/clear(), so entries written by one process were invisible to the next and then deleted as "orphans". set()/delete() now persist the index; index save fsyncs before rename. Smaller fixes: - cli: fail fast with an actionable error when ffmpeg is missing. - exporters: output_dir mkdir(parents=True). - Spotify 429 now carries structured retry_after on RateLimitError. - commitizen version_provider poetry -> pep621 (cz bump was broken since the uv migration). - scripts/generate_config_docs.py called a method that never existed; now uses ConfigDocGenerator. - Removed dead module-level identify_tracks() that iterated a string as segments. Infrastructure: - .github/workflows/ci.yml: ruff lint + format check, .env.example drift check, pytest on 3.11/3.12/3.13 (repo previously had NO CI). - docs/ARCHITECTURE.md: load-bearing map, implicit contracts, landmines, touch-X-update-Y couplings. - docs/PLAYBOOKS.md: step-by-step procedures for recurring changes. - docs/BACKLOG.md: residual risk register + prioritized backlog. - CLAUDE.md: corrected stale gotchas, new invariant pointers. - .env.example regenerated (ACR credential vars incl. new _HOST). 388 tests pass (369 existing + 19 new invariants).
Contributor
There was a problem hiding this comment.
Pull request overview
This PR implements a “handoff audit” remediation pass across Tracklistify’s core pipeline, focusing on enforcing previously-dead config validation, repairing provider construction/compatibility, and adding self-enforcing guardrails (CI + invariant tests + documentation) so regressions are caught automatically.
Changes:
- Enforced config validation at construction time and added segmentation safety checks to prevent infinite loops.
- Repaired provider factory/provider interfaces (notably ACRCloud) and implemented a primary→fallback provider chain with circuit-breaker outcome reporting.
- Hardened cache TTL/index persistence behavior and added CI + invariant tests + handoff documentation/playbooks.
Reviewed changes
Copilot reviewed 24 out of 25 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Dependency lock updates (aiohttp/idna/pytest/etc.). |
| pyproject.toml | Dependency bumps, commitizen pep621 version provider, pytest marker registration. |
| .github/workflows/ci.yml | Added CI running ruff + drift check + pytest matrix with ffmpeg installed. |
| src/tracklistify/config/base.py | Made validation rules actually run; added cross-field overlap/segment invariant. |
| src/tracklistify/core/base.py | Added runtime segmentation step guard; added ffmpeg presence logging in split_audio. |
| src/tracklistify/cli.py | Added CLI fail-fast ffmpeg check; minor logging formatting cleanup. |
| src/tracklistify/utils/rate_limiter.py | Added RateLimiter.record_result() API to feed circuit breaker. |
| src/tracklistify/utils/identification.py | Implemented provider fallback chain, AsyncExitStack lifecycle mgmt, and circuit-breaker reporting. |
| src/tracklistify/providers/factory.py | Fixed ACRCloud construction via env creds; added KNOWN_PROVIDERS and improved unknown-provider errors. |
| src/tracklistify/providers/acrcloud.py | Updated identify_track to accept AudioSegment-like inputs (file-backed) as well as bytes. |
| src/tracklistify/providers/spotify.py | Included structured retry_after details in raised RateLimitError. |
| src/tracklistify/exporters/tracklist.py | Ensured output dir creation includes parents. |
| src/tracklistify/cache/base.py | Fixed TTL stamping so “no explicit ttl” inherits cache-level TTL. |
| src/tracklistify/cache/storage.py | Persisted cache index on every set/delete to prevent cross-process invisibility/data loss. |
| src/tracklistify/cache/index.py | Added fsync-before-rename durability for index writes. |
| scripts/generate_env_example.py | Expanded credentials block documentation (ACRCloud host, Spotify notes). |
| scripts/generate_config_docs.py | Fixed generator to use ConfigDocGenerator instead of a nonexistent config method. |
| .env.example | Updated generated env example comments for credentials. |
| tests/test_handoff_invariants.py | Added invariant test suite (I1–I8) locking the audit fixes in place. |
| tests/test_logger.py | Assertion formatting adjustments. |
| tests/test_error_handling.py | Assertion formatting adjustments. |
| docs/ARCHITECTURE.md | Added handoff architecture map + invariants/landmines/couplings documentation. |
| docs/PLAYBOOKS.md | Added step-by-step playbooks with verification commands and traps. |
| docs/BACKLOG.md | Added residual risk register/backlog with wiring plans and “fixed do not re-fix” list. |
| CLAUDE.md | Updated command table + added project-specific gotchas and doc pointers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+297
to
+302
| if shutil.which("ffmpeg") is None: | ||
| self.logger.error( | ||
| "ffmpeg not found on PATH — segmentation will fail. " | ||
| "Install it first (e.g. `apt install ffmpeg` or " | ||
| "`brew install ffmpeg`)." | ||
| ) |
Comment on lines
+97
to
+99
| async def identify_track( | ||
| self, audio_segment: Union[bytes, object], start_time: float = 0 | ||
| ) -> Dict: |
…fmpeg Address Copilot review on #53: - acrcloud.identify_track: Union[bytes, object] collapsed to object (useless for static checking); now Union[bytes, AudioSegment] — the two shapes the pipeline actually passes. - core/base.split_audio: missing-ffmpeg path logged then continued into the segmentation loop, producing a noisy per-segment FileNotFoundError cascade. Now raises RuntimeError with the actionable install hint (the CLI-level check remains the primary gate; this is the defense-in-depth backstop).
betmoar
force-pushed
the
claude/codebase-handoff-audit-luqkzq
branch
from
July 31, 2026 00:42
feb2d1e to
acd72fe
Compare
betmoar
added a commit
that referenced
this pull request
Jul 31, 2026
Addresses #63 review findings, Shazam error-swallowing debt, and the salvageable bug fixes from #50. Shazam circuit-breaker fix: the broad except-Exception-return-None in shazam.py swallowed all errors into None, defeating the circuit breaker wired in #53 (identification.py recorded success=True on every None). Now raises ShazamError on unexpected exceptions, matching ACRCloud's typed-exception pattern. Legitimate no-match returns stay as None. Spotify crash fix: _set_metadata called asyncio.run() from inside the running event loop of async def download(), crashing whenever a track had cover art. Now async, awaits the cover fetch directly. #63 follow-up tests: default-path proxy test (locks empty-string-to-None coercion) and TRACKLISTIFY_SHAZAM_PROXY sensitive-key assertion. Perf salvages from #50: O(n²)→O(n) track dedup via single-pass dict, cache SizeStrategy short-circuit on precomputed size, precompiled sanitizer regexes, dead get_ydl_opts() removed. Also bumps pre-commit ruff v0.8.0→v0.16.0 to match CI (they disagreed on assert formatting, causing CI lint failures). 393 tests pass, lint/format/drift clean. Closes #50.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Handoff audit: fixes + self-enforcing guardrails
Full principal-architect audit of the codebase (recon → deep audit → remediation → infrastructure). Every fix is locked by an invariant test in
tests/test_handoff_invariants.py(I1–I8), and the judgment behind them is encoded in three new docs so it survives the handoff.Correctness fixes (production path)
BaseConfig._validate()had an empty body; the ~13 type/range/path rules registered in_setup_validationwere dead code_validate()now feeds dataclass fields toConfigValidator; added cross-field ruleoverlap_duration < segment_length(I1)split_audiowhenoverlap_duration >= segment_length(step ≤ 0 → while-loop never advances)ACRCloudProvider()with no credentials (instantTypeError), andidentify_tracktook rawbyteswhile the pipeline passesAudioSegment-p acrcloudcrashed unconditionallyTRACKLISTIFY_ACR_ACCESS_KEY/_SECRET/_HOSTfrom env (secrets deliberately kept off the config dataclass), raisesConfigErrornaming the exact vars when missing;identify_trackacceptsAudioSegmentor bytes (I3, I4)fallback_enabled,fallback_providers, and--no-fallbackwere read by nothingIdentificationManagerwalks a primary→fallback provider chain per segment, with per-provider rate limiting (I5)_update_circuit_breakerhad zero production callersRateLimiter.record_result(), called on every provider outcome (I6)set()storedttl: None, which shadowed the strategy default inmetadata.get("ttl", default_ttl), sois_validalways returned Trueset()stamps the cache-level TTL (I7)cleanup()/clear()set/delete; index save now fsyncs before rename (I8)Smaller: CLI fail-fast when ffmpeg missing (was a cryptic per-segment error); exporter
mkdir(parents=True); Spotify 429 carries structuredretry_after;cz bumprepaired (version_providerwas still"poetry"post-uv-migration);scripts/generate_config_docs.pycalled a method that never existed; removed dead module-levelidentify_tracks()that iterated a string as segments.Infrastructure (the actual deliverable)
.github/workflows/ci.yml) — the repo had no CI at all. Now: ruff lint + format check,.env.exampledrift check (generate_env_example.py --check), pytest on 3.11/3.12/3.13 with ffmpeg installed.tests/test_handoff_invariants.py— 19 tests locking the 8 named invariants above; header explains each.docs/ARCHITECTURE.md— load-bearing inventory (ranked by blast radius), implicit contracts, landmines (e.g.min_confidenceis 0–1 whileTrack.confidenceis 0–100 and the knob is deliberately unwired; the cache subsystem is still not wired into the pipeline), and touch-X-update-Y couplings.docs/PLAYBOOKS.md— literal step-by-step procedures (add a provider / config option / output format; touch split_audio / rate limiter / config loading; 3am "no tracks identified" triage), each ending with exact verification commands and the real trap that motivated it.docs/BACKLOG.md— residual risk register: P1 wire the cache into identification (now safe; plan included), P2min_confidencedecision, P2 Spotify enrichment wiring vs. the broken client-credentials/me/*playlist path, P3 deaddownloaders/spotify.py, dev_cli cleanup, security polish. Plus a "fixed, do not re-fix" table.Verification
uv run python -m pytest -q→ 388 passed (369 pre-existing + 19 new), 2 environment skipsuv run ruff check/ruff format --checkonsrc/ tests/ scripts/→ cleanuv run python scripts/generate_env_example.py --check→ in syncAsyncApp.split_audio→ 4 segments at the correct 50 s stride, sorted, non-empty; step guard fires with the actionable messageACRCloudProviderTypeError via factory, out-of-rangeTRACKLISTIFY_SEGMENT_LENGTH=5000accepted pre-fix, rejected post-fix)🤖 Generated with Claude Code
https://claude.ai/code/session_01LRC2BkXVyNBfa2a6w844uU
Generated by Claude Code