feat(research): measurements read collection (chat#1796)

Merged
SW
4mo ago
+764-0

The read half of the chat#1796 consolidation (the slice deferred from api#668), against the docs contract in recoupable/docs#242. TDD throughout — RED→GREEN per unit.

New reads — one measurements collection

  • GET /api/research/tracks/{id}/measurements — a track's measured series from the store (granularity=daily); aggregate=run_rate&window=365d returns the trailing-window annualized run-rate (a projection of the same series via computePlaycountDeltas). {id} is provider-neutral (ISRC or Spotify track id → resolved to ISRC). Replaces track/historic-stats + track/playcount-deltas.
  • GET /api/research/albums/{id}/measurements — latest measured count per track; a thin remap over getAlbumPlaycounts. Replaces playcounts.

Layered per house style (route → handler → validate → data, one exported fn per file), reusing the existing store reads — selectSongMeasurements, computePlaycountDeltas, getAlbumPlaycounts. No vendor calls in the request path.

Non-breaking

Legacy endpoints keep serving — they're marked deprecated in docs#242 only. Nothing is removed.

Testing

  • 24 new unit tests: id resolver, both data fns (series + run_rate + the 404 paths), both validators (auth/credits/param parsing), both handlers — all green.
  • Full lib/research suite: 268 passed, no regressions.
  • tsc --noEmit clean on touched files; lint clean.

Completes the #1796 api surface

With this + api#668 (the write resource + retryable drain), the full two-resource model from #1796 is implemented: measurements (read) + measurement-jobs (write). Base test; awaits review (not self-merged).

🤖 Generated with Claude Code


Summary by cubic

Adds a unified measurements read collection for research data, implementing the read side of chat#1796. This consolidates track and album stats into two endpoints without breaking existing APIs.

  • New Features

    • GET /api/research/tracks/{id}/measurements — daily series for a track; supports aggregate=run_rate&window=365d (annualized run-rate). {id} accepts ISRC or Spotify track id (resolved to ISRC). Defaults: platform=spotify, metric=platform_displayed_play_count, window=365d.
    • GET /api/research/albums/{id}/measurements — latest measured count per track for an album (thin remap over store reads).
    • Consistent routing/validation/handler layers with CORS preflight; store-only reads (no vendor calls).
  • Migration

    • Non-breaking: legacy endpoints continue to serve (deprecated in docs). Switch to the new endpoints when ready.
    • For run-rate projections, pass aggregate=run_rate and optional window like 90d (default 365d).