feat(research): measurements read collection (chat#1796)
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=365dreturns the trailing-window annualized run-rate (a projection of the same series viacomputePlaycountDeltas).{id}is provider-neutral (ISRC or Spotify track id → resolved to ISRC). Replacestrack/historic-stats+track/playcount-deltas.GET /api/research/albums/{id}/measurements— latest measured count per track; a thin remap overgetAlbumPlaycounts. Replacesplaycounts.
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/researchsuite: 268 passed, no regressions. tsc --noEmitclean 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; supportsaggregate=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).
- GET
-
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_rateand optionalwindowlike90d(default365d).