Promote test → main: research endpoints + cleanup (includes PR #366)#446
Conversation
* feat: add 30 research API endpoints, 28 MCP tools, Zod validation, and tests
Research primitive — provider-agnostic music industry research:
- 30 REST endpoints under /api/research/ (Chartmetric, Perplexity, Exa, Parallel)
- 28 MCP tools with proper auth (resolveAccountId) and credit deduction
- 2 shared handlers (handleArtistResearch, handleResearchRequest) for DRY
- Zod validation on discover endpoint
- 10 test files (token, proxy, artist resolution, charts, lookup, similar, search, track, web, discover)
- Source param allowlist on metrics to prevent path injection
- proxyToChartmetric wrapped in try/catch for consistent error contract
- All 1767 tests passing, 0 lint errors in research files
Made-with: Cursor
* feat: add GET /api/research/track/playlists endpoint
Track-level playlist lookup — returns editorial, indie, and algorithmic
playlists for a specific track. Accepts Chartmetric track ID or track
name (resolved via search). Proxies to Chartmetric
/track/{id}/{platform}/{status}/playlists.
Includes route handler, domain handler, MCP tool, and 8 unit tests.
Made-with: Cursor
* fix: use Spotify-powered track search for reliable q= resolution
Adds resolveTrack() — searches Spotify first (accurate matching with
artist: filter), maps Spotify track ID to Chartmetric ID, falls back
to Chartmetric search if Spotify fails. Adds optional artist= param
to track/playlists endpoint and MCP tool.
Made-with: Cursor
* fix: resolve tracks via ISRC for reliable Chartmetric ID mapping
Spotify search returns ISRC, which maps to Chartmetric more reliably
than Spotify track ID. Tries /track/isrc/{isrc} first, then
/track/spotify/{id}, then falls back to Chartmetric text search.
Made-with: Cursor
* fix: resolve track ID via artist playlists + tracks matching
Spotify search finds exact track name, then we match against the
artist's Chartmetric playlists/tracks by name to get the cm_track ID.
Avoids Chartmetric's broken text search and unreliable ID mapping.
Made-with: Cursor
* fix: use Chartmetric /track/:type/:id/get-ids for track resolution
Maps ISRC → chartmetric_ids via the correct endpoint path. Falls back
to Spotify track ID if ISRC lookup fails. Platform-agnostic.
Made-with: Cursor
* style: fix formatting in research track playlists files
Made-with: Cursor
* fix(lint): remove unused getCorsHeaders import
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(review): SRP on chartmetric token cache; drop MCP research tools
Per review feedback on this PR:
SRP — resetTokenCache in its own file:
- Extract shared cache state into lib/chartmetric/chartmetricTokenCache.ts
- Move resetTokenCache to lib/chartmetric/resetTokenCache.ts
- getChartmetricToken now reads/writes the shared cache module
Remove MCP research tools to keep this PR focused on the API surface:
- Delete lib/mcp/tools/research/ (29 register* files + index)
- Remove registerAllResearchTools import/call from lib/mcp/tools/index.ts
- MCP tools can land in a follow-up PR once the HTTP endpoints ship
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: replace handleArtistResearch orchestrator with small composable helpers
Ports 15 artist-scoped research handlers off the template-method
orchestrator (handleArtistResearch) onto three focused helpers:
requireArtist, getArtistResearch, and jsonSuccess/jsonError. Each
handler now reads top-to-bottom in ~10 lines and explicitly names
its response key (no more magic { status } spreading collision).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: rename getArtistResearch → handleArtistResearch (no functional change)
Pure rename: file, symbol, test file, and all 15 handler call sites.
Keeps the new composable-helper implementation — only the name now
matches the legacy naming.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: consolidate JSON response helpers (DRY)
errorResponse was duplicated across lib/networking/errorResponse.ts
(existing) and a new lib/networking/jsonResponse.ts helper added with
this PR's research refactor. Merge them:
- Update errorResponse to return { status: "error", error } (was just
{ error }) so success/error envelopes are symmetric.
- Move jsonSuccess into a dedicated successResponse.ts file (one
exported function per file per repo convention).
- Delete lib/networking/jsonResponse.ts.
- Point the 15 research handlers + requireArtist at the unified
helpers.
The one pre-existing caller of errorResponse (validateAgentVerifyBody)
now gets status: "error" in its body. Its tests only assert status
codes, not body shape — no test changes needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: align research handler naming + shape with codebase conventions
- validateArtistRequest (was requireArtist) — matches validate*Request family
- Composed validators for metrics/playlists/similar endpoints whose inline
validation was non-trivial
- Add try/catch + explicit Promise<NextResponse> return type to all 15
artist-scoped research handlers
- Rename `gate` local to `validated` to match the rest of the codebase
No behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(kiss): spread validated into handleArtistResearch call
The handlers were manually re-assembling { artist, accountId } from
validated into handleArtistResearch's params object — pure ceremony
since validated already carries exactly those fields. Switch to
`{ ...validated, path, ... }` across the 12 simple handlers, and use
destructure-into-rest for the 3 that carry endpoint-specific extras.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: align charts + discover handlers with validator/helper conventions
- New handleResearchProxy({ accountId, path, query?, credits? })
returns { data } | { error, status } — same shape as
handleArtistResearch. No auth (validators own that now).
- New validateGetResearchChartsRequest.ts and renamed
validateDiscoverQuery -> validateGetResearchDiscoverRequest; both
now do auth + param validation per the validate*Request convention.
- Charts + discover handlers: try/catch, Promise<NextResponse>,
successResponse / errorResponse, compose validator + proxy helper.
- handleResearchRequest is left in place — still used by radio,
genres, curator, and festivals handlers (out of scope here).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: port final 4 research handlers; delete handleResearchRequest
- getResearchRadioHandler
- getResearchGenresHandler
- getResearchCuratorHandler
- getResearchFestivalsHandler (non-artist-scoped: /festival/list is a
global Chartmetric endpoint, so it uses handleResearchProxy)
Each now uses its own validateGet<X>Request validator and
handleResearchProxy. None of the four were artist-scoped — all four
hit global Chartmetric paths (/radio/station-list, /genres,
/curator/{platform}/{id}, /festival/list). Deletes the last
template-method orchestrator.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(kiss): rename handleResearchProxy → handleResearch, proxyToChartmetric → fetchChartmetric
Drop the "proxy" terminology per review feedback:
- handleResearchProxy → handleResearch
- proxyToChartmetric → fetchChartmetric
Renames the symbols, file names, and test file names to match.
No behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: move fetchChartmetric to lib/chartmetric
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(dry): share CHARTMETRIC_BASE between token exchange and fetchChartmetric
Extracts the base URL into lib/chartmetric/chartmetricBase.ts so both
getChartmetricToken and fetchChartmetric reuse the same const.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: port 5 non-artist research GET handlers to validator+handleResearch pattern
- getResearchLookupHandler
- getResearchSearchHandler
- getResearchTrackHandler
- getResearchTrackPlaylistsHandler
- getResearchPlaylistHandler
Each now has a dedicated validateGetResearch<X>Request.ts (auth + param
validation) and composes handleResearch + errorResponse / successResponse
inside try/catch. Removes hand-rolled auth + direct deductCredits calls
from the handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: charge credits for each Chartmetric hop in track+playlist lookups
Both getResearchTrackHandler and getResearchPlaylistHandler do a
name-resolution search before the detail fetch. Previously the resolve
step used fetchChartmetric (no credit charge), so a name-based lookup
cost the same as a direct-ID lookup (5 credits) despite hitting the
upstream twice.
Swap the resolve step to handleResearch so each successful upstream
hit deducts 5 credits. Failed searches still deduct nothing
(handleResearch skips deduction on non-200).
Cost mapping:
- Direct ID → 1 call, 5 credits (unchanged)
- Name lookup → 2 calls, 10 credits (was 5)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(review): address 9 review comments
- Rename `gate` -> `validated` in 3 composed validators (KISS)
- Spotify artist URL regex only appears in validateGetResearchLookupRequest;
no duplication, left as-is
- resolveTrack now takes accountId and calls handleResearch so
Chartmetric searches deduct credits (was hand-rolled fetchChartmetric)
- Extract validator files for 4 POST research handlers:
validatePostResearchWebRequest, ...PeopleRequest, ...ExtractRequest,
...EnrichRequest. Handlers now compose validator + existing AI-provider
call inside try/catch with Promise<NextResponse> return types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(review): tighten validateGetResearchCuratorRequest with clear errors
Chartmetric's /curator/:platform/:id endpoint only supports spotify,
applemusic, and deezer and requires a numeric curator id. Callers were
passing string handles like "spotify" / "filtr" and getting opaque
upstream 400s back.
Now reject up front with helpful messages:
- "Invalid platform. Must be one of: spotify, applemusic, deezer"
- "id must be a numeric Chartmetric curator ID (e.g. 2 for Spotify)"
Matches the tightened docs spec in recoupable/docs#137.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(research/track): add artist disambiguation + better match ranking
Addresses PR #366 review (#366 (comment)):
every common track query ("God's Plan", "Hotline Bling", "Flowers",
"Sicko Mode", "Thriller") returned the wrong track because the resolver
called Chartmetric /search?type=tracks&limit=1 and took tracks[0] with
no ranking and no artist filter.
Changes:
- Raise the upstream limit to 25 (was 1) so real candidates make it into
the pool.
- New pickBestTrackMatch(tracks, q, artist?) — pure ranking function:
1. If artist supplied, drop candidates whose artist_names don't
contain artist (case-insensitive substring).
2. Within the remaining pool, prefer a track whose name equals q
(trimmed, case-insensitive) — fixes "God's Plan" vs "God's".
3. Otherwise fall back to the first remaining track.
- Validator accepts an optional `artist` query param.
- Handler returns 404 with both q and artist in the message when the
artist filter yields nothing (matches the OpenAPI 404 added in
recoupable/docs#138).
Docs PR (now merged): recoupable/docs#138 tightens the /research/track
spec to advertise the new `artist` param, the 404 response, and the
actual response shape (artists[], albums[], etc. — not the wrong
album_name/artist_names the spec claimed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(research/search): pass through beta, platforms, offset to Chartmetric
Chartmetric's default search engine returns exactly 1 result for every
track query (verified against preview: q=Hotline Bling → only T703R
'Bling', q=Flowers → only a Yuda cover, q=Drake&type=tracks → 1 track).
Its docs describe beta=true as "improved beta search engine for higher
relevance and accuracy", and platforms[] is beta-only.
This adds diagnostic passthrough so we can observe the beta response
shape live, and unblocks follow-up work on /api/research/track. All
three params are optional and only forwarded when explicitly set, so
default behavior is unchanged.
Handler also falls back to `suggestions` (beta response shape) when no
artists/tracks/albums array is populated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(research): /track and /playlist become pure ID proxies (KISS)
The original handlers chained /search → /track/:id (and /search → /playlist/:platform/:id
on non-numeric ids). The compound behavior produced wrong results silently,
unpredictable credit charges, and asymmetric error paths that we kept having
to patch (see #366 review). We're reverting both endpoints to
single-call proxies. Discovery is the caller's job via GET /api/research —
which already exposes beta=true for high-relevance search ranking
(a28deb2).
/api/research/track
- Takes a numeric Chartmetric track `id`, single upstream call to /track/:id
- Drops the `q` search param, the `artist` disambiguation param, and the
client-side pickBestTrackMatch ranking helper (not needed — search is the
caller's job)
- 400 when `id` is missing or non-numeric
/api/research/playlist
- Unchanged public shape: still takes platform + id
- Drops the non-numeric-id name-search fallback inside the handler
- Single upstream call to /playlist/:platform/:id for all inputs
Tests: 146 research tests green. `pickBestTrackMatch` and its 8 unit tests
deleted (never shipped; only lived on this branch in fda5592).
Follow-up docs PR will align /research/track's OpenAPI contract with the
new shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(research): /albums id-proxy + /charts enum validation (KISS)
Addresses the remaining two items from the PR #366 review comment.
/api/research/albums
- Replaces the composite name→artist_id→discography flow (same shape
that was returning "DJ Mix" feeds for artist=Drake and duplicated
"Elizabeth Taylor" albums for artist=Taylor Swift) with a thin
/artist/:id/albums proxy.
- New dedicated validator validateGetResearchAlbumsRequest.ts takes a
numeric artist_id; drops the old name/UUID fuzzy-resolve path.
- Discovery is the caller's job via GET /api/research?type=artists&beta=true.
- Note: the shared validateArtistRequest + handleArtistResearch
composites still power ~17 other artist-scoped endpoints (similar,
metrics, cities, audience, career, etc.). Those carry the same risk
class and should get the same treatment in follow-up PRs — out of
scope here.
/api/research/charts
- Validator now enforces the documented enums at our layer:
type ∈ {regional, viral}
interval ∈ {daily, weekly}
latest ∈ {true, false}
- This converts opaque upstream 400s (e.g. type=top → Chartmetric 400)
into specific 400s from us that name the valid values. Nothing is
silently ignored: params not in the spec (date, artist) were never
forwarded and now surface clearly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(research/albums): default isPrimary=true to exclude features
Per Chartmetric docs, /artist/:id/albums has an isPrimary query param
that defaults to false, meaning the response includes every album where
the artist is *featured* — not just their own discography. That's why
canonical ids were returning wrong data on the preview:
artist_id=3380 (Drake) → "nila: welcome to the aprtment (DJ Mix)" etc.
artist_id=3963 (Ariana Grande) → "Wicked: For Good" soundtrack etc.
Our handler now defaults to isPrimary=true so "albums" means the
artist's own discography by default. Callers who actually want features
and compilations can opt in with is_primary=false.
Also exposes limit and offset for pagination (Chartmetric defaults to
limit=100, offset=0). Sort params (sortColumn, sortOrderDesc) not
exposed — upstream defaults (release_date desc) are sensible; can
add later if a caller asks.
Validator rejects:
- is_primary values other than "true"/"false" (400)
- non-positive-integer limit (400)
- negative offset (400)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Recoupable <sidney@recoupable.com>
Co-authored-by: Sweets Sweetman <sweetmantech@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 44 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (40)
📒 Files selected for processing (96)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Promotes the current state of `test` to `main`.
Headline change — #366
30 research API endpoints + 28 MCP tools + Zod validation + tests.
Includes the KISS refactor that fixed the bugs flagged during preview testing:
Full verification against "El Gran Combo de Puerto Rico" on the preview: #366 (comment) (and 5 follow-up parts).
Docs mirroring this surface have already been merged: recoupable/docs#139, #140.
Test plan
🤖 Generated with Claude Code
Summary by cubic
Promotes the current
teststate tomainwith a new research API surface: 30 endpoints under/api/research, a Chartmetric proxy with token caching, and standardized JSON responses. Fixes wrong matches in track/playlist/albums flows and replaces opaque errors with clear validation.New Features
/web,/enrich,/extract,/people).fetchChartmetricwith auth token exchange/cache andobj-wrapper stripping; sharedCHARTMETRIC_BASE./api/researchsearch now passes throughbeta,platforms, andoffset.successResponse/errorResponsereturn{ status: "success" | "error" }.Migration
/api/research/trackis now an ID-only proxy;qandartistparams removed. Use/api/research?q=for discovery./api/research/playlistno longer falls back to name search; requiresplatform+ numericid./api/research/albumsrequires numericartist_id; defaultsis_primary=true; supportslimit/offset./api/research/chartsvalidates enums fortype(regional|viral),interval(daily|weekly), andlatest(true|false) to prevent opaque upstream 400s.{ status: "success" | "error" }; clients expecting{ error }only should adjust.Written for commit 8dcaa24. Summary will update on new commits.