Skip to content

docs: tighten /research/track spec — add artist disambiguation + 404#138

Merged
sweetmantech merged 2 commits into
mainfrom
docs/research-track-disambiguation
Apr 16, 2026
Merged

docs: tighten /research/track spec — add artist disambiguation + 404#138
sweetmantech merged 2 commits into
mainfrom
docs/research-track-disambiguation

Conversation

@sweetmantech
Copy link
Copy Markdown
Collaborator

@sweetmantech sweetmantech commented Apr 16, 2026

Summary

Surfaced while testing api PR recoupable/api#366 on preview (comment): every common track query ("God's Plan", "Hotline Bling", "Thriller", "Sicko Mode", "Flowers") returned the wrong track.

Root cause is upstream behavior: Chartmetric /search?type=tracks&limit=1 picks an arbitrary low-quality match with no popularity sort and no upstream `artist` filter (confirmed against the Chartmetric Search docs — no `artist` param, no sort param, default ordering is whatever upstream's relevance engine returns).

This PR tightens the spec so callers can disambiguate, and the api PR will follow with the implementation.

Spec changes

  • Endpoint description explains the resolver flow and recommends passing a Spotify URL or the new `artist` param for ambiguous names.
  • `q` description: clarify that plain names are searched (and may be ambiguous), while Spotify URLs resolve directly.
  • Add optional `artist` query param for case-insensitive disambiguation against candidate tracks' `artist_names`.
  • Add 404 response for the case where no track matches.

Follow-up

Implementation in the api repo (recoupable/api) follows in a separate PR — handler will switch Chartmetric's beta search engine on, raise `limit`, sort by `match_strength`, and apply the new `artist` filter against `artist_names[]` before picking `[0]`.

Test plan

  • Mintlify preview renders the updated `/research/track` page (title, params table, 404 row)
  • `artist` shows as optional in the param table
  • Endpoint description renders the disambiguation note as expected

🤖 Generated with Claude Code


Summary by cubic

Tightened the /api/research/track spec to add artist disambiguation, document a 404 for no matches, and fix the response schema to match the actual API. This helps callers resolve ambiguous titles and parse a stable payload.

  • New Features

    • Added optional artist query param for case-insensitive disambiguation when q is a plain name; recommend using a Spotify track URL or artist for ambiguous titles.
    • Documented 404 when no track matches q (and artist, if provided).
  • Bug Fixes

    • Corrected response schema to match reality: removed nonexistent album_name and artist_names; enumerated artists[], albums[], genres[], image_url, duration_ms, album_label, score, explicit, and kept additionalProperties: true.

Written for commit 1bd5911. Summary will update on new commits.

Summary by CodeRabbit

  • Documentation
    • Enhanced /api/research/track endpoint documentation with improved clarity on track search resolution methods.
    • Added optional artist query parameter to help disambiguate tracks with commonly used names.
    • Documented explicit 404 error responses for queries that fail to match any tracks.

Surfaced while testing api PR recoupable/api#366 on preview: every
common track query ("God's Plan", "Hotline Bling", "Thriller", "Sicko
Mode", "Flowers") returned the wrong track. Root cause is upstream:
Chartmetric `/search?type=tracks&limit=1` picks an arbitrary low-quality
match with no popularity sort and no artist disambiguation knob.

Spec changes:
- Endpoint description explains the resolver flow and recommends
  passing a Spotify URL or the new `artist` param for ambiguous names.
- `q` description: clarify that plain names are searched (and may be
  ambiguous), while Spotify URLs resolve directly.
- Add optional `artist` query param for case-insensitive artist
  disambiguation against candidate tracks' `artist_names`.
- Add 404 response for the case where no track matches.

Implementation in the api repo follows in a separate PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

Updated OpenAPI documentation for the GET /api/research/track endpoint to clarify track resolution behavior. Added an optional artist query parameter to help disambiguate common track names, expanded q parameter documentation to distinguish URL-based direct resolution from name-based search, and added explicit 404 error response documentation.

Changes

Cohort / File(s) Summary
OpenAPI Documentation Update
api-reference/openapi/research.json
Added optional artist query parameter for track disambiguation, enhanced q parameter documentation to clarify URL vs. name-based search resolution, and added 404 error response referencing ResearchErrorResponse schema.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Poem

🐰 With artist queries new and bright,
Tracks are found with clearer sight,
URLs resolve, names disambiguate,
No 404s will make us wait! 🎵

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main changes: documentation updates to the /research/track endpoint, adding artist disambiguation and a 404 response specification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/research-track-disambiguation

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api-reference/openapi/research.json`:
- Around line 2406-2412: The OpenAPI spec now references ResearchErrorResponse
for 404 in the endpoint response block but the components/schemas description
for ResearchErrorResponse currently states it's only for 400/401; update the
ResearchErrorResponse schema description under components/schemas (symbol:
ResearchErrorResponse) to include 404 and align its wording with its usage in
responses (or alternatively adjust the endpoint's 404 response to reference the
correct schema). Ensure the schema description and every response referencing
ResearchErrorResponse (including the endpoint that currently lists 404)
consistently reflect the same set of status codes and error wording.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 277959f0-a194-4012-8f39-6fa2ae720613

📥 Commits

Reviewing files that changed from the base of the PR and between 5d7d83c and c058e90.

📒 Files selected for processing (1)
  • api-reference/openapi/research.json

Comment on lines +2406 to +2412
"404": {
"description": "No track matched the supplied `q` (and `artist`, when present)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResearchErrorResponse"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align shared error-schema wording with the new 404 usage.

You now return ResearchErrorResponse for 404 here, but Line 5021 describes that schema as only for 400/401. This creates contradictory docs.

Proposed docs fix
-        "description": "Error response returned by all research endpoints for validation failures (400) and authentication errors (401).",
+        "description": "Error response returned by research endpoints for client errors (for example, 400 validation failures, 401 authentication failures, and 404 not found where documented).",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api-reference/openapi/research.json` around lines 2406 - 2412, The OpenAPI
spec now references ResearchErrorResponse for 404 in the endpoint response block
but the components/schemas description for ResearchErrorResponse currently
states it's only for 400/401; update the ResearchErrorResponse schema
description under components/schemas (symbol: ResearchErrorResponse) to include
404 and align its wording with its usage in responses (or alternatively adjust
the endpoint's 404 response to reference the correct schema). Ensure the schema
description and every response referencing ResearchErrorResponse (including the
endpoint that currently lists 404) consistently reflect the same set of status
codes and error wording.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 1 file

Verified against preview deployment of recoupable/api#366
(https://recoup-eu765jr34-recoupable-ad724970.vercel.app) with two
queries (plain name + Spotify URL). The previous schema was badly out
of sync with reality:

- `album_name` (string) — does not exist in the response
- `artist_names` (string) — does not exist; response has `artists[]`
  (array of full Chartmetric artist objects)

Replaced with the actually-returned top-level fields: `artists[]`,
`albums[]`, `genres[]`, `image_url`, `duration_ms`, `album_label`,
`score`, `explicit`. Kept `additionalProperties: true` so lesser-used
upstream fields (`cm_statistics`, `activities`, `moods`, `tags`, etc.)
still pass through without enumerating every nullable sub-field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sweetmantech
Copy link
Copy Markdown
Collaborator Author

Added a second commit to this PR: correct the ResearchTrackResponse schema to match reality.

Verified against the preview deployment of recoupable/api#366 (https://recoup-eu765jr34-recoupable-ad724970.vercel.app) with two probe queries (plain name + Spotify URL). Signed up a fresh agent key directly on the preview to confirm env permissions were correct.

Previous schema was wrong:

  • album_name (string) — doesn't exist in the response
  • artist_names (string) — doesn't exist; actual response has artists[] (array of full Chartmetric artist objects, not a string)

New schema enumerates the actually-returned top-level fields: artists[], albums[], genres[], image_url, duration_ms, album_label, score, explicit. additionalProperties: true is kept so lesser-used upstream fields (cm_statistics, activities, moods, tags, etc.) still pass through without enumerating every nullable sub-field.

Wrong-match bug from the original comment still repros on preview: ?q=Flowers returns track id 45449427 (release 2018-11-05), not Miley Cyrus. The artist param added in this spec is the disambiguation fix — api-side implementation follows in a separate PR.

@sweetmantech sweetmantech merged commit f354644 into main Apr 16, 2026
3 checks passed
@sweetmantech sweetmantech deleted the docs/research-track-disambiguation branch April 16, 2026 15:57
sweetmantech added a commit to recoupable/api that referenced this pull request Apr 16, 2026
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>
sweetmantech added a commit that referenced this pull request Apr 16, 2026
…fied

Follow-up to #138. The api PR (recoupable/api#366) is
being simplified to strictly one-upstream-call-per-endpoint so credit
charges are predictable and ambiguity never gets silently swallowed by
composite resolution.

/research/track
- Drop q and artist query params; require numeric `id` instead.
- Description reframes the endpoint as a thin /track/:id proxy and
  points callers at GET /api/research?type=tracks&beta=true for
  discovery.
- 400 now refers to id validation, 404 refers to unknown Chartmetric id.

/research/playlist
- No schema change (still takes platform + id), but the description now
  explicitly names this a /playlist/:platform/:id proxy and points
  callers at GET /api/research for name-based discovery.
- id description calls out that format varies by platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sweetmantech added a commit that referenced this pull request Apr 16, 2026
…fied (#139)

* docs: /research/track becomes pure id-proxy; /research/playlist clarified

Follow-up to #138. The api PR (recoupable/api#366) is
being simplified to strictly one-upstream-call-per-endpoint so credit
charges are predictable and ambiguity never gets silently swallowed by
composite resolution.

/research/track
- Drop q and artist query params; require numeric `id` instead.
- Description reframes the endpoint as a thin /track/:id proxy and
  points callers at GET /api/research?type=tracks&beta=true for
  discovery.
- 400 now refers to id validation, 404 refers to unknown Chartmetric id.

/research/playlist
- No schema change (still takes platform + id), but the description now
  explicitly names this a /playlist/:platform/:id proxy and points
  callers at GET /api/research for name-based discovery.
- id description calls out that format varies by platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: clarify /research/playlist id is Chartmetric numeric, not native

Verified against preview of recoupable/api#366:
  GET /playlist?platform=spotify&id=37i9dQZF1DXcBWIGoYBM5M → 400
  GET /playlist?platform=spotify&id=848051                 → 200 RapCaviar

Chartmetric's /playlist/:platform/:id accepts Chartmetric's own
numeric playlist IDs, not the streaming platform's native IDs.
Search results already expose the correct `id` field, so the
workflow is: search via /api/research, feed `id` into /research/playlist.

- Param description now explicitly calls out "not the native ID"
- Added pattern ^[1-9][0-9]*$ + example 848051
- Endpoint description leads with "Chartmetric's own numeric playlist ID"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: /research/albums becomes pure artist_id-proxy

Mirrors the api change in recoupable/api#366 (cb6152b2). The handler
no longer does a composite name→artist_id→discography resolve (which
was returning wrong data for artist=Drake and artist=Taylor Swift on
the preview), so the spec is updated:

- Param renamed `artist` → `artist_id`
- Typed as positive-integer string (pattern ^[1-9][0-9]*$), example 3380
- Description points callers at GET /api/research?type=artists&beta=true
  for discovery
- 400 description specific to the new validation

No change needed for /research/charts — the spec already enumerates
`type` to {regional, viral}, `interval` to {daily, weekly}; the api
change tightens the validator to actually enforce those at our layer
(turning opaque upstream 400s into specific ones).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: /research/albums adds is_primary, limit, offset params

Mirrors api change in recoupable/api#366 (969f5f8a). The api now sends
isPrimary=true by default to Chartmetric so /artist/:id/albums returns
the artist's own discography (not features, soundtracks, DJ mixes).

Spec additions:
- `is_primary` (default "true") — opt into features with "false"
- `limit` (positive integer) — pagination page size
- `offset` (non-negative integer) — pagination offset

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: /api/research gains beta/platforms/offset + realistic result schema

Mirrors the api change in recoupable/api#366 (a28deb25). /api/research
has been the discovery primitive for the research endpoints since that
commit, but the spec still described it as "Search for artists by name"
and didn't advertise the new passthrough params.

Spec additions:
- Description now frames /api/research as the discovery primitive for
  /track, /playlist, /albums (via type=tracks/playlists/artists).
- Recommends beta=true for ambiguous queries, citing the upstream bug
  where default-engine returns 1 low-quality match for common terms
  like "Hotline Bling" or "Flowers".
- New params: beta (enum "true"/"false"), platforms (comma-separated,
  beta-only), offset (non-negative integer for pagination).
- ResearchSearchResult schema loosened to reflect reality: the
  default engine and beta engine return different shapes, and
  default-engine shape varies by type. Common fields enumerated;
  additionalProperties: true keeps engine-specific fields passing
  through without over-specifying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: link detail endpoints ↔ /api/research search endpoint

The discovery-then-detail flow is core to the new "one call per
endpoint" design, so the references between endpoints should be
clickable — both for humans reading the docs and for LLMs using the
.md sitemap.

Per the existing convention (e.g. the chat endpoints already use
[text](/api-reference/chat/update)), updated:

- /research/playlist: link to /api-reference/research/search in both
  the endpoint description and the `id` param description
- /research/track: same, in both places
- /research/albums: same, in both places
- /api/research (search): forward-links to the three detail endpoints
  (/albums, /track, /playlist) in the endpoint description

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: /research/curator and /research/track/playlists link to search

Audit of Chartmetric-id params after 15bee59 found two more endpoints
that require a numeric Chartmetric ID but didn't link to the discovery
endpoint:

- /research/curator (id): now links to GET /api/research?type=curators&beta=true.
  Kept the existing hint about finding curators via /research/playlists
  as a secondary path.
- /research/track/playlists (id): now links to GET /api/research?type=tracks&beta=true.
  (Note: this endpoint still has a composite `q`/`artist` name-lookup
  fallback with the same wrong-match risk as the old /research/track
  behavior; KISS-ifying it is a separate follow-up.)

All five endpoints that reference a Chartmetric id now point callers at
the same discovery primitive:
  /research/albums (artist_id) ✓
  /research/curator (id) ✓
  /research/playlist (id) ✓
  /research/track (id) ✓
  /research/track/playlists (id) ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sweetmantech added a commit to recoupable/api that referenced this pull request Apr 16, 2026
* 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>
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.

1 participant