Skip to content

fix: handoff audit — enforce config validation, repair broken provider paths, add guardrails - #53

Merged
betmoar merged 2 commits into
mainfrom
claude/codebase-handoff-audit-luqkzq
Jul 31, 2026
Merged

fix: handoff audit — enforce config validation, repair broken provider paths, add guardrails#53
betmoar merged 2 commits into
mainfrom
claude/codebase-handoff-audit-luqkzq

Conversation

@betmoar

@betmoar betmoar commented Jul 5, 2026

Copy link
Copy Markdown
Owner

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)

Bug Impact Fix
Config validation never ranBaseConfig._validate() had an empty body; the ~13 type/range/path rules registered in _setup_validation were dead code Any garbage env value accepted silently _validate() now feeds dataclass fields to ConfigValidator; added cross-field rule overlap_duration < segment_length (I1)
Infinite loop in split_audio when overlap_duration >= segment_length (step ≤ 0 → while-loop never advances) Hang + unbounded memory Runtime guard raises with actionable message; config is mutable post-load so the config-time check alone wasn't enough (I2)
ACRCloud provider unusable since inception — factory called ACRCloudProvider() with no credentials (instant TypeError), and identify_track took raw bytes while the pipeline passes AudioSegment -p acrcloud crashed unconditionally Factory reads TRACKLISTIFY_ACR_ACCESS_KEY/_SECRET/_HOST from env (secrets deliberately kept off the config dataclass), raises ConfigError naming the exact vars when missing; identify_track accepts AudioSegment or bytes (I3, I4)
Provider fallback was a no-opfallback_enabled, fallback_providers, and --no-fallback were read by nothing Documented feature didn't exist IdentificationManager walks a primary→fallback provider chain per segment, with per-provider rate limiting (I5)
Circuit breaker never learned_update_circuit_breaker had zero production callers Breaker could never open New public RateLimiter.record_result(), called on every provider outcome (I6)
Cache TTL disabledset() stored ttl: None, which shadowed the strategy default in metadata.get("ttl", default_ttl), so is_valid always returned True Entries never expired set() stamps the cache-level TTL (I7)
Cache index only persisted from cleanup()/clear() Entries written by one process invisible to the next, then deleted as "orphans" by the next cleanup — silent data loss Index persisted on every 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 structured retry_after; cz bump repaired (version_provider was still "poetry" post-uv-migration); scripts/generate_config_docs.py called a method that never existed; removed dead module-level identify_tracks() that iterated a string as segments.

Infrastructure (the actual deliverable)

  • CI (.github/workflows/ci.yml) — the repo had no CI at all. Now: ruff lint + format check, .env.example drift 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_confidence is 0–1 while Track.confidence is 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), P2 min_confidence decision, P2 Spotify enrichment wiring vs. the broken client-credentials /me/* playlist path, P3 dead downloaders/spotify.py, dev_cli cleanup, security polish. Plus a "fixed, do not re-fix" table.
  • CLAUDE.md — corrected a gotcha that lied (per-module logger overrides don't exist), added the new invariants and doc pointers.

Verification

  • uv run python -m pytest -q388 passed (369 pre-existing + 19 new), 2 environment skips
  • uv run ruff check / ruff format --check on src/ tests/ scripts/ → clean
  • uv run python scripts/generate_env_example.py --check → in sync
  • End-to-end with real ffmpeg: generated a 180 s audio file, ran AsyncApp.split_audio → 4 segments at the correct 50 s stride, sorted, non-empty; step guard fires with the actionable message
  • Empirically confirmed each bug before fixing (e.g. ACRCloudProvider TypeError via factory, out-of-range TRACKLISTIFY_SEGMENT_LENGTH=5000 accepted pre-fix, rejected post-fix)

🤖 Generated with Claude Code

https://claude.ai/code/session_01LRC2BkXVyNBfa2a6w844uU


Generated by Claude Code

@betmoar
betmoar marked this pull request as ready for review July 30, 2026 23:56
@betmoar
betmoar requested a review from Copilot July 30, 2026 23:56
…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).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
betmoar force-pushed the claude/codebase-handoff-audit-luqkzq branch from feb2d1e to acd72fe Compare July 31, 2026 00:42
@betmoar
betmoar merged commit 40af3e9 into main Jul 31, 2026
8 checks passed
@betmoar
betmoar deleted the claude/codebase-handoff-audit-luqkzq branch July 31, 2026 00:48
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants