agent: @U0AJM7X8FBR implement the wildest craziest most WOW demoable feature. M#328
agent: @U0AJM7X8FBR implement the wildest craziest most WOW demoable feature. M#328recoup-coding-agent wants to merge 6 commits intotestfrom
Conversation
…plexity → marketing copy New domain: lib/artistIntel/ - generateArtistIntelPack: orchestrates 3 parallel data sources into a complete pack - getArtistSpotifyData: fetches artist profile + top tracks with 30-sec preview URLs - getArtistMusicAnalysis: runs 4 MusicFlamingo NVIDIA presets (catalog_metadata, audience_profile, playlist_pitch, mood_tags) in parallel on a Spotify preview URL - getArtistWebContext: Perplexity web research for recent press and streaming news - buildArtistMarketingCopy: AI synthesis → playlist pitch email, social captions, press release opener, key talking points - validateArtistIntelBody: Zod input validation - generateArtistIntelPackHandler: route handler with validateAuthContext New API endpoint: POST /api/artists/intel (artist_name → complete intelligence pack) New MCP tool: generate_artist_intel_pack — available in the AI agent chat UI 10 unit tests, all green. No lint errors on new files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…feature. M Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds JSDoc across many modules and introduces an Artist Intelligence Pack feature: new artist-intel library modules (Spotify fetch, MusicFlamingo analysis, web context, marketing-copy generation, formatting), a POST API route with CORS/OPTIONS, request validation and handler plumbing, and MCP tool registration to invoke pack generation. Changes
Sequence DiagramsequenceDiagram
participant Client
participant Route as /api/artists/intel
participant Handler as generateArtistIntelPackHandler
participant Auth as validateAuthContext
participant Validator as validateArtistIntelBody
participant Orchestrator as generateArtistIntelPack
participant Spotify as Spotify API
participant Analysis as MusicFlamingo
participant Perplexity as Perplexity API
participant TextGen as generateText
participant Response
Client->>Route: POST { artist_name }
Route->>Handler: forward request
Handler->>Auth: validateAuthContext(request)
Auth-->>Handler: auth OK / reject
Handler->>Validator: validateArtistIntelBody(body)
Validator-->>Handler: validated body
Handler->>Orchestrator: generateArtistIntelPack(artist_name)
par Concurrent fetch & analyze
Orchestrator->>Spotify: getArtistSpotifyData(artist_name)
Orchestrator->>Analysis: getArtistMusicAnalysis(previewUrl)
Orchestrator->>Perplexity: getArtistWebContext(artist_name)
Spotify-->>Orchestrator: artist/topTracks/previewUrl
Analysis-->>Orchestrator: analysis results
Perplexity-->>Orchestrator: web context + citations
end
Orchestrator->>TextGen: buildArtistMarketingCopy(spotifyData, musicAnalysis, webContext)
TextGen-->>Orchestrator: marketing copy (JSON)
Orchestrator-->>Handler: { type: "success", pack } / { type: "error", error }
Handler->>Response: 200/404 JSON + CORS
Response-->>Client: Artist Intelligence Pack or error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 warning)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
lib/prompts/getSystemPrompt.ts (2)
19-132: 🛠️ Refactor suggestion | 🟠 MajorRefactor to comply with 50-line function limit.
This function spans 113 lines, which violates the coding guideline: "Keep functions under 50 lines" for domain functions in
lib/**/*.ts.Consider decomposing into smaller, focused functions:
buildUserContextSection(accountWithDetails, email)for lines 66-109buildArtistInstructionSection(artistInstruction)for lines 111-120buildKnowledgeBaseSection(knowledgeBaseText)for lines 122-129This would improve testability, readability, and compliance with SRP and the line-count guideline.
As per coding guidelines: "Keep functions under 50 lines" for domain functions in
lib/**/*.ts.
67-101: 🛠️ Refactor suggestion | 🟠 MajorReplace "user" terminology with "account" per coding guidelines.
The coding guideline explicitly states: "Use 'account' terminology, never 'entity' or 'user'". This section contains multiple uses of "user":
- Line 67: "CURRENT USER CONTEXT"
- Line 70: "the person currently using this application"
- Line 101: "User's Custom Instructions & Preferences"
📋 Suggested terminology updates
- -----CURRENT USER CONTEXT----- - This is information about the person currently using this application (the human you're talking to): + -----CURRENT ACCOUNT CONTEXT----- + This is information about the account currently using this application (the person you're talking to):- User's Custom Instructions & Preferences: + Account's Custom Instructions & Preferences:As per coding guidelines: "Use 'account' terminology, never 'entity' or 'user'".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/prompts/getSystemPrompt.ts` around lines 67 - 101, In getSystemPrompt.ts update all human-facing and code identifiers that use "user" to "account": change the header string "-----CURRENT USER CONTEXT-----" to "-----CURRENT ACCOUNT CONTEXT-----", change the sentence "the person currently using this application" to use "account" instead of "user", change "User's Custom Instructions & Preferences:" to "Account's Custom Instructions & Preferences:", and rename the variable userSection to accountSection (and update any references) so strings and identifiers consistently use account; keep accountWithDetails as-is.
🧹 Nitpick comments (33)
lib/content/getArtistRootPrefix.ts (1)
1-5: Make the JSDoc actionable, not just present.Good addition, but this block currently only repeats parameter names. Please include short descriptions and return semantics so callers understand fallback behavior quickly.
Suggested JSDoc upgrade
/** - * - * `@param` paths - * `@param` artistSlug + * Selects the root prefix used for an artist's content paths. + * + * Prefers `artists/{artistSlug}/` when present in `paths`, otherwise falls back + * to `{artistSlug}/`, and defaults to `artists/{artistSlug}/` if no match exists. + * + * `@param` paths Candidate content paths to inspect. + * `@param` artistSlug Artist slug used to build candidate prefixes. + * `@returns` The chosen root prefix for downstream path construction. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/getArtistRootPrefix.ts` around lines 1 - 5, Update the JSDoc for getArtistRootPrefix: add brief descriptions for the parameters (describe what "paths" contains and what "artistSlug" represents) and document the return value and fallback behavior (what string is returned on success and what is returned when no matching path is found). Reference the function name getArtistRootPrefix and explicitly mention the expected shape or examples for "paths" and any edge-cases (empty arrays, undefined artistSlug) so callers know how to handle failures.lib/content/isCompletedRun.ts (1)
8-11: Replace placeholder JSDoc with meaningful documentation (or remove it).This block currently adds little value. Please include a short summary plus precise
@param/@returnsdescriptions so the comment improves maintainability.As per coding guidelines, "KISS: Prefer simple, readable implementations over clever optimizations."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/isCompletedRun.ts` around lines 8 - 11, Replace the placeholder JSDoc above the isCompletedRun function with a concise summary and precise tags: add a one-line description (e.g., "Determine whether a Run is completed"), an `@param` {Run} run describing expected properties used (e.g., status, finishedAt) and any accepted shapes, and an `@returns` {boolean} explaining true means the run is completed and under what conditions; ensure the doc matches the isCompletedRun implementation and types referenced in the function signature.lib/supabase/song_artists/insertSongArtists.ts (1)
9-10: Complete the@paramdocumentation for clarity.
@param songArtistsis currently undocumented beyond the name. Add a short description so generated docs and editor hints are actionable.Suggested JSDoc update
/** * Inserts song-artist relationships, skipping duplicates. * - * `@param` songArtists + * `@param` songArtists Array of song-artist join records to validate, deduplicate, and upsert. */As per coding guidelines, “Supabase database functions should import from
@/lib/supabase/serverClientand follow the documented pattern with JSDoc comments, error handling, and return types.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/supabase/song_artists/insertSongArtists.ts` around lines 9 - 10, Update the JSDoc for the insertSongArtists function by adding a clear description for the `@param` songArtists entry (e.g., "array of song-artist relationship objects to insert, each with song_id and artist_id"), and ensure the comment includes the expected shape of each item; keep the existing JSDoc tags for return and throws intact and follow the Supabase pattern used in this file (import from `@/lib/supabase/serverClient` and proper error handling in insertSongArtists) so editor/docs show the parameter shape and intent.lib/content/getArtistFileTree.ts (2)
7-9: Clarify JSDoc parameter intent.Good addition of
@paramtags, but these should include short descriptions (and expected format) so IDE/tooling hints are actually useful, e.g. repo URL/name format and slug format.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/getArtistFileTree.ts` around lines 7 - 9, Update the JSDoc for the getArtistFileTree function to add short descriptions and expected formats for the `@param` tags: describe githubRepo (e.g., repository owner/name or full GitHub URL) and artistSlug (e.g., URL-safe lowercase identifier, permitted characters), so IDE/tooling can show the intended input shape; keep descriptions concise and consistent with existing project conventions.
17-20: Extract duplicate artist-path matching logic into a helper.The
blobPaths+hasArtistcomputation is repeated in two branches. Pulling this into a small local utility would reduce duplication and keep the function easier to evolve.As per coding guidelines, "DRY: Consolidate similar logic into shared utilities".
Also applies to: 28-31
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/getArtistFileTree.ts` around lines 17 - 20, Extract the repeated blob path / artist-match logic inside getArtistFileTree into a small local helper (e.g., hasArtistInTree or getBlobPathsAndHasArtist) that accepts the tree array (mainTree) and artistSlug and returns the blobPaths and/or boolean hasArtist; replace both duplicated blocks that compute blobPaths and hasArtist with calls to this helper so both branches reuse the same logic and avoid duplication.lib/ai/isEmbedModel.ts (2)
6-7: Complete the@paramannotation with a description.The
@param mtag is missing a description. JSDoc best practice requires parameter documentation to include both the name and a description of what the parameter represents.📝 Proposed fix to complete the documentation
* -* `@param` m +* `@param` m - The gateway language model entry to check */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/ai/isEmbedModel.ts` around lines 6 - 7, The JSDoc for the isEmbedModel function is missing a description for the `@param` m tag; update the JSDoc above the isEmbedModel function (reference symbol: isEmbedModel) to include a brief description of parameter m (e.g., "the model identifier or object to check") so the `@param` line documents what m represents and its expected type/format.
16-16: Remove redundant default export.The function is already exported as a named export on line 9. Having both named and default exports of the same function creates ambiguity about the preferred import pattern and adds unnecessary complexity.
♻️ Proposed fix to simplify exports
- -export default isEmbedModel;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/ai/isEmbedModel.ts` at line 16, Remove the redundant default export for isEmbedModel: keep the existing named export (export function isEmbedModel ...) and delete the trailing "export default isEmbedModel;" statement; then search for any consumers importing the default and update them to the named import { isEmbedModel } to ensure a single, unambiguous export pattern.lib/trigger/triggerCreateContent.ts (1)
19-20: Enhance the JSDoc parameter documentation.The
@paramtag was added but lacks a description. Consider adding a brief explanation of what the payload contains, and include a@returnstag to document the returned handle.📝 Suggested JSDoc enhancement
/** * Triggers the create-content task in Trigger.dev. * - * `@param` payload + * `@param` payload - The content creation parameters including account, artist, template, and generation options. + * `@returns` A handle to the triggered task that can be used to track execution. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/trigger/triggerCreateContent.ts` around lines 19 - 20, Update the JSDoc for the triggerCreateContent function: add a brief description for the payload parameter (what fields it contains and their purpose, e.g., content data, metadata, userId) and add a `@returns` tag describing the handle returned (type and what it represents, e.g., Promise<Handle> or created content ID/URL). Ensure the tags reference the existing parameter name "payload" and the function "triggerCreateContent" so maintainers can quickly understand inputs and outputs.lib/chat/toolChains/getPrepareStepResult.ts (1)
15-16: Nice documentation addition!Adding the
@paramtag improves the JSDoc completeness. For even better developer experience, consider adding a brief description to explain the parameter's purpose.📝 Optional enhancement for more descriptive JSDoc
* - * `@param` options + * `@param` options - Configuration containing steps, model, messages, and step number for determining the next tool in the chain */Alternatively, you could document the individual properties if you want more detail:
* - * `@param` options + * `@param` options - The step preparation options + * `@param` options.steps - Array of previous step results with tool executions + * `@param` options.stepNumber - Current step number in the conversation + * `@param` options.model - The language model being used + * `@param` options.messages - Array of conversation messages */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/chat/toolChains/getPrepareStepResult.ts` around lines 15 - 16, Update the JSDoc for getPrepareStepResult to add a brief description for the `@param` options entry (and optionally document its key properties) so callers understand what the options object controls; locate the JSDoc above the getPrepareStepResult function and add one-line text describing the purpose of options (e.g., what values it holds and how it affects the function), and if helpful, add short `@param` entries for specific properties of options referenced in the function.lib/flamingo/getFlamingoPresetsHandler.ts (1)
13-13: Complete the JSDoc@paramannotation with a description.The
@param requesttag is missing a description. JSDoc best practice is to include a brief description after the parameter name to provide context in IDE tooltips and generated documentation.📝 Suggested fix for complete documentation
- * `@param` request + * `@param` request - The incoming Next.js HTTP request🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/flamingo/getFlamingoPresetsHandler.ts` at line 13, Add a brief descriptive text to the JSDoc `@param request` tag for the getFlamingoPresetsHandler function so IDE tooltips and generated docs show context; locate the JSDoc block above the getFlamingoPresetsHandler function and change the `@param request` line to include a short description of what `request` represents (e.g., incoming HTTP request or handler input) and any important details such as expected properties or type.lib/coding-agent/parseMergeActionId.ts (1)
4-11: Complete the JSDoc documentation and add explicit return type.The
@paramannotation was added but lacks a description. Additionally, the function is missing a@returnstag and an explicit return type annotation. While TypeScript can infer the return type, explicitly declaring it improves both documentation and type safety.📝 Proposed enhancements for documentation completeness
/** * Parses a merge action ID like "merge_pr:recoupable/api#42" * into { repo, number } or null if the format doesn't match. * - * `@param` actionId + * `@param` actionId - The action ID string to parse + * `@returns` An object with repo and PR number, or null if format is invalid */ -export function parseMergeActionId(actionId: string) { +export function parseMergeActionId( + actionId: string +): { repo: string; number: number } | null { const match = actionId.match(/^merge_pr:(.+)#(\d+)$/); if (!match) return null; return { repo: match[1], number: parseInt(match[2], 10) }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/coding-agent/parseMergeActionId.ts` around lines 4 - 11, Update the JSDoc for parseMergeActionId to document the parameter and return value and add an explicit return type on the function signature: describe the actionId parameter (e.g., "actionId - string in the form 'merge_pr:<repo>#<number>'"), add a `@returns` tag explaining that it returns an object with repo and number or null on no match, and change the signature of parseMergeActionId(actionId: string) to explicitly return { repo: string; number: number } | null.lib/coding-agent/parseMergeTestToMainActionId.ts (1)
4-12: Complete the JSDoc documentation and add explicit return type.The
@paramannotation was added but lacks a description. Additionally, the function is missing a@returnstag and an explicit return type annotation, which would improve both documentation clarity and TypeScript type safety.📝 Proposed enhancements for documentation completeness
/** * Parses a merge_test_to_main action ID like "merge_test_to_main:recoupable/api" * into the repo string, or null if the format doesn't match. * - * `@param` actionId + * `@param` actionId - The action ID string to parse + * `@returns` The repository string (e.g., "owner/repo") or null if format is invalid */ -export function parseMergeTestToMainActionId(actionId: string): string | null { +export function parseMergeTestToMainActionId(actionId: string): string | null {Note: The explicit return type annotation is already present, so only the JSDoc enhancements are needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/coding-agent/parseMergeTestToMainActionId.ts` around lines 4 - 12, Update the JSDoc for parseMergeTestToMainActionId to describe the parameter and return value: add a brief description for `@param` actionId (e.g., "the action identifier to parse, expected to start with 'merge_test_to_main:'") and add an `@returns` tag that explains the function returns the repository string in "owner/name" format or null if the prefix is missing or the repo is malformed; keep the existing explicit return type string | null and ensure the top-level description summarizes what the function does.lib/catalog/getCatalogs.ts (1)
11-14: Complete the JSDoc parameter description.The
@param accountIdtag lacks a description. Document what account ID is used for to provide clarity.📝 Suggested JSDoc improvement
/** - * - * `@param` accountId + * Fetches all catalogs for a given account + * + * `@param` accountId - The unique identifier of the account whose catalogs to fetch */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/catalog/getCatalogs.ts` around lines 11 - 14, Update the JSDoc for the getCatalogs function to describe the accountId parameter: replace the bare "@param accountId" with a short description such as "the unique identifier of the account whose catalogs should be retrieved" (or similarly precise wording) so readers know which account the function will query; ensure the description mentions expected format (e.g., numeric or UUID) if applicable and keep it adjacent to the getCatalogs function declaration.lib/catalog/formatCatalogSongsAsCSV.ts (1)
5-6: Complete the JSDoc parameter description.The
@param songstag lacks a description. JSDoc parameters should explain what the parameter represents and its purpose to be useful for developers.📝 Suggested JSDoc improvement
/** * Formats catalog songs into the CSV-like format expected by the scorer * - * `@param` songs + * `@param` songs - Array of catalog songs to format as CSV */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/catalog/formatCatalogSongsAsCSV.ts` around lines 5 - 6, The JSDoc for the function formatCatalogSongsAsCSV is missing a description for the `@param` songs tag; update the JSDoc to describe what songs is (e.g., an array of song objects), what each element contains (fields used by the function such as title, artist, album, duration, etc.), and any expectations (non-empty array, optional fields), so maintainers know the input shape and constraints expected by formatCatalogSongsAsCSV; reference the function name formatCatalogSongsAsCSV and the parameter name songs when adding the description.lib/catalog/getCatalogSongs.ts (1)
28-34: Complete the JSDoc parameter descriptions.All four
@paramtags lack descriptions. Effective documentation should explain each parameter's purpose, especially for optional parameters likeartistNameand parameters with default values.📝 Suggested JSDoc improvement
/** - * - * `@param` catalogId - * `@param` pageSize - * `@param` page - * `@param` artistName + * Fetches a single page of songs from a catalog + * + * `@param` catalogId - The unique identifier of the catalog + * `@param` pageSize - Number of songs per page (default: 100) + * `@param` page - Page number to fetch (default: 1) + * `@param` artistName - Optional filter to fetch songs by a specific artist */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/catalog/getCatalogSongs.ts` around lines 28 - 34, Update the JSDoc for getCatalogSongs to provide meaningful descriptions for each `@param`: describe catalogId as the identifier of the catalog to query (string), pageSize as the number of songs to return per page (number, default value if applicable), page as the 1-based page index to fetch (number, default if applicable), and artistName as an optional filter to limit results to a specific artist (string, optional). Mention default values and optionality where applicable so callers understand behavior and which parameters can be omitted.lib/catalog/getCatalogDataAsCSV.ts (1)
6-7: Complete the JSDoc parameter description.The
@param catalogIdtag lacks a description. Document what the catalog ID represents to help developers understand the function's requirements.📝 Suggested JSDoc improvement
/** * Gets all catalog songs and formats them as CSV for the scorer * - * `@param` catalogId + * `@param` catalogId - The unique identifier of the catalog to fetch and format */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/catalog/getCatalogDataAsCSV.ts` around lines 6 - 7, Add a clear JSDoc description for the catalogId parameter in the getCatalogDataAsCSV function: explain that catalogId is the unique identifier (string or numeric ID) of the catalog whose data will be exported to CSV, whether it must be a UUID or database primary key, and note any validation/format expectations (e.g., non-empty string). Update the `@param` tag for catalogId in the getCatalogDataAsCSV JSDoc accordingly so callers know what value to pass.lib/supabase/storage/uploadFileByKey.ts (1)
6-11: Enhance JSDoc with types and descriptions.The
@paramentries list parameter names but lack type information and descriptions, which reduces their utility for developers and IDE intellisense. Standard JSDoc format includes both:`@param` {string} key - The storage key/path for the uploaded file `@param` {File | Blob} file - The file content to upload `@param` {object} [options] - Upload options `@param` {string} [options.contentType] - MIME type of the file (defaults to "application/octet-stream") `@param` {boolean} [options.upsert] - Whether to overwrite existing files (defaults to false)📝 Proposed JSDoc enhancement
/** * Upload file to Supabase storage by key * - * `@param` key - * `@param` file - * `@param` options - * `@param` options.contentType - * `@param` options.upsert + * `@param` {string} key - The storage key/path for the uploaded file + * `@param` {File | Blob} file - The file content to upload + * `@param` {object} [options] - Upload options + * `@param` {string} [options.contentType] - MIME type of the file (defaults to "application/octet-stream") + * `@param` {boolean} [options.upsert] - Whether to overwrite existing files (defaults to false) + * `@returns` {Promise<void>} + * `@throws` {Error} When upload fails */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/supabase/storage/uploadFileByKey.ts` around lines 6 - 11, Update the JSDoc for the uploadFileByKey function to include types and short descriptions for each parameter: document key as `@param` {string} key - The storage key/path for the uploaded file; file as `@param` {File|Blob} file - The file content to upload; options as `@param` {object} [options] - Upload options; options.contentType as `@param` {string} [options.contentType] - MIME type of the file (defaults to "application/octet-stream"); and options.upsert as `@param` {boolean} [options.upsert] - Whether to overwrite existing files (defaults to false); keep the JSDoc block above the uploadFileByKey declaration so IDEs can pick up the types and defaults.lib/spotify/getSpotifyFollowers.ts (1)
57-61: Consider documenting the first-result assumption.The function returns follower count from the first search result without verifying it's the intended artist. For artists with common names or similar-sounding artists, this could return data for the wrong artist. Consider either:
- Adding a JSDoc note that the function returns the most relevant match
- Or adding validation logic to improve match confidence
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/spotify/getSpotifyFollowers.ts` around lines 57 - 61, The getSpotifyFollowers function currently returns followers from data.artists.items[0] without ensuring that item is the intended artist; update the code or docs: either add a JSDoc comment on getSpotifyFollowers stating it returns the most relevant (first) search match, or implement simple validation before returning (e.g., compare artistName normalized/case-insensitive against data.artists.items[0].name, optionally fall back to searching items for an exact match or returning null/error if no confident match). Reference data.artists.items[0] and the getSpotifyFollowers function when making the change.lib/content/getArtistContentReadiness.ts (2)
78-126: Optional: Consolidate file checks into a declarative structure.The six file-checking blocks follow a repetitive pattern. Consider refactoring to a data-driven approach for improved maintainability:
♻️ Example declarative refactor
const fileChecks = [ { check: () => hasFile("context/images/face-guide.png"), file: "context/images/face-guide.png", severity: "required" as const, fix: "Generate a face guide image before creating content.", }, { check: () => hasFile("config/content-creation/config.json"), file: "config/content-creation/config.json", severity: "recommended" as const, fix: "Add a pipeline config to override default model/resolution settings.", }, { check: () => hasAnyMp3, file: "songs/*.mp3", severity: "required" as const, fix: "Add at least one .mp3 file for audio selection.", }, // ... remaining checks ]; const issues = fileChecks .filter(check => !check.check()) .map(({ file, severity, fix }) => ({ file, severity, fix }));This approach reduces duplication, makes adding new file checks trivial, and aligns with the DRY principle.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/getArtistContentReadiness.ts` around lines 78 - 126, The repeated if-blocks performing file presence checks (using hasFile and hasAnyMp3) should be replaced with a declarative array of check descriptors and a single filter/map pipeline to populate the issues array; create a fileChecks array of objects each containing a check function (or boolean), file, severity, and fix, then set issues = fileChecks.filter(c => !c.check()).map(({file,severity,fix}) => ({file,severity,fix})); update references to hasFile and hasAnyMp3 inside those descriptors and remove the duplicated if blocks so adding or changing checks is just editing the fileChecks entries.
31-138: Consider splitting this function to meet the 50-line guideline.The
getArtistContentReadinessfunction currently spans ~107 lines, exceeding the coding guideline of "Keep functions under 50 lines." While the logic is clear, breaking it into smaller, focused functions would improve maintainability and testability.Consider extracting:
- File tree validation logic into a separate function
- The file-checking rules into a data-driven configuration
- Issue categorization and result building into helper functions
This refactor would align with both the 50-line guideline and Single Responsibility Principle at the function level. As per coding guidelines, "Keep functions under 50 lines" for domain functions in
lib/**/*.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/getArtistContentReadiness.ts` around lines 31 - 138, Split getArtistContentReadiness into smaller helpers: extract the repo/tree validation into a function (e.g., validateRepoAndTree that calls selectAccountSnapshots and getArtistFileTree and returns githubRepo and blobPaths or early-not-ready response), move path-check logic into a helper that builds issues from a data-driven list (e.g., buildFileChecks or collectIssues that uses getArtistRootPrefix and implements hasFile/hasAnyMp3 checks), and extract the final categorization/response construction into a buildReadinessResult helper that computes requiredMissing, warnings and returns the final object; keep getArtistContentReadiness as an orchestration function that calls validateRepoAndTree, collectIssues, and buildReadinessResult so each function stays under ~50 lines and names like getArtistContentReadiness, validateRepoAndTree, collectIssues/buildFileChecks, and buildReadinessResult appear in the diff for easy location.lib/prompts/getSystemPrompt.ts (1)
40-129: Consider using array-based string composition for cleaner code.The current pattern of
systemPrompt = \${systemPrompt}${section}`` works but could be simplified. Consider building sections in an array and joining them, which can improve readability and make testing individual sections easier.💡 Alternative approach
const sections = [SYSTEM_PROMPT + /* base prompt content */]; if (accountWithDetails) { sections.push(buildUserContextSection(accountWithDetails, email)); } if (artistInstruction) { sections.push(buildArtistInstructionSection(artistInstruction)); } if (knowledgeBaseText) { sections.push(buildKnowledgeBaseSection(knowledgeBaseText)); } return sections.join('\n');This pairs well with the earlier suggestion to extract section-building logic into separate functions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/prompts/getSystemPrompt.ts` around lines 40 - 129, The prompt-building code repeatedly mutates systemPrompt by concatenating sections; refactor to build an array of sections and join them to improve readability and testability: create a sections array seeded with SYSTEM_PROMPT and the IMAGE EDITING/IMPORTANT CONTEXT block, then replace the repeated `systemPrompt = \`${systemPrompt}${...}\`` logic by pushing sections for accountWithDetails (use a helper like buildUserContextSection to assemble the userSection using accountWithDetails and email), artistInstruction (buildArtistInstructionSection), and knowledgeBaseText (buildKnowledgeBaseSection), and finally set systemPrompt = sections.join('\n'); ensure existing identifiers systemPrompt, accountWithDetails, artistInstruction, knowledgeBaseText, and the userSection construction are used to locate and replace the current concatenation logic.lib/admins/privy/countNewAccounts.ts (1)
9-10: Align parameter docs with project terminology.Please avoid
usersin docs and preferaccounts(or a specific subtype) to match repo language conventions.As per coding guidelines "
**/*.{ts,tsx}: Use 'account' terminology, never 'entity' or 'user'; use specific names like 'artist', 'workspace', 'organization' when referring to specific types".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/admins/privy/countNewAccounts.ts` around lines 9 - 10, Update the JSDoc param name to use the project's preferred terminology: replace the `@param` tag that currently says "users" with "accounts" (or a more specific subtype if applicable) in the documentation for the countNewAccounts function so the docs match the repo convention; ensure any inline references in the comment block to "users" are renamed to "accounts" (or e.g., "artist", "workspace", "organization" if that more accurately describes the input) while leaving the function and implementation unchanged.app/api/accounts/[id]/route.ts (1)
26-27: Clarify destructured params in JSDoc.
@param params.paramsis ambiguous. Prefer documenting the destructured argument once (e.g.,@param context - Route context containing params.id) to avoid confusion.Suggested doc cleanup
- * `@param` params.params - * `@param` params - Route params containing the account ID + * `@param` context - Route context containing params.id (account UUID)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/accounts/`[id]/route.ts around lines 26 - 27, Update the JSDoc for the route handler to remove the ambiguous "@param params.params" and document the destructured parameter once; e.g., replace the two entries with a single line like "@param context - Route context containing params.id (the account ID)" and, if the handler destructures params (e.g., function handler({ params }) or function GET(context)), mention that params.id is the account identifier so readers know which property is used (reference params and params.id in the comment).lib/admins/privy/fetchPrivyLogins.ts (1)
23-26: Consolidate duplicated JSDoc into a single function-level block.This new placeholder doc is too thin and, combined with the earlier block, leaves docs split across the type alias and function. Prefer one complete JSDoc directly above
fetchPrivyLoginsfor clarity and DRYness.♻️ Suggested doc cleanup
-/** - * Fetches Privy users active or created within the given period via the Privy Management API. - * Returns the full, unmodified user objects from Privy. - * Paginates through all users, collecting those whose created_at or latest_verified_at - * falls within the time window. - * - * `@see` https://docs.privy.io/api-reference/users/get-all - * `@param` period - "daily", "weekly", or "monthly" - * `@returns` Array of full Privy user objects within the time window, sorted by created_at descending - */ export type FetchPrivyLoginsResult = { users: User[]; totalPrivyUsers: number; }; /** - * - * `@param` period + * Fetches Privy users active or created within the given period via the Privy Management API. + * Returns full user objects from Privy and total user count seen during pagination. + * + * `@see` https://docs.privy.io/api-reference/users/get-all + * `@param` period - "all", "daily", "weekly", or "monthly" + * `@returns` Users in window (sorted by created_at desc) and totalPrivyUsers */ export async function fetchPrivyLogins(period: PrivyLoginsPeriod): Promise<FetchPrivyLoginsResult> {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/admins/privy/fetchPrivyLogins.ts` around lines 23 - 26, Consolidate the duplicated JSDoc by removing the thin/placeholder block currently above the type alias and create one complete JSDoc directly above the fetchPrivyLogins function describing its parameters (e.g., period), return value, and behavior; ensure the JSDoc fully documents fetchPrivyLogins (and remove the separate doc on the type alias) so all documentation is single-source and immediately precedes the fetchPrivyLogins declaration.lib/artistIntel/getArtistMusicAnalysis.ts (1)
29-40: Generation parameters are overridden by preset config.Per
processAnalyzeMusicRequestimplementation, when apresetis provided, the preset's own parameters (max_new_tokens,temperature,do_sample) are extracted from the preset config and override any values passed here. Thetop_pis also set toundefinedfor presets.These hardcoded values (lines 34-37) are effectively dead code and may mislead future maintainers into thinking they control the generation behavior. Consider removing them for clarity:
♻️ Suggested simplification
ANALYSIS_PRESETS.map(preset => processAnalyzeMusicRequest({ preset, audio_url: previewUrl, - max_new_tokens: 512, - temperature: 1.0, - top_p: 1.0, - do_sample: false, }), ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/artistIntel/getArtistMusicAnalysis.ts` around lines 29 - 40, The generation params passed into processAnalyzeMusicRequest are shadowed by the provided preset, so remove the dead hardcoded options (max_new_tokens, temperature, top_p, do_sample) from the ANALYSIS_PRESETS.map call and only pass the preset and audio_url (and any truly dynamic args); update the map invocation that calls processAnalyzeMusicRequest(preset, audio_url, ...) to call processAnalyzeMusicRequest({ preset, audio_url }) so the preset-driven config is used and future maintainers aren’t misled by ineffective overrides.app/api/admins/privy/route.ts (1)
18-20: Empty JSDoc for OPTIONS handler.The OPTIONS handler should document its CORS preflight purpose:
📝 Suggested improvement
/** - * + * OPTIONS /api/admins/privy + * + * CORS preflight handler returning appropriate headers. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/admins/privy/route.ts` around lines 18 - 20, The empty JSDoc above the OPTIONS handler should be replaced with a short descriptive comment that documents this handler's role as the CORS preflight responder: update the JSDoc for the OPTIONS function (the OPTIONS handler in app/api/admins/privy/route.ts) to state it handles CORS preflight requests, lists the allowed methods and headers it responds with (e.g., Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin), and notes it returns a 204/OK empty response for preflight checks so future readers understand its purpose.app/api/transcribe/route.ts (1)
5-8: Empty JSDoc provides no value.The JSDoc block has an empty description and
@param reqwithout any explanation. Per the coding guidelines, API routes should have meaningful JSDoc comments that describe the endpoint's purpose, expected inputs, and responses.Consider documenting what this endpoint does:
📝 Suggested improvement
/** - * - * `@param` req + * POST /api/transcribe + * + * Transcribes audio files using OpenAI Whisper and saves both the original + * audio and transcript markdown to the artist's files. + * + * `@param` req - Request containing audio_url, account_id, artist_account_id, title, and include_timestamps. + * `@returns` Transcription result with audio file, transcript file, text, and language. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/transcribe/route.ts` around lines 5 - 8, Replace the empty JSDoc above the API route handler with a meaningful comment: describe the endpoint's purpose, the expected request shape (e.g., multipart/form-data or JSON, file/audio format, and any required fields in req), the returned response structure and status codes, and document the req parameter (e.g., Request) and what it should contain; update the JSDoc immediately above the route handler function (the POST handler/exported route in route.ts) to include these details and remove the blank `@param` entry so the comment provides clear API usage and expectations.lib/mcp/resolveAccountId.ts (1)
18-21: JSDoc param annotations lack descriptions.The nested
@paramtags would be more helpful with brief descriptions:📝 Suggested improvement
* `@param` params - The auth info and optional account_id override. - * `@param` params.authInfo - * `@param` params.accountIdOverride + * `@param` params.authInfo - MCP authentication info from the request. + * `@param` params.accountIdOverride - Optional account_id to use instead of authenticated account. * `@returns` The resolved accountId or an error message.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/resolveAccountId.ts` around lines 18 - 21, Update the JSDoc for resolveAccountId to add brief descriptions for the nested parameters: describe params as the input object containing auth info and optional override, document params.authInfo (e.g., the authentication object containing user/org identifiers or tokens used for resolution), and document params.accountIdOverride (e.g., optional string to force a specific accountId). Also ensure the `@returns` line briefly explains the resolved accountId or an error message format so callers know what to expect.lib/mcp/tools/transcribe/registerTranscribeAudioTool.ts (1)
18-21: Empty JSDoc provides no documentation value.The JSDoc block has an empty description and
@param serverwithout explanation. Consider providing meaningful documentation:📝 Suggested improvement
/** - * - * `@param` server + * Registers the `transcribe_audio` MCP tool for audio transcription via OpenAI Whisper. + * + * `@param` server - The MCP server instance to register the tool on. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/transcribe/registerTranscribeAudioTool.ts` around lines 18 - 21, The JSDoc above the registerTranscribeAudioTool function is empty and unhelpful; replace it with a concise description of what registerTranscribeAudioTool does and document the parameter `server` (its type or expected interface and role) and any return value or side effects; update the comment block near the registerTranscribeAudioTool declaration in registerTranscribeAudioTool.ts to include a one-line summary, `@param server` explanation, and `@returns` or `@throws` if applicable so IDEs and readers get meaningful documentation.lib/artistIntel/buildArtistMarketingCopy.ts (2)
63-66: Consider validating AI response structure with Zod.The
JSON.parse(result.text)result is cast directly toArtistMarketingCopywithout runtime validation. AI models can return malformed JSON, missing fields, or unexpected structures. Adding Zod validation would provide type safety and clearer error handling:♻️ Suggested improvement
+import { z } from "zod"; + +const ArtistMarketingCopySchema = z.object({ + playlist_pitch_email: z.string(), + instagram_caption: z.string(), + tiktok_caption: z.string(), + twitter_post: z.string(), + press_release_opener: z.string(), + key_talking_points: z.array(z.string()), +}); + // In the try block: try { const result = await generateText({ system: SYSTEM_PROMPT, prompt }); - const parsed = JSON.parse(result.text) as ArtistMarketingCopy; - return parsed; + const parsed = ArtistMarketingCopySchema.parse(JSON.parse(result.text)); + return parsed; } catch (error) {This aligns with the codebase pattern of using Zod for input/output validation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 63 - 66, The code in buildArtistMarketingCopy calls generateText and directly casts JSON.parse(result.text) to ArtistMarketingCopy; add runtime validation using a Zod schema (e.g., define/import ArtistMarketingCopySchema) to parse and validate the parsed object instead of a direct cast, catching Zod or JSON errors and returning/throwing a clear error with context (include result.text) from the buildArtistMarketingCopy function so malformed AI responses are detected and handled safely.
34-35: Guard against emptytopTracksarray.If
topTracksis an empty array,topTracks[0]will beundefined. While the optional chaining ontopTrack?.namehandles this downstream, adding an explicit check improves readability:♻️ Minor clarity improvement
const { artist, topTracks } = spotifyData; - const topTrack = topTracks[0]; + const topTrack = topTracks.length > 0 ? topTracks[0] : undefined;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 34 - 35, The code grabs topTracks from spotifyData and directly indexes topTracks[0] into topTrack which can be undefined when topTracks is an empty array; update the buildArtistMarketingCopy logic to explicitly guard for an empty topTracks (e.g., compute topTrack = Array.isArray(topTracks) && topTracks.length > 0 ? topTracks[0] : null) and use that guarded value wherever topTrack is referenced so downstream optional chaining remains clear and the intent is explicit (referencing symbols: spotifyData, topTracks, topTrack, buildArtistMarketingCopy).lib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts (1)
11-16: Share theartist_nameschema with API validation to prevent drift.This schema duplicates
lib/artistIntel/validateArtistIntelBody.ts; any future change can desync MCP vs API behavior.♻️ Suggested refactor
-const toolSchema = z.object({ - artist_name: z - .string() - .min(1) - .describe("The artist name to analyze (e.g. 'Taylor Swift', 'Bad Bunny', 'Olivia Rodrigo')."), -}); +import { artistIntelBodySchema } from "@/lib/artistIntel/artistIntelSchema"; + +const toolSchema = artistIntelBodySchema;Create
lib/artistIntel/artistIntelSchema.tsas the shared source of truth, then import it in both MCP and API validators.As per coding guidelines
**/*.{ts,tsx}: Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts` around lines 11 - 16, The toolSchema definition duplicates the API validator; extract the shared schema into a new module (e.g., export a zod schema from lib/artistIntel/artistIntelSchema.ts) and replace the local const toolSchema here with an import of that shared schema (preserving the artist_name field constraints), and update lib/artistIntel/validateArtistIntelBody.ts to import the same symbol so both MCP (registerGenerateArtistIntelPackTool -> toolSchema / artist_name) and the API use the single source of truth.lib/artistIntel/getArtistSpotifyData.ts (1)
5-23: Use shared Spotify types instead of local duplicates.These local interfaces will drift from the canonical Spotify types and duplicate maintenance effort.
As per coding guidelines
**/*.{ts,tsx}: Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/artistIntel/getArtistSpotifyData.ts` around lines 5 - 23, The file defines local duplicate interfaces SpotifyArtist and SpotifyTrack which should be replaced with the project's shared Spotify types to avoid drift; remove these local interfaces from getArtistSpotifyData.ts and import the canonical types (e.g., SpotifyArtist, SpotifyTrack or equivalent) from the shared types module used across the repo (check modules like types/spotify, shared/types, or src/types) and update any references in getArtistSpotifyData and related functions to use the imported types so the code relies on the single source of truth.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4a95954a-95fd-4f4b-8f6b-555ee862cce5
⛔ Files ignored due to path filters (24)
lib/admins/emails/__tests__/validateGetAdminEmailsQuery.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artistIntel/__tests__/generateArtistIntelPack.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artists/__tests__/createArtistPostHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artists/__tests__/validateCreateArtistBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/auth/__tests__/validateAuthContext.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/chat/__tests__/integration/chatEndToEnd.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/coding-agent/__tests__/handleGitHubWebhook.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/coding-agent/__tests__/onMergeTestToMainAction.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/__tests__/validateCreateContentBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/evals/callChatFunctions.tsis excluded by!**/evals/**and included bylib/**lib/evals/callChatFunctionsWithResult.tsis excluded by!**/evals/**and included bylib/**lib/evals/createToolsCalledScorer.tsis excluded by!**/evals/**and included bylib/**lib/evals/extractTextFromResult.tsis excluded by!**/evals/**and included bylib/**lib/evals/extractTextResultFromSteps.tsis excluded by!**/evals/**and included bylib/**lib/evals/getCatalogSongsCountExpected.tsis excluded by!**/evals/**and included bylib/**lib/evals/getSpotifyFollowersExpected.tsis excluded by!**/evals/**and included bylib/**lib/evals/scorers/CatalogAvailability.tsis excluded by!**/evals/**and included bylib/**lib/evals/scorers/QuestionAnswered.tsis excluded by!**/evals/**and included bylib/**lib/evals/scorers/ToolsCalled.tsis excluded by!**/evals/**and included bylib/**lib/flamingo/__tests__/getFlamingoPresetsHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/notifications/__tests__/createNotificationHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/notifications/__tests__/validateCreateNotificationBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/getTaskRunHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/validateGetTaskRunQuery.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (66)
app/api/accounts/[id]/route.tsapp/api/admins/privy/route.tsapp/api/artists/intel/route.tsapp/api/coding-agent/[platform]/route.tsapp/api/songs/analyze/presets/route.tsapp/api/transcribe/route.tslib/admins/privy/countNewAccounts.tslib/admins/privy/fetchPrivyLogins.tslib/admins/privy/getCutoffMs.tslib/admins/privy/getLatestVerifiedAt.tslib/admins/privy/toMs.tslib/ai/getModel.tslib/ai/isEmbedModel.tslib/artistIntel/buildArtistMarketingCopy.tslib/artistIntel/generateArtistIntelPack.tslib/artistIntel/generateArtistIntelPackHandler.tslib/artistIntel/getArtistMusicAnalysis.tslib/artistIntel/getArtistSpotifyData.tslib/artistIntel/getArtistWebContext.tslib/artistIntel/validateArtistIntelBody.tslib/catalog/formatCatalogSongsAsCSV.tslib/catalog/getCatalogDataAsCSV.tslib/catalog/getCatalogSongs.tslib/catalog/getCatalogs.tslib/chat/toolChains/getPrepareStepResult.tslib/chats/processCompactChatRequest.tslib/coding-agent/encodeGitHubThreadId.tslib/coding-agent/handleMergeSuccess.tslib/coding-agent/parseMergeActionId.tslib/coding-agent/parseMergeTestToMainActionId.tslib/composio/getCallbackUrl.tslib/content/contentTemplates.tslib/content/createContentHandler.tslib/content/getArtistContentReadiness.tslib/content/getArtistFileTree.tslib/content/getArtistRootPrefix.tslib/content/getContentValidateHandler.tslib/content/isCompletedRun.tslib/content/persistCreateContentRunVideo.tslib/content/validateCreateContentBody.tslib/content/validateGetContentEstimateQuery.tslib/content/validateGetContentValidateQuery.tslib/credits/getCreditUsage.tslib/credits/handleChatCredits.tslib/emails/processAndSendEmail.tslib/flamingo/getFlamingoPresetsHandler.tslib/github/expandSubmoduleEntries.tslib/github/getRepoGitModules.tslib/github/resolveSubmodulePath.tslib/mcp/resolveAccountId.tslib/mcp/tools/artistIntel/index.tslib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.tslib/mcp/tools/index.tslib/mcp/tools/transcribe/registerTranscribeAudioTool.tslib/prompts/getSystemPrompt.tslib/spotify/getSpotifyFollowers.tslib/supabase/account_artist_ids/getAccountArtistIds.tslib/supabase/account_workspace_ids/getAccountWorkspaceIds.tslib/supabase/files/createFileRecord.tslib/supabase/song_artists/insertSongArtists.tslib/supabase/storage/uploadFileByKey.tslib/transcribe/processAudioTranscription.tslib/transcribe/saveAudioToFiles.tslib/transcribe/saveTranscriptToFiles.tslib/transcribe/types.tslib/trigger/triggerCreateContent.ts
| * @param params.params | ||
| * @param params - Route params containing the platform name |
There was a problem hiding this comment.
Incorrect JSDoc parameter: @param params.params is nonsensical.
The parameter structure is { params }: { params: Promise<{ platform: string }> }, so the correct documentation should reference params.platform, not params.params.
📝 Suggested fix
- * `@param` params.params
* `@param` params - Route params containing the platform name
+ * `@param` params.platform - The platform identifier (e.g., "slack", "whatsapp")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| * @param params.params | |
| * @param params - Route params containing the platform name | |
| * `@param` params - Route params containing the platform name | |
| * `@param` params.platform - The platform identifier (e.g., "slack", "whatsapp") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/coding-agent/`[platform]/route.ts around lines 13 - 14, The JSDoc
incorrectly documents `@param params.params`; update the comment to reference
the actual parameter structure and property — replace `@param params.params`
with `@param params.platform` and update the description to indicate that
`params` is an object (or promise-resolved object) containing the `platform`
string used by the route handler (the handler signature is `{ params }: {
params: Promise<{ platform: string }> }`). Ensure the JSDoc matches the
handler's parameter name and property (`params.platform`) and remove the
nonsensical `params.params` entry.
| * @param params.params | ||
| * @param params - Route params containing the platform name |
There was a problem hiding this comment.
Same JSDoc error repeated for POST handler.
📝 Suggested fix
- * `@param` params.params
* `@param` params - Route params containing the platform name
+ * `@param` params.platform - The platform identifier (e.g., "slack", "whatsapp")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/coding-agent/`[platform]/route.ts around lines 38 - 39, The JSDoc for
both the GET and POST route handlers contains a duplicate/incorrect `@param` entry
("@param params.params" and "@param params"); update the JSDoc for the GET and
POST functions (the exported GET and POST handlers) to remove the redundant
"@param params.params" line and keep a single correct parameter doc like "@param
params - Route params containing the platform name" (or rename to "@param
param0" if matching the handler signature), ensuring the doc matches the handler
signature exactly.
| * Returns the most recent latest_verified_at (in ms) across all linked_accounts for a Privy user. | ||
| * Returns null if no linked account has a latest_verified_at. | ||
| * | ||
| * @param user |
There was a problem hiding this comment.
Use account-specific terminology in JSDoc param naming.
@param user conflicts with the repository terminology rule. Rename to account-focused wording (for example, @param accountHolder), and add a brief description for clarity.
As per coding guidelines, "Use 'account' terminology, never 'entity' or 'user'; use specific names like 'artist', 'workspace', 'organization' when referring to specific types".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/admins/privy/getLatestVerifiedAt.ts` at line 8, The JSDoc currently uses
"@param user" which violates the repo's account-first terminology; update the
JSDoc for getLatestVerifiedAt to use an account-focused name (for example
"@param accountHolder") and add a short description (e.g., "the account to
retrieve verification info for"). Also consider renaming the function parameter
from "user" to "accountHolder" in the getLatestVerifiedAt signature (and update
all internal references) so the doc and code consistently use account
terminology.
| if (tokenResult.error || !tokenResult.access_token) { | ||
| console.error("Failed to get Spotify token:", tokenResult.error); | ||
| return null; | ||
| } | ||
|
|
||
| const accessToken = tokenResult.access_token; | ||
|
|
||
| const searchResult = await getSearch({ q: artistName, type: "artist", limit: 1, accessToken }); | ||
| if (searchResult.error || !searchResult.data) { | ||
| console.error("Spotify artist search failed:", searchResult.error); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Differentiate “not found” from upstream Spotify failures.
This currently returns null for token/search errors and true not-found cases, so downstream code cannot return accurate status/error semantics.
🛠️ Suggested direction
+export type ArtistSpotifyDataResult =
+ | { type: "success"; data: ArtistSpotifyData }
+ | { type: "not_found" }
+ | { type: "error"; error: string };
-export async function getArtistSpotifyData(artistName: string): Promise<ArtistSpotifyData | null> {
+export async function getArtistSpotifyData(artistName: string): Promise<ArtistSpotifyDataResult> {
const tokenResult = await generateAccessToken();
if (tokenResult.error || !tokenResult.access_token) {
- console.error("Failed to get Spotify token:", tokenResult.error);
- return null;
+ return { type: "error", error: "Failed to authenticate with Spotify" };
}
...
if (!artist) {
- return null;
+ return { type: "not_found" };
}
...
- return { artist, topTracks, previewUrl };
+ return { type: "success", data: { artist, topTracks, previewUrl } };
}Then map not_found to 404 and error to 5xx in generateArtistIntelPack.
Also applies to: 59-62
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/getArtistSpotifyData.ts` around lines 39 - 50,
getArtistSpotifyData currently returns null for both upstream failures and true
"artist not found" cases, losing semantic difference; modify
getArtistSpotifyData to return a structured result (e.g., { status: "ok"|
"not_found" | "error", data?: Artist, error?: Error }) instead of null so
callers can distinguish outcomes when handling tokenResult and getSearch
responses (inspect tokenResult.error and searchResult.data to set status
appropriately). Then update generateArtistIntelPack to handle the new statuses:
map "not_found" to a 404 response and map "error" (upstream/token/search
failures) to a 5xx response, using the returned error details for logging;
ensure functions referenced are getArtistSpotifyData, tokenResult, getSearch,
and generateArtistIntelPack.
| const tracksData = topTracksResult.data as { tracks: SpotifyTrack[] }; | ||
| const topTracks = tracksData.tracks || []; | ||
| const previewUrl = topTracks.find(t => t.preview_url)?.preview_url ?? null; | ||
|
|
There was a problem hiding this comment.
Keep analyzed audio source consistent with reported top_track.
previewUrl comes from the first track with a preview, but top_track elsewhere is topTracks[0]. That can produce analysis for one track while reporting another as top track.
🎯 Suggested fix
-const previewUrl = topTracks.find(t => t.preview_url)?.preview_url ?? null;
+const previewUrl = topTracks[0]?.preview_url ?? null;If you want fallback analysis from non-top tracks, return analyzed_track_id alongside previewUrl and expose it in the pack.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const tracksData = topTracksResult.data as { tracks: SpotifyTrack[] }; | |
| const topTracks = tracksData.tracks || []; | |
| const previewUrl = topTracks.find(t => t.preview_url)?.preview_url ?? null; | |
| const tracksData = topTracksResult.data as { tracks: SpotifyTrack[] }; | |
| const topTracks = tracksData.tracks || []; | |
| const previewUrl = topTracks[0]?.preview_url ?? null; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/getArtistSpotifyData.ts` around lines 64 - 67, The current
code sets previewUrl from the first track that has a preview while the reported
top_track is topTracks[0], causing a mismatch; change the logic in
getArtistSpotifyData so previewUrl is taken from topTracks[0] (tracksData /
topTracks) to guarantee the analyzed audio matches top_track, and if you want to
allow fallback analysis from a different track, return an additional
analyzed_track_id alongside previewUrl (e.g., set analyzed_track_id when you
pick a non-top track) and expose that field in the pack so callers know which
track was actually analyzed.
| * @param usage.usage | ||
| * @param usage - The language model usage data | ||
| * @param model - The model ID used for the chat | ||
| * @param accountId - The account ID to deduct credits from (optional) | ||
| * @param usage.model | ||
| * @param usage.accountId |
There was a problem hiding this comment.
Remove incorrect JSDoc parameter annotations.
Lines 15, 19, and 20 use incorrect dot-notation (@param usage.usage, @param usage.model, @param usage.accountId) that doesn't match the actual function signature. The function uses destructured parameters {usage, model, accountId}, which are already correctly documented on lines 16-18 as top-level parameters.
These duplicate annotations create confusion and conflict with the accurate documentation.
📝 Proposed fix: Remove the incorrect parameter annotations
/**
* Handles credit deduction after chat completion.
* Calculates usage cost and deducts appropriate credits from the user's account.
*
- * `@param` usage.usage
* `@param` usage - The language model usage data
* `@param` model - The model ID used for the chat
* `@param` accountId - The account ID to deduct credits from (optional)
- * `@param` usage.model
- * `@param` usage.accountId
*/📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| * @param usage.usage | |
| * @param usage - The language model usage data | |
| * @param model - The model ID used for the chat | |
| * @param accountId - The account ID to deduct credits from (optional) | |
| * @param usage.model | |
| * @param usage.accountId | |
| /** | |
| * Handles credit deduction after chat completion. | |
| * Calculates usage cost and deducts appropriate credits from the user's account. | |
| * | |
| * `@param` usage - The language model usage data | |
| * `@param` model - The model ID used for the chat | |
| * `@param` accountId - The account ID to deduct credits from (optional) | |
| */ |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/credits/handleChatCredits.ts` around lines 15 - 20, The JSDoc contains
incorrect dotted param annotations that duplicate and conflict with the real
signature of the function handleChatCredits (which takes destructured params
{usage, model, accountId}); remove the erroneous lines "@param usage.usage",
"@param usage.model", and "@param usage.accountId" from the JSDoc so only the
correct top-level "@param usage", "@param model", and "@param accountId" entries
remain, leaving the rest of the comment intact.
| * @param regularEntries.regularEntries | ||
| * @param regularEntries - Non-submodule file tree entries | ||
| * @param submoduleEntries - Submodule references (type "commit" from GitHub Trees API) | ||
| * @param repo - Repository context for fetching .gitmodules | ||
| * @param regularEntries.submoduleEntries | ||
| * @param regularEntries.repo | ||
| * @param regularEntries.repo.owner | ||
| * @param regularEntries.repo.repo | ||
| * @param regularEntries.repo.branch |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the recommended JSDoc style for destructured object parameters in TypeScript when documenting top-level keys versus nested keys?
💡 Result:
The recommended JSDoc style for destructured object parameters in TypeScript, for both top-level and nested keys, uses the @param tag with dot notation to document the object parameter and its properties. This approach is explicitly supported by official JSDoc documentation and works well with TypeScript's JSDoc integration. For top-level keys in a destructured parameter like function foo({a, b}) {}, document as: / * @param {Object} obj - The object parameter. * @param {string} obj.a - Description of a. * @param {number} [obj.b] - Optional description of b. */ function foo({a, b}) {} For nested keys, such as function foo({a, b: {c}}) {} or deeper nesting like {x: {y: {z}}}, extend the dot notation: / * Nesting example. * @param {Object} param - The parameter object. * @param {number} param.a - Top-level property. * @param {Object} param.b - Nested wrapper object. * @param {string} param.b.c - Nested property. */ const fn = ({a, b: {c}}) => a + c; This syntax documents the full shape of the incoming object, providing IntelliSense and hover information in TypeScript-aware editors like VS Code. TypeScript supports this JSDoc pattern, as confirmed in its JSDoc reference and wiki. Alternatives like inline object types (@param {{a: string, b: {c: number}}} obj) or @typedef for complex shapes are also common and recommended for reusability, especially for deeply nested or recursive structures: / * @typedef {Object} NestedConfig * @property {string} topLevel * @property {Object} nested * @property {number} nested.deep */ / * @param {NestedConfig} config */ function bar({topLevel, nested: {deep}}) {} This style is consistent across JSDoc 3.6+, TypeScript 5.x+, and style guides like Google's TypeScript Style Guide, which permits simple destructured parameters while favoring explicit typing for clarity.
Citations:
- 1: https://stackoverflow.com/questions/36916790/document-destructured-function-parameter-in-jsdoc
- 2: https://stackoverflow.com/questions/58410776/how-to-document-destructured-parameters-in-jsdoc
- 3: https://docs.joshuatz.com/cheatsheets/js/jsdoc/
- 4: https://jsdoc.app/tags-param
- 5: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html
- 6: https://github.com/microsoft/TypeScript/wiki/JSDoc-support-in-JavaScript/5706acaee451da7c1d30df60a72affddfefa5dff
- 7: https://stackoverflow.com/questions/65997536/jsdoc-documenting-nested-array-parameters
- 8: https://stackoverflow.com/questions/77669267/standard-way-to-define-recursive-object-shape-using-jsdoc
🏁 Script executed:
cat -n lib/github/expandSubmoduleEntries.ts | head -40Repository: recoupable/api
Length of output: 1744
JSDoc describes the wrong parameter structure.
The tags on lines 14 and 18-22 describe fields under regularEntries.*, but the function actually takes a destructured object with three sibling top-level parameters: regularEntries, submoduleEntries, and repo. The incorrect nesting in JSDoc will mislead developers using IDE hints.
Remove the nested references (lines 14, 18-22) and document the top-level structure instead:
Suggested fix
- * `@param` regularEntries.regularEntries
* `@param` regularEntries - Non-submodule file tree entries
* `@param` submoduleEntries - Submodule references (type "commit" from GitHub Trees API)
* `@param` repo - Repository context for fetching .gitmodules
- * `@param` regularEntries.submoduleEntries
- * `@param` regularEntries.repo
- * `@param` regularEntries.repo.owner
- * `@param` regularEntries.repo.repo
- * `@param` regularEntries.repo.branch📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| * @param regularEntries.regularEntries | |
| * @param regularEntries - Non-submodule file tree entries | |
| * @param submoduleEntries - Submodule references (type "commit" from GitHub Trees API) | |
| * @param repo - Repository context for fetching .gitmodules | |
| * @param regularEntries.submoduleEntries | |
| * @param regularEntries.repo | |
| * @param regularEntries.repo.owner | |
| * @param regularEntries.repo.repo | |
| * @param regularEntries.repo.branch | |
| * `@param` regularEntries - Non-submodule file tree entries | |
| * `@param` submoduleEntries - Submodule references (type "commit" from GitHub Trees API) | |
| * `@param` repo - Repository context for fetching .gitmodules |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/github/expandSubmoduleEntries.ts` around lines 14 - 22, The JSDoc
currently nests fields under regularEntries but the function
expandSubmoduleEntries actually accepts three top-level destructured parameters
(regularEntries, submoduleEntries, repo); update the comments to remove the
erroneous nested tags and instead document the three top-level parameters:
`@param` regularEntries - Non-submodule file tree entries, `@param` submoduleEntries
- Submodule references (type "commit" from GitHub Trees API), and `@param` repo -
Repository context (with repo.owner, repo.repo, repo.branch). Keep references to
repo.owner, repo.repo, and repo.branch as properties of the repo param and
remove any `@param` lines that prefix those with regularEntries.
| * @param owner.owner | ||
| * @param owner - The GitHub repository owner | ||
| * @param repo - The GitHub repository name | ||
| * @param branch - The branch to fetch from | ||
| * @param owner.repo | ||
| * @param owner.branch |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In JSDoc for a TypeScript function with signature function fn({ owner, repo, branch }: {...}), should @paramtags beowner, repo, branchrather thanowner.owner, owner.repo, owner.branch?
💡 Result:
No, @param tags should use owner, repo, branch (the destructured property names), not owner.owner, owner.repo, owner.branch. The standard JSDoc approach for a destructured parameter like function fn({ owner, repo, branch }) is to assign a name to the input object (e.g., options) and document its properties directly with that name as the prefix: / * @param {Object} options - The repository options. * @param {string} options.owner - Repository owner. * @param {string} options.repo - Repository name. * @param {string} options.branch - Branch name. */ function fn({ owner, repo, branch }) { ... } This is explicitly shown in the official JSDoc documentation for documenting destructuring parameters. TypeScript supports this JSDoc syntax, providing IntelliSense and type checking based on these tags. Using the destructured names alone (e.g., @param {string} owner) without the object prefix is not standard and may not reliably associate the documentation with the parameter structure.
Citations:
- 1: https://jsdoc.app/tags-param
- 2: https://typescriptlang.org/docs/handbook/jsdoc-supported-types.html
🏁 Script executed:
cat -n lib/github/getRepoGitModules.ts | head -30Repository: recoupable/api
Length of output: 1190
Remove mismatched JSDoc param declarations.
Lines 7, 11, and 12 document nested properties (owner.owner, owner.repo, owner.branch), but the function signature destructures parameters directly (owner, repo, branch). This creates incorrect documentation that doesn't reflect the actual parameter structure.
Fix
- * `@param` owner.owner
* `@param` owner - The GitHub repository owner
* `@param` repo - The GitHub repository name
* `@param` branch - The branch to fetch from
- * `@param` owner.repo
- * `@param` owner.branch📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| * @param owner.owner | |
| * @param owner - The GitHub repository owner | |
| * @param repo - The GitHub repository name | |
| * @param branch - The branch to fetch from | |
| * @param owner.repo | |
| * @param owner.branch | |
| * `@param` owner - The GitHub repository owner | |
| * `@param` repo - The GitHub repository name | |
| * `@param` branch - The branch to fetch from |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/github/getRepoGitModules.ts` around lines 7 - 12, Update the JSDoc in
getRepoGitModules (lib/github/getRepoGitModules.ts) to remove incorrect nested
param names and match the function signature: replace any occurrences of
owner.owner, owner.repo, owner.branch with flat `@param` entries for owner, repo,
and branch (and remove duplicated/incorrect lines), ensuring the descriptions
reflect the actual parameters used by the function.
| * @param githubRepo.githubRepo | ||
| * @param githubRepo - The parent GitHub repository URL | ||
| * @param path - The file path to resolve | ||
| * @param githubRepo.path | ||
| * @returns The resolved repo URL and path |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For TypeScript JSDoc on a function that destructures { githubRepo, path }directly in parameters, are@param githubRepoand@param paththe correct tags, and is@param githubRepo.path invalid in this case?
💡 Result:
No, @param githubRepo and @param path are not the correct tags for a function that destructures { githubRepo, path } directly in the parameters. The correct approach uses @param tags with dot notation on a named object parameter, such as @param {string} options - The options object. followed by @param {string} options.githubRepo - The GitHub repository. and @param {string} options.path - The file path.. Using plain @param githubRepo and @param path documents them as if they were separate top-level parameters, which mismatches the destructuring signature and can lead to TypeScript errors like "no parameter with that name". Additionally, @param githubRepo.path is invalid here because there is no top-level parameter named githubRepo with a path property—the destructuring assigns githubRepo and path as separate local variables from the single input object. The dot notation applies to properties of the documented object parameter, not the destructured variables. This is confirmed by the official JSDoc documentation on parameters with properties and destructuring, which explicitly shows the object-named approach for destructured params. TypeScript supports this JSDoc syntax for type checking and IntelliSense in JS files.
Citations:
- 1: https://jsdoc.app/tags-param
- 2: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html
- 3: https://stackoverflow.com/questions/36916790/document-destructured-function-parameter-in-jsdoc
- 4: https://stackoverflow.com/questions/65325353/how-to-document-destructured-function-arguments-e-g-a-when-writing-const-fn
🏁 Script executed:
fd "resolveSubmodulePath.ts" -t f --exec cat -n {}Repository: recoupable/api
Length of output: 1808
JSDoc for destructured parameters requires a named parameter object with dot notation.
The current JSDoc is malformed, but the suggested fix is also incorrect. For a function with an unnamed destructured parameter { githubRepo, path }, JSDoc best practices require either:
-
Rename the parameter to have an explicit name (e.g.,
optionsorparams):export async function resolveSubmodulePath({ githubRepo, path, }: { githubRepo: string; path: string; })
Then document with:
`@param` options - The submodule resolution options `@param` options.githubRepo - The parent GitHub repository URL `@param` options.path - The file path to resolve -
Or use destructuring JSDoc syntax that explicitly documents the unnamed object structure. Using
@param githubRepoand@param pathas standalone parameters misrepresents the actual signature and will confuse TypeScript tooling.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/github/resolveSubmodulePath.ts` around lines 9 - 13, The JSDoc for
resolveSubmodulePath incorrectly documents destructured parameters; update the
function signature to use a named options object (e.g., rename the unnamed
destructured param to options in resolveSubmodulePath) and then change JSDoc to
`@param` options - The submodule resolution options, `@param` options.githubRepo -
The parent GitHub repository URL, and `@param` options.path - The file path to
resolve; alternatively, if you prefer to keep the inline destructuring, replace
the current `@param` lines with a single `@param` that documents the object shape
using JSDoc object/destructuring syntax so the docs match the actual parameter
structure used by resolveSubmodulePath.
| * @param params.artistInstruction - The artist instruction | ||
| * @param params.conversationName - The name of the conversation | ||
| * @param params.accountWithDetails - The account with details | ||
| * @param params.orgId |
There was a problem hiding this comment.
Complete the JSDoc parameter description.
The JSDoc for params.orgId is missing a description. All other parameters include descriptions (e.g., "The ID of the room", "The ID of the artist"), but this one is incomplete.
📝 Suggested fix
- * `@param` params.orgId
+ * `@param` params.orgId - The ID of the organization (null for personal accounts)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/prompts/getSystemPrompt.ts` at line 16, Add a clear description for the
JSDoc parameter params.orgId in the getSystemPrompt function's comment block:
replace the incomplete "@param params.orgId" with a descriptive line such as
"@param params.orgId - The ID of the organization" (matching wording/style used
for other params like roomId/artistId) so the JSDoc documents what orgId
represents.
…r web context, markdown report - Add proper TypeScript types for MusicFlamingo analysis responses (CatalogMetadata, AudienceProfile, MoodTagsResult) replacing `unknown` - Switch getArtistWebContext from Perplexity search snippets to chatWithPerplexity (sonar-pro) for richer narrative summaries with citations - Add formatArtistIntelPackAsMarkdown: formats the full pack as a structured markdown report with sections for artist profile, music DNA, web context, and marketing pack - Add formatted_report field to ArtistIntelPack returned by the API - Update generate_artist_intel_pack MCP tool to return formatted markdown directly (instead of raw JSON) so the chat UI renders it beautifully - 15 new formatter tests; 1529 total passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
lib/artistIntel/getArtistWebContext.ts (1)
20-27:⚠️ Potential issue | 🟠 MajorReplace fixed “2024-2025” range with dynamic years.
At Line 22, the prompt hard-codes 2024-2025. On March 21, 2026, this biases results away from 2026 coverage and can degrade “current/recent” context quality.
Suggested fix
export async function getArtistWebContext(artistName: string): Promise<ArtistWebContext | null> { try { + const currentYear = new Date().getUTCFullYear(); + const previousYear = currentYear - 1; const result = await chatWithPerplexity([ { role: "user", content: `Research the music artist "${artistName}". Provide a concise but rich overview covering: 1. Who they are and their current career stage -2. Recent releases, streaming milestones, or news (2024-2025) +2. Recent releases, streaming milestones, or news (${previousYear}-${currentYear}) 3. Their core sound and genre positioning 4. Notable collaborations, press coverage, or cultural moments 5. Why they matter right now in the music industry🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/artistIntel/getArtistWebContext.ts` around lines 20 - 27, The prompt in getArtistWebContext.ts hard-codes the year range "2024-2025" in the content template, which will become stale; update the template in the content field used by getArtistWebContext to compute the current year dynamically (e.g., const currentYear = new Date().getFullYear() and previousYear = currentYear - 1) and inject those values into the prompt string instead of the fixed "2024-2025" text so the “current/recent” range always reflects the present year.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/artistIntel/formatArtistIntelPackAsMarkdown.ts`:
- Around line 13-157: The formatter function formatArtistIntelPackAsMarkdown is
doing too many things; split it into smaller section-builder functions (e.g.,
buildArtistProfileSection(pack.artist, pack.top_track),
buildMusicDNASection(pack.music_analysis),
buildWebContextSection(pack.web_context),
buildMarketingPackSection(pack.marketing_pack)) that each return an array/string
of markdown lines, then have formatArtistIntelPackAsMarkdown simply compose
those pieces and join them; refactor logic for BPM/Key/Mood, audience, moods,
playlist_pitch, citations, and key_talking_points into their respective builders
and keep each builder under ~50 lines and single-responsibility so the top-level
function only orchestrates composition.
In `@lib/artistIntel/getArtistMusicAnalysis.ts`:
- Around line 90-99: The code in getArtistMusicAnalysis is assuming result.value
has a response when result.status === "fulfilled" and result.value.type ===
"success"; instead, first narrow the union by checking the actual payload shape
(e.g., verify "response" in result.value or that the object matches
AnalysisSuccess rather than FullReportSuccess) before casting and reading
value.response; update the branch that assigns analysis.catalog_metadata /
analysis.audience_profile / analysis.playlist_pitch / analysis.mood_tags to only
read value.response when the payload contains response, else handle or ignore
FullReportSuccess (which has report) appropriately so you don't assign
undefined.
---
Duplicate comments:
In `@lib/artistIntel/getArtistWebContext.ts`:
- Around line 20-27: The prompt in getArtistWebContext.ts hard-codes the year
range "2024-2025" in the content template, which will become stale; update the
template in the content field used by getArtistWebContext to compute the current
year dynamically (e.g., const currentYear = new Date().getFullYear() and
previousYear = currentYear - 1) and inject those values into the prompt string
instead of the fixed "2024-2025" text so the “current/recent” range always
reflects the present year.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 183f51fb-4b8e-4fec-87f3-1a6205318558
⛔ Files ignored due to path filters (2)
lib/artistIntel/__tests__/formatArtistIntelPackAsMarkdown.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artistIntel/__tests__/generateArtistIntelPack.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (5)
lib/artistIntel/formatArtistIntelPackAsMarkdown.tslib/artistIntel/generateArtistIntelPack.tslib/artistIntel/getArtistMusicAnalysis.tslib/artistIntel/getArtistWebContext.tslib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- lib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts
- lib/artistIntel/generateArtistIntelPack.ts
| export function formatArtistIntelPackAsMarkdown(pack: ArtistIntelPack): string { | ||
| const { artist, top_track, music_analysis, web_context, marketing_pack, elapsed_seconds } = pack; | ||
|
|
||
| const lines: string[] = []; | ||
|
|
||
| lines.push(`# Artist Intelligence Pack: ${artist.name}`); | ||
| lines.push(``); | ||
| lines.push(`> Generated in ${elapsed_seconds}s · Spotify + MusicFlamingo AI + Perplexity`); | ||
| lines.push(``); | ||
| lines.push(`---`); | ||
| lines.push(``); | ||
|
|
||
| // Artist profile | ||
| lines.push(`## Artist Profile`); | ||
| lines.push(``); | ||
| lines.push(`- **Followers:** ${artist.followers.toLocaleString()}`); | ||
| lines.push(`- **Popularity:** ${artist.popularity}/100`); | ||
| if (artist.genres.length > 0) { | ||
| lines.push(`- **Genres:** ${artist.genres.slice(0, 4).join(", ")}`); | ||
| } | ||
| if (top_track) { | ||
| lines.push( | ||
| `- **Top Track:** "${top_track.name}" — *${top_track.album_name}* (popularity: ${top_track.popularity}/100)`, | ||
| ); | ||
| if (top_track.preview_url) { | ||
| lines.push(`- **Preview:** ${top_track.preview_url}`); | ||
| } | ||
| } | ||
| lines.push(``); | ||
| lines.push(`---`); | ||
| lines.push(``); | ||
|
|
||
| // Music DNA | ||
| if (music_analysis) { | ||
| lines.push(`## Music DNA (NVIDIA MusicFlamingo AI)`); | ||
| lines.push(``); | ||
|
|
||
| const meta = music_analysis.catalog_metadata; | ||
| if (meta) { | ||
| const genreLine = meta.subgenres?.length | ||
| ? `**Genre:** ${meta.genre} *(${meta.subgenres.join(", ")})*` | ||
| : `**Genre:** ${meta.genre}`; | ||
| lines.push(genreLine); | ||
| lines.push( | ||
| `**BPM:** ${meta.tempo_bpm} | **Key:** ${meta.key} | **Time:** ${meta.time_signature}`, | ||
| ); | ||
| lines.push(`**Energy:** ${meta.energy_level}/10 | **Danceability:** ${meta.danceability}/10`); | ||
| if (meta.mood?.length) lines.push(`**Mood:** ${meta.mood.join(", ")}`); | ||
| if (meta.instruments?.length) lines.push(`**Instruments:** ${meta.instruments.join(", ")}`); | ||
| if (meta.vocal_style) lines.push(`**Vocal Style:** ${meta.vocal_style}`); | ||
| if (meta.production_style) lines.push(`**Production:** ${meta.production_style}`); | ||
| if (meta.similar_artists?.length) | ||
| lines.push(`**Sounds Like:** ${meta.similar_artists.join(", ")}`); | ||
| if (meta.description) lines.push(`**Description:** ${meta.description}`); | ||
| lines.push(``); | ||
| } | ||
|
|
||
| const audience = music_analysis.audience_profile; | ||
| if (audience) { | ||
| lines.push( | ||
| `**Target Audience:** ${audience.age_range}${audience.gender_skew !== "neutral" ? `, ${audience.gender_skew}` : ""}`, | ||
| ); | ||
| if (audience.listening_contexts?.length) { | ||
| lines.push( | ||
| `**Listening Contexts:** ${audience.listening_contexts.slice(0, 3).join(" · ")}`, | ||
| ); | ||
| } | ||
| if (audience.platforms?.length) { | ||
| lines.push(`**Key Platforms:** ${audience.platforms.join(", ")}`); | ||
| } | ||
| if (audience.comparable_fanbases?.length) { | ||
| lines.push(`**Comparable Fanbases:** ${audience.comparable_fanbases.join(", ")}`); | ||
| } | ||
| if (audience.marketing_hook) { | ||
| lines.push(`**Marketing Hook:** *"${audience.marketing_hook}"*`); | ||
| } | ||
| lines.push(``); | ||
| } | ||
|
|
||
| const moods = music_analysis.mood_tags; | ||
| if (moods?.tags?.length) { | ||
| lines.push(`**Vibe Tags:** ${moods.tags.join(" · ")}`); | ||
| if (moods.primary_mood) lines.push(`**Primary Mood:** ${moods.primary_mood}`); | ||
| lines.push(``); | ||
| } | ||
|
|
||
| if (music_analysis.playlist_pitch) { | ||
| lines.push(`**Playlist Pitch (AI-generated from audio):**`); | ||
| lines.push(``); | ||
| lines.push(music_analysis.playlist_pitch); | ||
| lines.push(``); | ||
| } | ||
|
|
||
| lines.push(`---`); | ||
| lines.push(``); | ||
| } | ||
|
|
||
| // Web context | ||
| if (web_context?.summary) { | ||
| lines.push(`## Recent Web Context`); | ||
| lines.push(``); | ||
| lines.push(web_context.summary); | ||
| lines.push(``); | ||
| if (web_context.citations?.length) { | ||
| lines.push(`*Sources: ${web_context.citations.slice(0, 3).join(", ")}*`); | ||
| lines.push(``); | ||
| } | ||
| lines.push(`---`); | ||
| lines.push(``); | ||
| } | ||
|
|
||
| // Marketing pack | ||
| lines.push(`## Marketing Pack`); | ||
| lines.push(``); | ||
|
|
||
| lines.push(`### Playlist Pitch Email`); | ||
| lines.push(``); | ||
| lines.push(marketing_pack.playlist_pitch_email); | ||
| lines.push(``); | ||
|
|
||
| lines.push(`### Social Media Captions`); | ||
| lines.push(``); | ||
| lines.push(`**Instagram:** ${marketing_pack.instagram_caption}`); | ||
| lines.push(``); | ||
| lines.push(`**TikTok:** ${marketing_pack.tiktok_caption}`); | ||
| lines.push(``); | ||
| lines.push(`**Twitter/X:** ${marketing_pack.twitter_post}`); | ||
| lines.push(``); | ||
|
|
||
| lines.push(`### Press Release Opener`); | ||
| lines.push(``); | ||
| lines.push(marketing_pack.press_release_opener); | ||
| lines.push(``); | ||
|
|
||
| if (marketing_pack.key_talking_points?.length) { | ||
| lines.push(`### Key Talking Points`); | ||
| lines.push(``); | ||
| for (const point of marketing_pack.key_talking_points) { | ||
| lines.push(`- ${point}`); | ||
| } | ||
| lines.push(``); | ||
| } | ||
|
|
||
| return lines.join("\n").trim(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Split this formatter into section builders (current function is too large/multi-purpose).
At Line 13-Line 157, this function handles multiple distinct concerns in one body and is well over the project’s size target for domain functions, which will make future edits risky.
Refactor direction
export function formatArtistIntelPackAsMarkdown(pack: ArtistIntelPack): string {
- const { artist, top_track, music_analysis, web_context, marketing_pack, elapsed_seconds } = pack;
- const lines: string[] = [];
- ...
- return lines.join("\n").trim();
+ const lines: string[] = [];
+ lines.push(...buildHeaderSection(pack));
+ lines.push(...buildArtistProfileSection(pack));
+ lines.push(...buildMusicDnaSection(pack.music_analysis));
+ lines.push(...buildWebContextSection(pack.web_context));
+ lines.push(...buildMarketingPackSection(pack.marketing_pack));
+ return lines.join("\n").trim();
}As per coding guidelines, lib/**/*.ts: “Single responsibility per function” and “Keep functions under 50 lines.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/formatArtistIntelPackAsMarkdown.ts` around lines 13 - 157,
The formatter function formatArtistIntelPackAsMarkdown is doing too many things;
split it into smaller section-builder functions (e.g.,
buildArtistProfileSection(pack.artist, pack.top_track),
buildMusicDNASection(pack.music_analysis),
buildWebContextSection(pack.web_context),
buildMarketingPackSection(pack.marketing_pack)) that each return an array/string
of markdown lines, then have formatArtistIntelPackAsMarkdown simply compose
those pieces and join them; refactor logic for BPM/Key/Mood, audience, moods,
playlist_pitch, citations, and key_talking_points into their respective builders
and keep each builder under ~50 lines and single-responsibility so the top-level
function only orchestrates composition.
| if (result.status === "fulfilled" && result.value.type === "success") { | ||
| const value = result.value as { type: "success"; response: unknown }; | ||
| if (preset === "catalog_metadata") { | ||
| analysis.catalog_metadata = value.response as CatalogMetadata; | ||
| } else if (preset === "audience_profile") { | ||
| analysis.audience_profile = value.response as AudienceProfile; | ||
| } else if (preset === "playlist_pitch") { | ||
| analysis.playlist_pitch = value.response as string; | ||
| } else if (preset === "mood_tags") { | ||
| analysis.mood_tags = value.response as MoodTagsResult; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== AnalyzeMusicResult variants =="
fd processAnalyzeMusicRequest.ts --exec sed -n '1,140p' {}
echo
echo "== Current narrowing in getArtistMusicAnalysis =="
fd getArtistMusicAnalysis.ts --exec sed -n '80,120p' {}Repository: recoupable/api
Length of output: 4331
Guard success payload shape before reading response.
At lines 90–99, checking type === "success" alone cannot distinguish between FullReportSuccess (which has a report field) and AnalysisSuccess (which has a response field). The type assertion bypasses this gap, allowing the code to access value.response even when the object is actually a FullReportSuccess. This silently assigns undefined instead of proper analysis data.
Suggested fix
results.forEach((result, i) => {
const preset = ANALYSIS_PRESETS[i] as AnalysisPreset;
- if (result.status === "fulfilled" && result.value.type === "success") {
- const value = result.value as { type: "success"; response: unknown };
+ if (
+ result.status === "fulfilled" &&
+ result.value.type === "success" &&
+ "response" in result.value
+ ) {
+ const value = result.value;
if (preset === "catalog_metadata") {
analysis.catalog_metadata = value.response as CatalogMetadata;
} else if (preset === "audience_profile") {
analysis.audience_profile = value.response as AudienceProfile;
} else if (preset === "playlist_pitch") {
analysis.playlist_pitch = value.response as string;
} else if (preset === "mood_tags") {
analysis.mood_tags = value.response as MoodTagsResult;
}
anySuccess = true;
}
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/getArtistMusicAnalysis.ts` around lines 90 - 99, The code in
getArtistMusicAnalysis is assuming result.value has a response when
result.status === "fulfilled" and result.value.type === "success"; instead,
first narrow the union by checking the actual payload shape (e.g., verify
"response" in result.value or that the object matches AnalysisSuccess rather
than FullReportSuccess) before casting and reading value.response; update the
branch that assigns analysis.catalog_metadata / analysis.audience_profile /
analysis.playlist_pitch / analysis.mood_tags to only read value.response when
the payload contains response, else handle or ignore FullReportSuccess (which
has report) appropriately so you don't assign undefined.
…ts & labels Replace generic social media captions as the primary output with high-value music industry materials: artist one-sheet, A&R memo, sync licensing brief, named Spotify editorial playlist targets, and brand partnership pitch. Social captions remain but are moved to a secondary 'Outreach & Social' section. The Industry Pack section now leads with the outputs that drive actual revenue and career decisions for artists, managers, and label A&R teams. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
lib/artistIntel/buildArtistMarketingCopy.ts (2)
37-114: Function exceeds 50-line guideline; consider extracting helpers.The function spans ~78 lines, violating the coding guideline to "Keep functions under 50 lines." The responsibilities—prompt construction, AI invocation, response parsing, and fallback generation—could be split for improved testability and adherence to Single Responsibility Principle.
As per coding guidelines: "Keep functions under 50 lines."
♻️ Suggested decomposition
// Helper: build the AI prompt function buildMarketingPrompt( artist: SpotifyArtist, topTrack: SpotifyTrack | null, musicAnalysis: ArtistMusicAnalysis | null, webContext: ArtistWebContext | null, ): string { // ... prompt template logic (~40 lines) } // Helper: generate fallback copy function buildFallbackMarketingCopy( artist: SpotifyArtist, topTrack: SpotifyTrack | null, ): ArtistMarketingCopy { // ... fallback object construction (~25 lines) } // Main function stays lean export async function buildArtistMarketingCopy(...): Promise<ArtistMarketingCopy> { const { artist, topTracks } = spotifyData; const topTrack = topTracks[0] ?? null; const prompt = buildMarketingPrompt(artist, topTrack, musicAnalysis, webContext); try { const result = await generateText({ system: SYSTEM_PROMPT, prompt }); // ... parse and return } catch (error) { console.error("Failed to generate marketing copy:", error); return buildFallbackMarketingCopy(artist, topTrack); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 37 - 114, The function buildArtistMarketingCopy is too long and mixes responsibilities (prompt construction, AI call, parsing, fallback); extract a helper buildMarketingPrompt(artist, topTrack, musicAnalysis, webContext) to return the long prompt string and another helper buildFallbackMarketingCopy(artist, topTrack) to construct the fallback ArtistMarketingCopy object, then simplify buildArtistMarketingCopy to assemble topTrack, call buildMarketingPrompt, await generateText({ system: SYSTEM_PROMPT, prompt }), JSON.parse the result, and on catch call buildFallbackMarketingCopy while preserving the existing logging behavior; keep signature and return types the same.
42-43: Consider explicit guard for emptytopTracksarray.Per
getArtistSpotifyData.ts(line 61),topTrackscan be an empty array when the track fetch fails. While the downstream optional chaining (topTrack?.name) prevents runtime errors, an explicit guard improves readability and intent.♻️ Suggested improvement
const { artist, topTracks } = spotifyData; -const topTrack = topTracks[0]; +const topTrack = topTracks.length > 0 ? topTracks[0] : null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 42 - 43, Replace the direct indexing of topTracks with an explicit guard: instead of const topTrack = topTracks[0]; check that topTracks is an array and has length > 0 (e.g., if (!Array.isArray(topTracks) || topTracks.length === 0) { /* handle missing topTrack: set topTrack = undefined or return/compose fallback marketing copy */ }) and then safely assign topTrack when present; update buildArtistMarketingCopy's logic where topTrack is used to rely on this clear guard (symbols: topTracks, topTrack, buildArtistMarketingCopy).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/artistIntel/buildArtistMarketingCopy.ts`:
- Around line 87-90: In buildArtistMarketingCopy, validate that generateText
returned a non-empty result.text before calling JSON.parse (follow the pattern
in lib/evals/extractTextFromResult.ts): if result.text is falsy, throw or return
a clear error indicating "AI returned no text". After parsing, do not blindly
assert with "as ArtistMarketingCopy"; instead perform runtime validation of
required fields on the parsed object (e.g., check presence and types of title,
description, callToAction, etc.) and throw a descriptive error if validation
fails so malformed/missing fields are caught early. Ensure these checks are
applied around the generateText call and replace the unsafe JSON.parse+type
assertion in buildArtistMarketingCopy.
---
Nitpick comments:
In `@lib/artistIntel/buildArtistMarketingCopy.ts`:
- Around line 37-114: The function buildArtistMarketingCopy is too long and
mixes responsibilities (prompt construction, AI call, parsing, fallback);
extract a helper buildMarketingPrompt(artist, topTrack, musicAnalysis,
webContext) to return the long prompt string and another helper
buildFallbackMarketingCopy(artist, topTrack) to construct the fallback
ArtistMarketingCopy object, then simplify buildArtistMarketingCopy to assemble
topTrack, call buildMarketingPrompt, await generateText({ system: SYSTEM_PROMPT,
prompt }), JSON.parse the result, and on catch call buildFallbackMarketingCopy
while preserving the existing logging behavior; keep signature and return types
the same.
- Around line 42-43: Replace the direct indexing of topTracks with an explicit
guard: instead of const topTrack = topTracks[0]; check that topTracks is an
array and has length > 0 (e.g., if (!Array.isArray(topTracks) ||
topTracks.length === 0) { /* handle missing topTrack: set topTrack = undefined
or return/compose fallback marketing copy */ }) and then safely assign topTrack
when present; update buildArtistMarketingCopy's logic where topTrack is used to
rely on this clear guard (symbols: topTracks, topTrack,
buildArtistMarketingCopy).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 19d7143d-e649-4e82-b598-c3126e9593a1
⛔ Files ignored due to path filters (2)
lib/artistIntel/__tests__/formatArtistIntelPackAsMarkdown.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artistIntel/__tests__/generateArtistIntelPack.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (2)
lib/artistIntel/buildArtistMarketingCopy.tslib/artistIntel/formatArtistIntelPackAsMarkdown.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/artistIntel/formatArtistIntelPackAsMarkdown.ts
| try { | ||
| const result = await generateText({ system: SYSTEM_PROMPT, prompt }); | ||
| const parsed = JSON.parse(result.text) as ArtistMarketingCopy; | ||
| return parsed; |
There was a problem hiding this comment.
Validate result.text before parsing to avoid misleading errors.
Per the pattern in lib/evals/extractTextFromResult.ts, result.text can be undefined or falsy. Calling JSON.parse(result.text) when result.text is undefined produces SyntaxError: "undefined" is not valid JSON, which obscures the real issue (AI returned no text).
Additionally, the as ArtistMarketingCopy type assertion is unsafe—the parsed JSON may be missing required fields or have incorrect types.
🛡️ Proposed defensive parsing
try {
const result = await generateText({ system: SYSTEM_PROMPT, prompt });
- const parsed = JSON.parse(result.text) as ArtistMarketingCopy;
- return parsed;
+ if (typeof result.text !== "string" || !result.text.trim()) {
+ throw new Error("AI returned empty or invalid response");
+ }
+ const parsed: unknown = JSON.parse(result.text);
+ // Consider adding runtime validation (e.g., Zod) for production robustness
+ return parsed as ArtistMarketingCopy;
} catch (error) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const result = await generateText({ system: SYSTEM_PROMPT, prompt }); | |
| const parsed = JSON.parse(result.text) as ArtistMarketingCopy; | |
| return parsed; | |
| try { | |
| const result = await generateText({ system: SYSTEM_PROMPT, prompt }); | |
| if (typeof result.text !== "string" || !result.text.trim()) { | |
| throw new Error("AI returned empty or invalid response"); | |
| } | |
| const parsed: unknown = JSON.parse(result.text); | |
| // Consider adding runtime validation (e.g., Zod) for production robustness | |
| return parsed as ArtistMarketingCopy; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 87 - 90, In
buildArtistMarketingCopy, validate that generateText returned a non-empty
result.text before calling JSON.parse (follow the pattern in
lib/evals/extractTextFromResult.ts): if result.text is falsy, throw or return a
clear error indicating "AI returned no text". After parsing, do not blindly
assert with "as ArtistMarketingCopy"; instead perform runtime validation of
required fields on the parsed object (e.g., check presence and types of title,
description, callToAction, etc.) and throw a descriptive error if validation
fails so malformed/missing fields are caught early. Ensure these checks are
applied around the generateText call and replace the unsafe JSON.parse+type
assertion in buildArtistMarketingCopy.
- Add peer benchmarking via Spotify related-artists API — actual follower counts and popularity scores for 5 related artists, with percentile rankings showing exactly where the target artist sits among their competitive set - Add algorithmic opportunity scores (0-100) computed from real MusicFlamingo audio data and Spotify metrics — no AI inference, pure signal: Sync Score (BPM, energy, mood diversity, production), Playlist Score (danceability, energy, algorithmic momentum), A&R Score (popularity-to-follower efficiency, peer gap analysis), Brand Score (lifestyle tags, demographic specificity) - Add catalog depth analysis across all 10 top tracks: consistency score, hit-concentration %, catalog type classification (consistent / hit-driven / emerging) - Ground all AI marketing copy in real competitor data — the prompt now receives actual peer follower counts so 'comparable artist' references cite real numbers not hallucinations - Upgrade markdown report with: Opportunity Scores dashboard table with ASCII bars, Peer Benchmarking table with gap-to-peers column and YOU marker, Catalog Analysis with per-track popularity bars and actionable catalog type callout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
lib/artistIntel/computeArtistOpportunityScores.ts (1)
46-274: Split the four scoring models into private helpers.
computeArtistOpportunityScores()now owns four separate heuristics, rationale assembly, clamping, and weighting in one body, which makes threshold changes hard to test or reason about safely. Extract per-domain calculators and keep this exported function focused on composition. As per coding guidelines,lib/**/*.ts: “Single responsibility per function”, “Keep functions under 50 lines”, and “DRY: Consolidate similar logic into shared utilities”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/artistIntel/computeArtistOpportunityScores.ts` around lines 46 - 274, computeArtistOpportunityScores is doing four distinct heuristics in one large function; extract each domain into a private helper to improve testability and single responsibility. Create four helpers (e.g., computeSyncScore, computePlaylistScore, computeArScore, computeBrandScore) that accept the relevant inputs (musicAnalysis/meta, followers, popularity, peerBenchmark) and return a consistent shape like { score: number, rationale: string[] }; move domain-specific clamping/rating logic into either each helper or a small shared utility (e.g., clampTo100, buildRationale) so duplicate min/max/rationale joins are removed; then refactor computeArtistOpportunityScores to call those helpers, compute the weighted overall, and return the same public shape using rating(...) and joined rationales. Ensure helper names match the ones above to make locating code straightforward and keep each function under ~50 lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/artistIntel/analyzeCatalogDepth.ts`:
- Around line 34-42: The avg_popularity value is being rounded before it's used
to compute variance/popularity_std_dev which alters the std-dev; compute a raw
mean (e.g., raw_avg or mean) from popularities using track_count and use that
raw value in the variance/popularity_std_dev calculation, then only round values
(avg_popularity and popularity_std_dev) when building the returned payload;
update references to avg_popularity in the variance calculation to use the new
raw mean and keep the rounded avg_popularity for output only.
In `@lib/artistIntel/formatArtistIntelPackAsMarkdown.ts`:
- Around line 95-124: The formatter currently assumes opportunity_scores exists
and crashes; update formatArtistIntelPackAsMarkdown to guard or normalize before
pushing lines: check if opportunity_scores is present on the pack (or create a
default opportunity_scores object with safe defaults for overall,
sync/playlist/ar/brand {score, rating, rationale}) and then use those safe
values when calling ratingEmoji(...) and scoreBar(...); alternatively wrap the
whole Opportunity Scores block in a presence check for opportunity_scores and
skip it if missing, ensuring all references (opportunity_scores.overall,
opportunity_scores.sync.score, opportunity_scores.sync.rating,
opportunity_scores.sync.rationale, and similarly for playlist/ar/brand) and
calls to ratingEmoji and scoreBar only run against validated/defaulted data.
In `@lib/artistIntel/generateArtistIntelPack.ts`:
- Around line 74-79: The current Promise.all call for optional enrichments
(getArtistMusicAnalysis, getArtistWebContext, getRelatedArtistsData) must be
made "fail soft": replace the Promise.all with Promise.allSettled (or wrap each
call in a try/catch) and map any rejected result to null so that failures from
MusicFlamingo, Perplexity, or related-artists do not throw the whole pack;
ensure spotifyData.previewUrl still gates getArtistMusicAnalysis and that the
resolved values are assigned back to the same variables (musicAnalysis,
webContext, peerBenchmark) with null fallbacks on error.
In `@lib/artistIntel/getRelatedArtistsData.ts`:
- Around line 1-2: The module currently imports generateAccessToken at top-level
which throws when Spotify creds are missing; change to lazy-load it inside
getRelatedArtistsData (or move the env validation into generateAccessToken) so
module evaluation never throws: update getRelatedArtistsData to require/import
or call generateAccessToken dynamically at runtime before using it (and keep
getRelatedArtists imported as needed), ensuring when creds are absent the
function returns null instead of causing an import-time exception.
- Around line 60-68: Percentile calculation in the percentile(arr:number[],
value:number) function ignores ties (uses v < value), which makes tied
popularities report 0; change the logic to count values strictly less
(countLess) and values equal (countEqual) and compute percentile as
round(((countLess + 0.5 * countEqual) / arr.length) * 100), and handle empty
arrays defensively (e.g., return 0); update the percentile function (and any
callers that expect popularity_percentile) to use this tied-aware formula while
leaving median(...) unchanged.
- Around line 44-48: The current code slices related before sorting which only
ranks the first 10 items; instead sort the entire related array by followers
(b.followers.total - a.followers.total) and then take the top 5, i.e. replace
the chain "related.slice(0, 10).sort(...).slice(0, 5)" with a
sort-first-then-slice approach so the peers variable truly contains the top 5
most-followed artists; update the peers assignment in getRelatedArtistsData (the
peers/related logic) accordingly.
In `@lib/spotify/getRelatedArtists.ts`:
- Around line 15-19: The Spotify fetch in getRelatedArtists currently has no
cancellation; wrap the request with an AbortController and a short timeout
(e.g., 2–5s) that calls controller.abort() via setTimeout, pass
controller.signal to the fetch call, and catch the abort error; in the
getRelatedArtists function treat an aborted/timeout error the same as other soft
failures (returning the existing fallback path), and ensure you clear the
timeout on success to avoid leaks.
---
Nitpick comments:
In `@lib/artistIntel/computeArtistOpportunityScores.ts`:
- Around line 46-274: computeArtistOpportunityScores is doing four distinct
heuristics in one large function; extract each domain into a private helper to
improve testability and single responsibility. Create four helpers (e.g.,
computeSyncScore, computePlaylistScore, computeArScore, computeBrandScore) that
accept the relevant inputs (musicAnalysis/meta, followers, popularity,
peerBenchmark) and return a consistent shape like { score: number, rationale:
string[] }; move domain-specific clamping/rating logic into either each helper
or a small shared utility (e.g., clampTo100, buildRationale) so duplicate
min/max/rationale joins are removed; then refactor
computeArtistOpportunityScores to call those helpers, compute the weighted
overall, and return the same public shape using rating(...) and joined
rationales. Ensure helper names match the ones above to make locating code
straightforward and keep each function under ~50 lines.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 968f891d-420e-4f95-8c2e-4aacef8d8b09
📒 Files selected for processing (7)
lib/artistIntel/analyzeCatalogDepth.tslib/artistIntel/buildArtistMarketingCopy.tslib/artistIntel/computeArtistOpportunityScores.tslib/artistIntel/formatArtistIntelPackAsMarkdown.tslib/artistIntel/generateArtistIntelPack.tslib/artistIntel/getRelatedArtistsData.tslib/spotify/getRelatedArtists.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/artistIntel/buildArtistMarketingCopy.ts
| const popularities = topTracks.map(t => t.popularity); | ||
| const track_count = topTracks.length; | ||
| const avg_popularity = Math.round(popularities.reduce((s, p) => s + p, 0) / track_count); | ||
| const top_track_popularity = Math.max(...popularities); | ||
|
|
||
| // Standard deviation | ||
| const variance = | ||
| popularities.reduce((sum, p) => sum + Math.pow(p - avg_popularity, 2), 0) / track_count; | ||
| const popularity_std_dev = Math.round(Math.sqrt(variance) * 10) / 10; |
There was a problem hiding this comment.
Use the unrounded mean for the variance math.
Line 36 rounds the mean before Lines 40-42 compute popularity_std_dev, which can nudge consistency_score and flip borderline catalog labels. Keep a raw average for the calculations and round only in the returned payload.
Possible fix
- const avg_popularity = Math.round(popularities.reduce((s, p) => s + p, 0) / track_count);
+ const avgPopularityRaw = popularities.reduce((s, p) => s + p, 0) / track_count;
+ const avg_popularity = Math.round(avgPopularityRaw);
const top_track_popularity = Math.max(...popularities);
// Standard deviation
const variance =
- popularities.reduce((sum, p) => sum + Math.pow(p - avg_popularity, 2), 0) / track_count;
+ popularities.reduce((sum, p) => sum + Math.pow(p - avgPopularityRaw, 2), 0) / track_count;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/analyzeCatalogDepth.ts` around lines 34 - 42, The
avg_popularity value is being rounded before it's used to compute
variance/popularity_std_dev which alters the std-dev; compute a raw mean (e.g.,
raw_avg or mean) from popularities using track_count and use that raw value in
the variance/popularity_std_dev calculation, then only round values
(avg_popularity and popularity_std_dev) when building the returned payload;
update references to avg_popularity in the variance calculation to use the new
raw mean and keep the rounded avg_popularity for output only.
| lines.push(`## Opportunity Scores`); | ||
| lines.push(``); | ||
| lines.push( | ||
| `> Algorithmically computed from real audio data and Spotify metrics — not AI-generated text.`, | ||
| ); | ||
| lines.push(``); | ||
| lines.push(`**Overall Score: ${opportunity_scores.overall}/100**`); | ||
| lines.push(``); | ||
| lines.push(`| Category | Score | Rating | Signal |`); | ||
| lines.push(`|----------|-------|--------|--------|`); | ||
| lines.push( | ||
| `| 🎬 Sync & Licensing | ${opportunity_scores.sync.score}/100 | ${ratingEmoji(opportunity_scores.sync.rating)} ${opportunity_scores.sync.rating} | ${scoreBar(opportunity_scores.sync.score)} |`, | ||
| ); | ||
| lines.push( | ||
| `| 🎵 Playlist Placement | ${opportunity_scores.playlist.score}/100 | ${ratingEmoji(opportunity_scores.playlist.rating)} ${opportunity_scores.playlist.rating} | ${scoreBar(opportunity_scores.playlist.score)} |`, | ||
| ); | ||
| lines.push( | ||
| `| 🎤 A&R Priority | ${opportunity_scores.ar.score}/100 | ${ratingEmoji(opportunity_scores.ar.rating)} ${opportunity_scores.ar.rating} | ${scoreBar(opportunity_scores.ar.score)} |`, | ||
| ); | ||
| lines.push( | ||
| `| 🤝 Brand Partnership | ${opportunity_scores.brand.score}/100 | ${ratingEmoji(opportunity_scores.brand.rating)} ${opportunity_scores.brand.rating} | ${scoreBar(opportunity_scores.brand.score)} |`, | ||
| ); | ||
| lines.push(``); | ||
|
|
||
| lines.push(`**What's driving these scores:**`); | ||
| lines.push(``); | ||
| lines.push(`- **Sync:** ${opportunity_scores.sync.rationale}`); | ||
| lines.push(`- **Playlist:** ${opportunity_scores.playlist.rationale}`); | ||
| lines.push(`- **A&R:** ${opportunity_scores.ar.rationale}`); | ||
| lines.push(`- **Brand:** ${opportunity_scores.brand.rationale}`); |
There was a problem hiding this comment.
Don’t let the new opportunity dashboard hard-fail formatting.
The current pipeline is failing on Line 101 because some existing pack shapes still reach this formatter without opportunity_scores. Please either guard this block or normalize defaults before formatting so one missing enrichment does not prevent the rest of the report from rendering.
🧰 Tools
🪛 GitHub Actions: Unit Tests
[error] 101-101: TypeError: Cannot read properties of undefined (reading 'overall') at opportunity_scores.overall while building Markdown (affects 21 tests in formatArtistIntelPackAsMarkdown.test.ts).
🪛 GitHub Check: test
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes industry pack sections
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:155:20
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes web context and citations
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:148:20
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes mood tags
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:142:20
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes audience profile
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:135:20
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes music DNA section when analysis is available
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:127:20
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes top track name and album
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:121:20
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes genre information
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:115:20
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes follower count and popularity
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:109:20
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes elapsed time in the header
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:104:20
[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes the artist name in the heading
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:99:20
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/formatArtistIntelPackAsMarkdown.ts` around lines 95 - 124,
The formatter currently assumes opportunity_scores exists and crashes; update
formatArtistIntelPackAsMarkdown to guard or normalize before pushing lines:
check if opportunity_scores is present on the pack (or create a default
opportunity_scores object with safe defaults for overall, sync/playlist/ar/brand
{score, rating, rationale}) and then use those safe values when calling
ratingEmoji(...) and scoreBar(...); alternatively wrap the whole Opportunity
Scores block in a presence check for opportunity_scores and skip it if missing,
ensuring all references (opportunity_scores.overall,
opportunity_scores.sync.score, opportunity_scores.sync.rating,
opportunity_scores.sync.rationale, and similarly for playlist/ar/brand) and
calls to ratingEmoji and scoreBar only run against validated/defaulted data.
| // All three data-fetch steps run in parallel | ||
| const [musicAnalysis, webContext, peerBenchmark] = await Promise.all([ | ||
| spotifyData.previewUrl ? getArtistMusicAnalysis(spotifyData.previewUrl) : Promise.resolve(null), | ||
| getArtistWebContext(artistName), | ||
| getRelatedArtistsData(artist.id, artist.followers.total, artist.popularity), | ||
| ]); |
There was a problem hiding this comment.
Make the enrichment fan-out fail soft.
These branches are optional enrichments, but Promise.all turns any rejection from MusicFlamingo, Perplexity, or related-artists lookup into a full pack failure. Please switch this block to safe fallbacks or Promise.allSettled so a provider outage degrades to null instead of dropping the entire response. As per coding guidelines, lib/**/*.ts: “Proper error handling”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/generateArtistIntelPack.ts` around lines 74 - 79, The current
Promise.all call for optional enrichments (getArtistMusicAnalysis,
getArtistWebContext, getRelatedArtistsData) must be made "fail soft": replace
the Promise.all with Promise.allSettled (or wrap each call in a try/catch) and
map any rejected result to null so that failures from MusicFlamingo, Perplexity,
or related-artists do not throw the whole pack; ensure spotifyData.previewUrl
still gates getArtistMusicAnalysis and that the resolved values are assigned
back to the same variables (musicAnalysis, webContext, peerBenchmark) with null
fallbacks on error.
| import generateAccessToken from "@/lib/spotify/generateAccessToken"; | ||
| import { getRelatedArtists } from "@/lib/spotify/getRelatedArtists"; |
There was a problem hiding this comment.
Avoid credential-gating the whole module at import time.
generateAccessToken.ts throws when Spotify credentials are absent, so this top-level import can make any consumer of getRelatedArtistsData() fail during module evaluation instead of returning null. That matches the current test failure; please lazy-load the token helper here or move env validation into the helper body.
🧰 Tools
🪛 GitHub Actions: Unit Tests
[error] 1-1: Test suite failure caused by missing Spotify credentials (upstream error from generateAccessToken).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/getRelatedArtistsData.ts` around lines 1 - 2, The module
currently imports generateAccessToken at top-level which throws when Spotify
creds are missing; change to lazy-load it inside getRelatedArtistsData (or move
the env validation into generateAccessToken) so module evaluation never throws:
update getRelatedArtistsData to require/import or call generateAccessToken
dynamically at runtime before using it (and keep getRelatedArtists imported as
needed), ensuring when creds are absent the function returns null instead of
causing an import-time exception.
| // Take top 5 most-followed peers for a focused comparison | ||
| const peers: PeerArtist[] = related | ||
| .slice(0, 10) | ||
| .sort((a, b) => b.followers.total - a.followers.total) | ||
| .slice(0, 5) |
There was a problem hiding this comment.
Sort the full related-artist set before truncating it.
slice(0, 10).sort(...).slice(0, 5) only ranks the first 10 API results, not the full related-artist list. If Spotify returns a larger relevance-ordered set, the “top 5 most-followed peers” and all downstream medians/percentiles become arbitrary.
Possible fix
- const peers: PeerArtist[] = related
- .slice(0, 10)
- .sort((a, b) => b.followers.total - a.followers.total)
- .slice(0, 5)
+ const peers: PeerArtist[] = [...related]
+ .sort((a, b) => b.followers.total - a.followers.total)
+ .slice(0, 5)
.map(a => ({🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/getRelatedArtistsData.ts` around lines 44 - 48, The current
code slices related before sorting which only ranks the first 10 items; instead
sort the entire related array by followers (b.followers.total -
a.followers.total) and then take the top 5, i.e. replace the chain
"related.slice(0, 10).sort(...).slice(0, 5)" with a sort-first-then-slice
approach so the peers variable truly contains the top 5 most-followed artists;
update the peers assignment in getRelatedArtistsData (the peers/related logic)
accordingly.
| const median = (arr: number[]) => { | ||
| const mid = Math.floor(arr.length / 2); | ||
| return arr.length % 2 !== 0 ? arr[mid] : Math.round((arr[mid - 1] + arr[mid]) / 2); | ||
| }; | ||
|
|
||
| const percentile = (arr: number[], value: number) => { | ||
| const below = arr.filter(v => v < value).length; | ||
| return Math.round((below / arr.length) * 100); | ||
| }; |
There was a problem hiding this comment.
Percentile rank should account for ties.
With the current v < value formula, an artist tied with every peer shows up as the 0th percentile. Popularity is a 0–100 integer, so ties are common, and this also skews the A&R bonus that consumes popularity_percentile.
Possible fix
const percentile = (arr: number[], value: number) => {
const below = arr.filter(v => v < value).length;
- return Math.round((below / arr.length) * 100);
+ const equal = arr.filter(v => v === value).length;
+ return Math.round(((below + equal / 2) / arr.length) * 100);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/getRelatedArtistsData.ts` around lines 60 - 68, Percentile
calculation in the percentile(arr:number[], value:number) function ignores ties
(uses v < value), which makes tied popularities report 0; change the logic to
count values strictly less (countLess) and values equal (countEqual) and compute
percentile as round(((countLess + 0.5 * countEqual) / arr.length) * 100), and
handle empty arrays defensively (e.g., return 0); update the percentile function
(and any callers that expect popularity_percentile) to use this tied-aware
formula while leaving median(...) unchanged.
| const response = await fetch(`https://api.spotify.com/v1/artists/${artistId}/related-artists`, { | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Add a timeout to the Spotify request.
This call has no cancellation path, so a slow Spotify edge can stall the whole pack generation until the platform-level timeout kicks in. Please add an explicit timeout/abort and treat that the same way you already treat other soft failures here.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/spotify/getRelatedArtists.ts` around lines 15 - 19, The Spotify fetch in
getRelatedArtists currently has no cancellation; wrap the request with an
AbortController and a short timeout (e.g., 2–5s) that calls controller.abort()
via setTimeout, pass controller.signal to the fetch call, and catch the abort
error; in the getRelatedArtists function treat an aborted/timeout error the same
as other soft failures (returning the existing fallback path), and ensure you
clear the timeout on success to avoid leaks.
…coverage - computeArtistOpportunityScores: fix misleading "established/saturated" A&R rationale for artists with <1000 followers AND <10 popularity. These are pre-market acts (e.g. Gatsby Grace: 2 followers, 0 popularity), not saturated ones. Now correctly says "Pre-market artist — highest early- discovery upside" and awards a +10 discovery bonus. - Add computeArtistOpportunityScores.test.ts with 6 tests covering pre-market detection, high-follower low-popularity case, music analysis paths, and overall score weighting. - generateArtistIntelPack.test.ts: add Gatsby Grace scenario (8 tests) using real Spotify data (Spotify ID: 7ljukJB2Ctl0T4vCoYfb2x, 2 followers, 0 popularity, no genres). Mock now also covers getRelatedArtistsData. - formatArtistIntelPackAsMarkdown.test.ts: add missing peer_benchmark, opportunity_scores, and catalog_depth fields to basePack — these were added in a prior upgrade but the test mock was never updated (21 tests now pass). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
lib/artistIntel/computeArtistOpportunityScores.ts (1)
46-280: Split the scorer into smaller pure helpers.
computeArtistOpportunityScoresnow owns four separate scoring algorithms, rationale assembly, clamping, and weighting in one ~230-line function. Extracting per-domain helpers plus shared score-building utilities will make threshold changes much safer to review and test.♻️ Refactor sketch
+function clampScore(score: number): number { + return Math.min(100, Math.max(0, score)); +} + +function toOpportunityScore( + score: number, + rationale: string[], + fallback: string, +): OpportunityScore { + const clamped = clampScore(score); + return { + score: clamped, + rating: rating(clamped), + rationale: rationale.join(". ") || fallback, + }; +} + +function computeSyncOpportunityScore( + musicAnalysis: ArtistMusicAnalysis | null, + popularity: number, +): OpportunityScore { + // sync-specific logic only +} + +function computePlaylistOpportunityScore( + musicAnalysis: ArtistMusicAnalysis | null, + popularity: number, +): OpportunityScore { + // playlist-specific logic only +} + +function computeArOpportunityScore( + followers: number, + popularity: number, + peerBenchmark: PeerBenchmark | null, +): OpportunityScore { + // A&R-specific logic only +} + +function computeBrandOpportunityScore( + musicAnalysis: ArtistMusicAnalysis | null, + popularity: number, +): OpportunityScore { + // brand-specific logic only +} + export function computeArtistOpportunityScores(...) { - // all scoring logic inline - return { - sync: { ... }, - playlist: { ... }, - ar: { ... }, - brand: { ... }, - overall, - }; + const sync = computeSyncOpportunityScore(musicAnalysis, popularity); + const playlist = computePlaylistOpportunityScore(musicAnalysis, popularity); + const ar = computeArOpportunityScore(followers, popularity, peerBenchmark); + const brand = computeBrandOpportunityScore(musicAnalysis, popularity); + const overall = Math.round( + ar.score * 0.3 + playlist.score * 0.25 + sync.score * 0.25 + brand.score * 0.2, + ); + return { sync, playlist, ar, brand, overall }; }As per coding guidelines,
lib/**/*.ts: "Single responsibility per function", "Keep functions under 50 lines", and "DRY: Consolidate similar logic into shared utilities".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/artistIntel/computeArtistOpportunityScores.ts` around lines 46 - 280, The computeArtistOpportunityScores function is doing four distinct scoring workflows plus common tasks (clamping, rating, rationale assembly, weighting); extract pure helper functions computeSyncScore, computePlaylistScore, computeARScore, and computeBrandScore that each accept the minimal inputs (musicAnalysis/catalog_metadata, followers, popularity, peerBenchmark as needed) and return { score:number, rationale:string[] }, and pull shared utilities like clampScore(score), buildRationale(rationaleArray) and any numeric helpers (e.g., followerEfficiency) into small pure functions; then reduce computeArtistOpportunityScores to calling these helpers, clamping/ratifying results with clampScore and rating(), joining rationales with buildRationale, and computing the overall weighted score—ensure all original symbols (syncRationale/playlistRationale/arRationale/brandRationale, rating(), and overall weighting) are preserved so behavior remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/artistIntel/computeArtistOpportunityScores.ts`:
- Around line 151-168: The pre-market check is being bypassed because
followerEfficiency is evaluated first; move the pre-market condition (check
followers < 1000 && popularity < 10) before the followerEfficiency tiers so
tiny-but-nonzero artists (e.g., followers=10, popularity=9) get the pre-market
bonus; update computeArtistOpportunityScores to evaluate the pre-market branch
prior to computing/applying the followerEfficiency > 12 / > 8 branches and still
push the same arScore and arRationale entries when that pre-market condition
matches.
- Around line 176-189: The peer-gap and median-gap strings currently use whole-K
rounding (e.g., ((...)/1000).toFixed(0) and (followerGap/1000).toFixed(0)) which
yields misleading "0K" or coarse values; update the formatting logic where
followerGap, peerBenchmark.median_followers and the corresponding arRationale
pushes are built (look for variables followerGap,
peerBenchmark.median_followers, arRationale and peerBenchmark.top_peer?.name) to
format deltas < 1000 as raw integer counts (e.g., "499 below peer median") and
deltas >= 1000 with one decimal K precision (e.g., "1.2K gap to top peer");
implement this either via a small helper formatDelta(delta) used in both the
median and top-peer messages or inline conditional formatting before pushing to
arRationale.
---
Nitpick comments:
In `@lib/artistIntel/computeArtistOpportunityScores.ts`:
- Around line 46-280: The computeArtistOpportunityScores function is doing four
distinct scoring workflows plus common tasks (clamping, rating, rationale
assembly, weighting); extract pure helper functions computeSyncScore,
computePlaylistScore, computeARScore, and computeBrandScore that each accept the
minimal inputs (musicAnalysis/catalog_metadata, followers, popularity,
peerBenchmark as needed) and return { score:number, rationale:string[] }, and
pull shared utilities like clampScore(score), buildRationale(rationaleArray) and
any numeric helpers (e.g., followerEfficiency) into small pure functions; then
reduce computeArtistOpportunityScores to calling these helpers,
clamping/ratifying results with clampScore and rating(), joining rationales with
buildRationale, and computing the overall weighted score—ensure all original
symbols (syncRationale/playlistRationale/arRationale/brandRationale, rating(),
and overall weighting) are preserved so behavior remains unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8aab9895-99dd-408b-b1b7-603480e0473b
⛔ Files ignored due to path filters (3)
lib/artistIntel/__tests__/computeArtistOpportunityScores.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artistIntel/__tests__/formatArtistIntelPackAsMarkdown.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artistIntel/__tests__/generateArtistIntelPack.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (1)
lib/artistIntel/computeArtistOpportunityScores.ts
| const followerEfficiency = followers > 0 ? popularity / Math.log10(followers + 1) : 0; | ||
|
|
||
| if (followerEfficiency > 12) { | ||
| arScore += 25; | ||
| arRationale.push( | ||
| `High popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) — strong algorithmic pull without mass audience`, | ||
| ); | ||
| } else if (followerEfficiency > 8) { | ||
| arScore += 15; | ||
| arRationale.push( | ||
| `Solid popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) indicates organic traction`, | ||
| ); | ||
| } else if (followers < 1000 && popularity < 10) { | ||
| // Pre-market artist: very low followers AND very low popularity = early discovery opportunity | ||
| arScore += 10; | ||
| arRationale.push( | ||
| `Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`, | ||
| ); |
There was a problem hiding this comment.
Check the pre-market rule before the efficiency tiers.
A tiny-but-nonzero artist can miss the new pre-market classification here. For example, followers = 10 and popularity = 9 gives followerEfficiency ≈ 8.6, so this code takes the "organic traction" branch before it ever reaches the intended <1000 && <10 pre-market bonus/rationale.
🐛 Suggested fix
- if (followerEfficiency > 12) {
+ if (followers < 1000 && popularity < 10) {
+ // Pre-market artist: very low followers AND very low popularity = early discovery opportunity
+ arScore += 10;
+ arRationale.push(
+ `Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`,
+ );
+ } else if (followerEfficiency > 12) {
arScore += 25;
arRationale.push(
`High popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) — strong algorithmic pull without mass audience`,
);
} else if (followerEfficiency > 8) {
arScore += 15;
arRationale.push(
`Solid popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) indicates organic traction`,
);
- } else if (followers < 1000 && popularity < 10) {
- // Pre-market artist: very low followers AND very low popularity = early discovery opportunity
- arScore += 10;
- arRationale.push(
- `Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`,
- );
} else {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const followerEfficiency = followers > 0 ? popularity / Math.log10(followers + 1) : 0; | |
| if (followerEfficiency > 12) { | |
| arScore += 25; | |
| arRationale.push( | |
| `High popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) — strong algorithmic pull without mass audience`, | |
| ); | |
| } else if (followerEfficiency > 8) { | |
| arScore += 15; | |
| arRationale.push( | |
| `Solid popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) indicates organic traction`, | |
| ); | |
| } else if (followers < 1000 && popularity < 10) { | |
| // Pre-market artist: very low followers AND very low popularity = early discovery opportunity | |
| arScore += 10; | |
| arRationale.push( | |
| `Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`, | |
| ); | |
| const followerEfficiency = followers > 0 ? popularity / Math.log10(followers + 1) : 0; | |
| if (followers < 1000 && popularity < 10) { | |
| // Pre-market artist: very low followers AND very low popularity = early discovery opportunity | |
| arScore += 10; | |
| arRationale.push( | |
| `Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`, | |
| ); | |
| } else if (followerEfficiency > 12) { | |
| arScore += 25; | |
| arRationale.push( | |
| `High popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) — strong algorithmic pull without mass audience`, | |
| ); | |
| } else if (followerEfficiency > 8) { | |
| arScore += 15; | |
| arRationale.push( | |
| `Solid popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) indicates organic traction`, | |
| ); | |
| } else { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/computeArtistOpportunityScores.ts` around lines 151 - 168,
The pre-market check is being bypassed because followerEfficiency is evaluated
first; move the pre-market condition (check followers < 1000 && popularity < 10)
before the followerEfficiency tiers so tiny-but-nonzero artists (e.g.,
followers=10, popularity=9) get the pre-market bonus; update
computeArtistOpportunityScores to evaluate the pre-market branch prior to
computing/applying the followerEfficiency > 12 / > 8 branches and still push the
same arScore and arRationale entries when that pre-market condition matches.
| const followerGap = peerBenchmark.top_peer ? peerBenchmark.top_peer.followers - followers : 0; | ||
|
|
||
| if (followers < peerBenchmark.median_followers) { | ||
| arScore += 20; | ||
| arRationale.push( | ||
| `${((peerBenchmark.median_followers - followers) / 1000).toFixed(0)}K below peer median — significant upside runway`, | ||
| ); | ||
| } else { | ||
| arRationale.push("At or above peer median — focus on next tier of growth"); | ||
| } | ||
|
|
||
| if (followerGap > 0) { | ||
| arRationale.push( | ||
| `${(followerGap / 1000).toFixed(0)}K gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`, |
There was a problem hiding this comment.
Avoid whole-K rounding in the peer-gap copy.
These strings can emit 0K below peer median for a 499-follower gap and 1K for 500, which makes the new peer benchmarking output look less trustworthy than the underlying data. Format sub-1K deltas as raw counts and use at least one decimal for larger gaps.
🧮 Suggested fix
+ const formatFollowerGap = (value: number) =>
+ value < 1000 ? value.toLocaleString() : `${(value / 1000).toFixed(1)}K`;
+
if (followers < peerBenchmark.median_followers) {
arScore += 20;
arRationale.push(
- `${((peerBenchmark.median_followers - followers) / 1000).toFixed(0)}K below peer median — significant upside runway`,
+ `${formatFollowerGap(peerBenchmark.median_followers - followers)} below peer median — significant upside runway`,
);
} else {
arRationale.push("At or above peer median — focus on next tier of growth");
}
if (followerGap > 0) {
arRationale.push(
- `${(followerGap / 1000).toFixed(0)}K gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`,
+ `${formatFollowerGap(followerGap)} gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`,
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const followerGap = peerBenchmark.top_peer ? peerBenchmark.top_peer.followers - followers : 0; | |
| if (followers < peerBenchmark.median_followers) { | |
| arScore += 20; | |
| arRationale.push( | |
| `${((peerBenchmark.median_followers - followers) / 1000).toFixed(0)}K below peer median — significant upside runway`, | |
| ); | |
| } else { | |
| arRationale.push("At or above peer median — focus on next tier of growth"); | |
| } | |
| if (followerGap > 0) { | |
| arRationale.push( | |
| `${(followerGap / 1000).toFixed(0)}K gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`, | |
| const followerGap = peerBenchmark.top_peer ? peerBenchmark.top_peer.followers - followers : 0; | |
| const formatFollowerGap = (value: number) => | |
| value < 1000 ? value.toLocaleString() : `${(value / 1000).toFixed(1)}K`; | |
| if (followers < peerBenchmark.median_followers) { | |
| arScore += 20; | |
| arRationale.push( | |
| `${formatFollowerGap(peerBenchmark.median_followers - followers)} below peer median — significant upside runway`, | |
| ); | |
| } else { | |
| arRationale.push("At or above peer median — focus on next tier of growth"); | |
| } | |
| if (followerGap > 0) { | |
| arRationale.push( | |
| `${formatFollowerGap(followerGap)} gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`, | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/artistIntel/computeArtistOpportunityScores.ts` around lines 176 - 189,
The peer-gap and median-gap strings currently use whole-K rounding (e.g.,
((...)/1000).toFixed(0) and (followerGap/1000).toFixed(0)) which yields
misleading "0K" or coarse values; update the formatting logic where followerGap,
peerBenchmark.median_followers and the corresponding arRationale pushes are
built (look for variables followerGap, peerBenchmark.median_followers,
arRationale and peerBenchmark.top_peer?.name) to format deltas < 1000 as raw
integer counts (e.g., "499 below peer median") and deltas >= 1000 with one
decimal K precision (e.g., "1.2K gap to top peer"); implement this either via a
small helper formatDelta(delta) used in both the median and top-peer messages or
inline conditional formatting before pushing to arRationale.
Automated PR from coding agent.
Summary by CodeRabbit
New Features
Documentation