Skip to content

docs: make SRP/DRY review expectations explicit in AGENTS#258

Open
sidneyswift wants to merge 2 commits intotestfrom
chore/clarify-agents-guidelines
Open

docs: make SRP/DRY review expectations explicit in AGENTS#258
sidneyswift wants to merge 2 commits intotestfrom
chore/clarify-agents-guidelines

Conversation

@sidneyswift
Copy link
Contributor

@sidneyswift sidneyswift commented Mar 4, 2026

What this updates

  • Adds explicit PR-blocking reviewer expectations for SRP/DRY in AGENTS.md
  • Adds concrete pre-push SRP/DRY checklist
  • Fixes MCP auth example to consistently use resolveAccountId()
  • Clarifies API/MCP schema-sharing and contract parity requirements

Why

Recent review feedback requested stricter SRP/DRY compliance (utility/file splitting and schema reuse). This update makes expectations unambiguous for future agent-authored changes.

Scope

  • Docs-only change
  • No runtime API behavior changes

Summary by CodeRabbit

  • New Features
    • Added content creation API with template support and customization options
    • Added cost estimation for content projects based on batch size
    • Added template library and retrieval for content creation workflows
    • Added content validation to verify readiness before creating content
    • Added video asset management for created content

@vercel
Copy link
Contributor

vercel bot commented Mar 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Mar 4, 2026 10:57pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive content creation API feature with four new endpoints and supporting infrastructure. It includes request validation layers, delegated handler functions, Trigger.dev integration for background job processing, and Supabase storage integration for persisting video outputs. The feature supports template management, cost estimation, artist readiness validation, and content creation workflows.

Changes

Cohort / File(s) Summary
API Routes
app/api/content/create/route.ts, app/api/content/estimate/route.ts, app/api/content/templates/route.ts, app/api/content/validate/route.ts
Four new Next.js API route files with CORS preflight support (OPTIONS handlers) and HTTP method handlers (POST/GET). All export dynamic, fetchCache, and revalidate constants for route behavior control.
Request Validation
lib/content/validate...Body.ts, lib/content/validate...Query.ts
Five validation modules implementing Zod schemas for request payloads and query parameters. Handle authentication checks via validateAuthContext and return 400 responses for validation failures with detailed error messages.
Handler Functions
lib/content/create...Handler.ts, lib/content/getContent...Handler.ts
Four handler modules that validate incoming requests and delegate to business logic. Return appropriate HTTP responses with CORS headers and JSON payloads (202 for async, 200 for success, 400/500 for errors).
Content Templates & Readiness
lib/content/contentTemplates.ts, lib/content/getArtistContentReadiness.ts
Template management and artist readiness evaluation. getArtistContentReadiness inspects GitHub repository file trees to verify required/recommended files (face-guide.png, config.json, audio, metadata) and reports issues with severity levels.
Video Persistence
lib/content/persistCreateContentRunVideo.ts
Handles video output persistence from completed Trigger.dev runs. Fetches videos from source URLs, uploads to Supabase storage, creates file database records, and generates signed URLs for later retrieval.
Supabase Integration
lib/supabase/files/selectFileByStorageKey.ts, lib/supabase/storage/createSignedFileUrlByKey.ts
Database query and storage utilities. selectFileByStorageKey queries the files table; createSignedFileUrlByKey generates time-limited signed URLs for secure file access.
Trigger.dev Integration
lib/trigger/triggerCreateContent.ts
Background job trigger module. Wraps Trigger.dev's tasks.trigger API to queue content creation jobs with validated payload data.
Utilities
lib/const.ts, lib/tasks/getTaskRunHandler.ts
Added CREATE_CONTENT_TASK_ID constant; refactored EVAL_ARTISTS formatting. Enhanced getTaskRunHandler to hydrate task run videos via persistCreateContentRunVideo.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as API Route:<br/>create
    participant Validator as Validator:<br/>validateCreateContentBody
    participant Auth as Auth Context
    participant Readiness as getArtistContentReadiness
    participant GitHub as GitHub API
    participant Trigger as Trigger.dev
    participant BgJob as Background Job:<br/>CreateContent

    Client->>API: POST /api/content/create<br/>(artist_slug, template, lipsync)
    API->>Validator: validateCreateContentBody(request)
    Validator->>Auth: validateAuthContext()
    Auth-->>Validator: accountId (or NextResponse)
    Validator-->>API: ValidatedCreateContentBody | NextResponse
    
    alt Validation Failed
        API-->>Client: 400 Bad Request (CORS headers)
    else Validation Passed
        API->>Readiness: getArtistContentReadiness(accountId, artistSlug)
        Readiness->>GitHub: Fetch repo file tree
        GitHub-->>Readiness: File paths
        Readiness-->>API: ArtistContentReadiness<br/>(ready, missing, warnings)
        
        alt Artist Not Ready
            API-->>Client: 400 Bad Request<br/>(readiness details, CORS headers)
        else Artist Ready
            API->>Trigger: triggerCreateContent(payload)
            Trigger-->>BgJob: Queue task
            Trigger-->>API: { runId, ... }
            API-->>Client: 202 Accepted<br/>(runId, status, CORS headers)
            BgJob->>BgJob: Process video creation
        end
    end
Loading
sequenceDiagram
    actor Client
    participant API as API Route:<br/>validate
    participant Validator as validateGetContentValidateQuery
    participant Auth as Auth Context
    participant Readiness as getArtistContentReadiness
    participant GitHub as GitHub API

    Client->>API: GET /api/content/validate<br/>?artist_slug=...
    API->>Validator: validateGetContentValidateQuery(request)
    Validator->>Auth: validateAuthContext()
    Auth-->>Validator: accountId (or NextResponse)
    
    alt Auth Failed
        Validator-->>API: NextResponse
        API-->>Client: 401/403 Unauthorized (CORS headers)
    else Auth Passed
        Validator-->>API: { accountId, artistSlug }
        API->>Readiness: getArtistContentReadiness(accountId, artistSlug)
        Readiness->>GitHub: Fetch repo file tree
        GitHub-->>Readiness: File paths
        Readiness-->>API: ArtistContentReadiness
        API-->>Client: 200 Success<br/>{ status, ready, missing, warnings }<br/>(CORS headers)
    end
Loading
sequenceDiagram
    participant BgJob as Background Job:<br/>Completed
    participant Hydrator as persistCreateContentRunVideo
    participant Fetch as Fetch Video
    participant Storage as Supabase Storage
    participant Database as Supabase Database
    participant FileQuery as selectFileByStorageKey
    participant SignedURL as createSignedFileUrlByKey

    BgJob->>Hydrator: [Completed Run]
    
    alt Task is CREATE_CONTENT
        Hydrator->>FileQuery: selectFileByStorageKey(storageKey)
        
        alt File Exists
            FileQuery-->>Hydrator: FileRecord
            Hydrator->>SignedURL: createSignedFileUrlByKey(storageKey)
            SignedURL-->>Hydrator: signed URL
            Hydrator-->>BgJob: [Run with video metadata]
        else File Not Found
            FileQuery-->>Hydrator: null
            Hydrator->>Fetch: Fetch videoSourceUrl
            Fetch-->>Hydrator: blob, mimeType
            Hydrator->>Storage: Upload blob with storageKey
            Storage-->>Hydrator: { path: ... }
            Hydrator->>Database: Create file record<br/>(ownerAccountId, artistAccountId, storageKey, etc.)
            Database-->>Hydrator: FileRecord (with fileId)
            Hydrator->>SignedURL: createSignedFileUrlByKey(storageKey)
            SignedURL-->>Hydrator: signed URL
            Hydrator-->>BgJob: [Run with video metadata & signed URL]
        end
    else Other Task Type
        Hydrator-->>BgJob: [Unchanged Run]
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

The changes span 17+ new modules with heterogeneous complexity. While API routes and most handlers follow consistent patterns (low effort), two modules—getArtistContentReadiness (131 lines, GitHub API integration with file tree inspection and validation logic) and persistCreateContentRunVideo (133 lines, multi-step video fetch/upload/storage/database workflow)—contain dense, intricate logic requiring careful architectural review. Understanding interactions across validation chains, handler delegation, Supabase integration, and Trigger.dev job triggering adds cognitive load. The varied nature of edits across domain-specific concerns (auth, file operations, job triggering, GitHub API) necessitates separate reasoning per module family.

Poem

🎬 Content creation takes flight,
Four endpoints harmonize just right,
Templates, validation, readiness clear,
Videos persist without fear,
Trigger.dev handles the queue—
Your features are finally true! ✨

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning PR violates stated SOLID principles: 128 lines of duplicated route handlers, repeated validation patterns across three modules, oversized functions combining multiple responsibilities, and unaddressed security issues including unsafe boolean coercion and SSRF vulnerabilities. Implement route factory to eliminate handler duplication, extract reusable validation wrapper, split oversized functions into focused helpers, fix type safety and security issues per review guidelines.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/clarify-agents-guidelines

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (6)
lib/content/validateGetContentEstimateQuery.ts (1)

7-13: Export the schema constant for consistency with validator conventions.

ValidatedGetContentEstimateQuery is exported, but the schema itself is not. Exporting both keeps validation contracts reusable and consistent.

As per coding guidelines, lib/**/validate*.ts: “... export both the schema and inferred TypeScript type”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/validateGetContentEstimateQuery.ts` around lines 7 - 13, Export
the existing Zod schema constant so it’s available alongside the inferred type:
add an export for getContentEstimateQuerySchema (the z.object(...) currently
declared) so both getContentEstimateQuerySchema and
ValidatedGetContentEstimateQuery are exported; update the declaration of
getContentEstimateQuerySchema to be exported (export const
getContentEstimateQuerySchema = ...) to follow the validator convention and keep
the schema and type reusable.
lib/content/contentTemplates.ts (1)

7-25: Remove duplicated default template literal to avoid drift.

DEFAULT_CONTENT_TEMPLATE repeats "artist-caption-bedroom" from CONTENT_TEMPLATES. Derive it from the source list so updates stay consistent.

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/content/contentTemplates.ts` around lines 7 - 25,
DEFAULT_CONTENT_TEMPLATE duplicates the literal "artist-caption-bedroom"; change
it to be derived from the source array to avoid drift by referencing
CONTENT_TEMPLATES so updates stay consistent. Replace the hardcoded
DEFAULT_CONTENT_TEMPLATE value with a computed value that reads
CONTENT_TEMPLATES (for example using CONTENT_TEMPLATES[0].name or a small helper
like getDefaultTemplateName()) after CONTENT_TEMPLATES is declared, ensuring the
symbol DEFAULT_CONTENT_TEMPLATE is exported and CONTENT_TEMPLATES remains the
single source of truth.
lib/content/validateGetContentValidateQuery.ts (1)

7-16: Export the Zod schema and inferred query type from this validator module.

This file currently keeps the schema private and uses a manual output type. Please export a schema type (z.infer) for the validated query contract to match repository validator conventions.

As per coding guidelines, lib/**/validate*.ts: “Create validate functions ... that export both the schema and inferred TypeScript type”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/validateGetContentValidateQuery.ts` around lines 7 - 16, The
schema is currently private and the output type is hand-written; export the Zod
schema and derive the TS type from it: export the existing
getContentValidateQuerySchema and replace the manual
ValidatedGetContentValidateQuery definition with export type
ValidatedGetContentValidateQuery = z.infer<typeof
getContentValidateQuerySchema>, ensuring the module exports both the schema and
the inferred type so callers can import the validator contract directly.
lib/trigger/triggerCreateContent.ts (1)

4-9: Extract this payload shape into a shared type to prevent drift.

TriggerCreateContentPayload overlaps with content run output fields in lib/content/persistCreateContentRunVideo.ts (account/template/lipsync identifiers). A shared contract type would reduce divergence risk.

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/trigger/triggerCreateContent.ts` around lines 4 - 9, Extract the
overlapping shape into a single exported shared type (e.g.,
ContentCreatePayload) instead of duplicating it: create the new shared type
matching the fields in TriggerCreateContentPayload and the content run output
used in persistCreateContentRunVideo.ts, replace TriggerCreateContentPayload
with the new shared type, and update all imports/usages (including where
persistCreateContentRunVideo references account/template/lipsync) to import the
shared type so both modules reference the same contract and prevent divergence.
lib/content/getArtistContentReadiness.ts (1)

45-120: Refactor repeated file checks into a declarative rule set.

This function is doing too much in one block and repeats near-identical issue construction six times. Extracting checks into a config list + small helper will improve readability and keep the function below the 50-line target.

As per coding guidelines: lib/**/*.ts: For domain functions, ensure ... 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/content/getArtistContentReadiness.ts` around lines 45 - 120, The
getArtistContentReadiness function repeats nearly identical file-check logic;
refactor by extracting a declarative rules array (each item: path, severity,
fix, and a check predicate) and a small helper (e.g., applyRule or checkRule)
that runs the predicate using existing helpers hasFile/hasAnyMp3 and pushes the
issue to issues when false; replace the repeated if-blocks with a single loop
over rules to construct issues, keeping getArtistContentReadiness concise and
under 50 lines while reusing the hasFile and hasAnyMp3 symbols.
lib/content/createContentHandler.ts (1)

47-49: Add explicit return type to triggerCreateContent for type safety.

The function lacks a return type annotation and relies on the Trigger.dev SDK contract rather than compile-time guarantees. While the code works (accessing handle.id succeeds), explicit typing strengthens the type system and aligns with TypeScript best practices.

Consider adding the return type from @trigger.dev/sdk:

export async function triggerCreateContent(payload: TriggerCreateContentPayload): Promise<TaskRunHandle> {
  const handle = await tasks.trigger(CREATE_CONTENT_TASK_ID, payload);
  return handle;
}

Note: Similar trigger functions (triggerSetupSandbox, triggerRunSandboxCommand) also lack explicit return types and would benefit from the same treatment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/createContentHandler.ts` around lines 47 - 49, Add explicit
return type annotations to the trigger helper functions to enforce compile-time
safety: update triggerCreateContent to declare its return type as
Promise<TaskRunHandle> and annotate its payload parameter with
TriggerCreateContentPayload imported from `@trigger.dev/sdk`; do the same for
triggerSetupSandbox and triggerRunSandboxCommand (use the appropriate payload
types for each) so each function signature explicitly returns
Promise<TaskRunHandle> and prevents relying solely on the Trigger.dev runtime
contract.
🤖 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/content/getArtistContentReadiness.ts`:
- Around line 122-129: The return currently sets missing to the full issues
array causing duplication; change the returned object in
getArtistContentReadiness (the block that builds artist_slug, ready, missing,
warnings) to set missing: requiredMissing instead of missing: issues so that
missing contains only items from requiredMissing while warnings remains the
recommended filter.

In `@lib/content/persistCreateContentRunVideo.ts`:
- Around line 64-113: Add a DB uniqueness guard on (owner_account_id,
storage_key) and make the create path resilient to races: ensure the files table
has a unique constraint/index on (owner_account_id, storage_key), then update
the createFileRecord flow (or persistCreateContentRunVideo where
selectFileByStorageKey, uploadFileByKey and createFileRecord are used) to handle
conflicts by catching the unique-violation error (or using an upsert/INSERT ...
ON CONFLICT DO NOTHING/DO UPDATE) and then re-selecting the existing row via
selectFileByStorageKey to return its id/signedUrl; this keeps upload+create
idempotent and prevents duplicate records when concurrent requests race.
- Around line 90-93: The code calls fetch(output.videoSourceUrl) without
validation — add explicit URL validation before fetching: in the
persistCreateContentRunVideo flow (the block using output.videoSourceUrl / the
const response = await fetch(...) call) construct a URL object from
output.videoSourceUrl, verify url.protocol === 'https:', and verify url.hostname
is in an allowlist (e.g., ALLOWED_VIDEO_HOSTS or a config-driven set); reject or
throw if validation fails. Also guard against hostnames that resolve to
localhost/private IPs (or use net.isIP + CIDR checks) and ensure only the
validated url.href is passed to fetch (no unvalidated redirects allowed). Ensure
the new allowlist constant and validation logic are used immediately before the
existing fetch call.
- Around line 47-133: persistCreateContentRunVideo is doing too much; split into
small helpers: (1) extract eligibility checks into a helper like
isEligibleForPersist(run) that returns the validated CreateContentOutput (moves
taskIdentifier/isCompleted and required fields checks), (2) extract
existing-file lookup into findExistingFile(ownerAccountId, storageKey) which
calls selectFileByStorageKey and createSignedFileUrlByKey and returns the mapped
video object if found, (3) extract download+upload+create-record flow into
downloadUploadCreateVideo(output, storageKey, fileName) which fetches
output.videoSourceUrl, validates response, reads blob, calls uploadFileByKey and
createFileRecord, then calls createSignedFileUrlByKey and returns the mapped
created video object, and (4) extract mapping into buildRunWithVideo(run,
output, videoObj) to return the updated run; then refactor
persistCreateContentRunVideo to orchestrate: call isEligibleForPersist, compute
fileName/storageKey, call findExistingFile and if not found call
downloadUploadCreateVideo, then return buildRunWithVideo. Reference functions:
persistCreateContentRunVideo, selectFileByStorageKey, createSignedFileUrlByKey,
uploadFileByKey, createFileRecord.

In `@lib/content/validateCreateContentBody.ts`:
- Line 22: Remove acceptance of account_id from the request schema and
auth-override logic: delete the account_id field from the Zod schema (the
property named account_id) and remove the code path that reads/uses
request-provided account_id in the validateCreateContentBody flow (the auth
override logic around account_id). Ensure account ID is obtained only from the
authentication context (the existing auth/account extraction used elsewhere in
validateCreateContentBody) and not from the request body or tool schema.

In `@lib/content/validateGetContentEstimateQuery.ts`:
- Around line 8-10: The schema fields lipsync and compare currently use
z.coerce.boolean(), which treats any non-empty string (e.g. "false") as true;
update these fields in validateGetContentEstimateQuery to explicitly parse
string query values by accepting only "true"/"false" and transforming to boolean
(e.g., replace z.coerce.boolean() for lipsync and compare with
z.enum(["true","false"]).transform(v => v === "true") or equivalent
z.string().pipe/transform logic) while keeping batch as the numeric coercion;
ensure the transformed fields default to false if absent.

In `@lib/supabase/files/selectFileByStorageKey.ts`:
- Around line 2-17: The function selectFileByStorageKey currently returns
Promise<FileRecord | null>; change its return type to use the DB table type
(Promise<Tables<"files"> | null>) to avoid drift with write-helper types—update
the function signature and any related imports/usages (remove or stop using
FileRecord in this file) so the function returns the Tables<"files"> shape
directly and callers are updated to expect that DB table type.

In `@lib/tasks/getTaskRunHandler.ts`:
- Line 27: The current use of Promise.all and direct await on
persistCreateContentRunVideo makes the whole GET fail if any hydration errors
occur; change hydration to be best-effort by catching per-run errors and falling
back to the original run payload: replace the Promise.all(runs.map(run =>
persistCreateContentRunVideo(run))) with a resilient pattern (either
Promise.allSettled over runs.map(...) and map settled results to either the
fulfilled value or the original run, or map runs to async functions that try {
return await persistCreateContentRunVideo(run) } catch (err) { return run }),
and similarly wrap the single await persistCreateContentRunVideo call in a
try/catch that returns the original run on error; keep function names
persistCreateContentRunVideo and variable hydratedRuns to locate the changes.

---

Nitpick comments:
In `@lib/content/contentTemplates.ts`:
- Around line 7-25: DEFAULT_CONTENT_TEMPLATE duplicates the literal
"artist-caption-bedroom"; change it to be derived from the source array to avoid
drift by referencing CONTENT_TEMPLATES so updates stay consistent. Replace the
hardcoded DEFAULT_CONTENT_TEMPLATE value with a computed value that reads
CONTENT_TEMPLATES (for example using CONTENT_TEMPLATES[0].name or a small helper
like getDefaultTemplateName()) after CONTENT_TEMPLATES is declared, ensuring the
symbol DEFAULT_CONTENT_TEMPLATE is exported and CONTENT_TEMPLATES remains the
single source of truth.

In `@lib/content/createContentHandler.ts`:
- Around line 47-49: Add explicit return type annotations to the trigger helper
functions to enforce compile-time safety: update triggerCreateContent to declare
its return type as Promise<TaskRunHandle> and annotate its payload parameter
with TriggerCreateContentPayload imported from `@trigger.dev/sdk`; do the same for
triggerSetupSandbox and triggerRunSandboxCommand (use the appropriate payload
types for each) so each function signature explicitly returns
Promise<TaskRunHandle> and prevents relying solely on the Trigger.dev runtime
contract.

In `@lib/content/getArtistContentReadiness.ts`:
- Around line 45-120: The getArtistContentReadiness function repeats nearly
identical file-check logic; refactor by extracting a declarative rules array
(each item: path, severity, fix, and a check predicate) and a small helper
(e.g., applyRule or checkRule) that runs the predicate using existing helpers
hasFile/hasAnyMp3 and pushes the issue to issues when false; replace the
repeated if-blocks with a single loop over rules to construct issues, keeping
getArtistContentReadiness concise and under 50 lines while reusing the hasFile
and hasAnyMp3 symbols.

In `@lib/content/validateGetContentEstimateQuery.ts`:
- Around line 7-13: Export the existing Zod schema constant so it’s available
alongside the inferred type: add an export for getContentEstimateQuerySchema
(the z.object(...) currently declared) so both getContentEstimateQuerySchema and
ValidatedGetContentEstimateQuery are exported; update the declaration of
getContentEstimateQuerySchema to be exported (export const
getContentEstimateQuerySchema = ...) to follow the validator convention and keep
the schema and type reusable.

In `@lib/content/validateGetContentValidateQuery.ts`:
- Around line 7-16: The schema is currently private and the output type is
hand-written; export the Zod schema and derive the TS type from it: export the
existing getContentValidateQuerySchema and replace the manual
ValidatedGetContentValidateQuery definition with export type
ValidatedGetContentValidateQuery = z.infer<typeof
getContentValidateQuerySchema>, ensuring the module exports both the schema and
the inferred type so callers can import the validator contract directly.

In `@lib/trigger/triggerCreateContent.ts`:
- Around line 4-9: Extract the overlapping shape into a single exported shared
type (e.g., ContentCreatePayload) instead of duplicating it: create the new
shared type matching the fields in TriggerCreateContentPayload and the content
run output used in persistCreateContentRunVideo.ts, replace
TriggerCreateContentPayload with the new shared type, and update all
imports/usages (including where persistCreateContentRunVideo references
account/template/lipsync) to import the shared type so both modules reference
the same contract and prevent divergence.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 09dae1ab-89c9-44e7-9746-8d9b3123337c

📥 Commits

Reviewing files that changed from the base of the PR and between 0d37cbc and b10017e.

⛔ Files ignored due to path filters (10)
  • AGENTS.md is excluded by none and included by none
  • lib/content/__tests__/createContentHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/__tests__/getArtistContentReadiness.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/__tests__/getContentEstimateHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/__tests__/getContentTemplatesHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/__tests__/getContentValidateHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/__tests__/persistCreateContentRunVideo.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/__tests__/validateCreateContentBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/tasks/__tests__/getTaskRunHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/trigger/__tests__/triggerCreateContent.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (19)
  • app/api/content/create/route.ts
  • app/api/content/estimate/route.ts
  • app/api/content/templates/route.ts
  • app/api/content/validate/route.ts
  • lib/const.ts
  • lib/content/contentTemplates.ts
  • lib/content/createContentHandler.ts
  • lib/content/getArtistContentReadiness.ts
  • lib/content/getContentEstimateHandler.ts
  • lib/content/getContentTemplatesHandler.ts
  • lib/content/getContentValidateHandler.ts
  • lib/content/persistCreateContentRunVideo.ts
  • lib/content/validateCreateContentBody.ts
  • lib/content/validateGetContentEstimateQuery.ts
  • lib/content/validateGetContentValidateQuery.ts
  • lib/supabase/files/selectFileByStorageKey.ts
  • lib/supabase/storage/createSignedFileUrlByKey.ts
  • lib/tasks/getTaskRunHandler.ts
  • lib/trigger/triggerCreateContent.ts

Comment on lines +122 to +129
const requiredMissing = issues.filter(item => item.severity === "required");
const warnings = issues.filter(item => item.severity === "recommended");

return {
artist_slug: artistSlug,
ready: requiredMissing.length === 0,
missing: issues,
warnings,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

missing should return only required issues.

Line 128 currently returns all issues (required + recommended) in missing, while warnings already returns recommended items. Return requiredMissing in missing to avoid duplicated/misleading output.

✅ Suggested fix
   return {
     artist_slug: artistSlug,
     ready: requiredMissing.length === 0,
-    missing: issues,
+    missing: requiredMissing,
     warnings,
   };
📝 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.

Suggested change
const requiredMissing = issues.filter(item => item.severity === "required");
const warnings = issues.filter(item => item.severity === "recommended");
return {
artist_slug: artistSlug,
ready: requiredMissing.length === 0,
missing: issues,
warnings,
const requiredMissing = issues.filter(item => item.severity === "required");
const warnings = issues.filter(item => item.severity === "recommended");
return {
artist_slug: artistSlug,
ready: requiredMissing.length === 0,
missing: requiredMissing,
warnings,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/getArtistContentReadiness.ts` around lines 122 - 129, The return
currently sets missing to the full issues array causing duplication; change the
returned object in getArtistContentReadiness (the block that builds artist_slug,
ready, missing, warnings) to set missing: requiredMissing instead of missing:
issues so that missing contains only items from requiredMissing while warnings
remains the recommended filter.

Comment on lines +47 to +133
export async function persistCreateContentRunVideo<T extends TriggerRunLike>(run: T): Promise<T> {
if (run.taskIdentifier !== CREATE_CONTENT_TASK_ID || !isCompleted(run)) {
return run;
}

const output = (run.output ?? {}) as CreateContentOutput;
if (!output.accountId || !output.artistSlug || !output.videoSourceUrl) {
return run;
}

if (output.video?.storageKey) {
return run;
}

const fileName = `${output.artistSlug}-${run.id}.mp4`;
const storageKey = `content/${output.accountId}/${output.artistSlug}/${fileName}`;

const existingFile = await selectFileByStorageKey({
ownerAccountId: output.accountId,
storageKey,
});

if (existingFile) {
const signedUrl = await createSignedFileUrlByKey({
key: existingFile.storage_key,
});

return {
...run,
output: {
...output,
video: {
fileId: existingFile.id,
storageKey: existingFile.storage_key,
fileName: existingFile.file_name,
mimeType: existingFile.mime_type,
sizeBytes: existingFile.size_bytes,
signedUrl,
},
},
};
}

const response = await fetch(output.videoSourceUrl);
if (!response.ok) {
throw new Error(`Failed to download rendered video: ${response.status} ${response.statusText}`);
}

const videoBlob = await response.blob();
const mimeType = response.headers.get("content-type") || "video/mp4";

await uploadFileByKey(storageKey, videoBlob, {
contentType: mimeType,
upsert: true,
});

const createdFile = await createFileRecord({
ownerAccountId: output.accountId,
// Phase 1: artist account mapping is not wired yet, so we scope to owner account.
artistAccountId: output.accountId,
storageKey,
fileName,
mimeType,
sizeBytes: videoBlob.size,
description: `Content pipeline output for ${output.artistSlug}`,
tags: ["content", "video", output.template ?? "unknown-template"],
});

const signedUrl = await createSignedFileUrlByKey({
key: createdFile.storage_key,
});

return {
...run,
output: {
...output,
video: {
fileId: createdFile.id,
storageKey: createdFile.storage_key,
fileName: createdFile.file_name,
mimeType: createdFile.mime_type,
sizeBytes: createdFile.size_bytes,
signedUrl,
},
},
};
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Split this into smaller helpers; current function exceeds SRP and size limits.

This method combines task filtering, remote I/O, storage, DB persistence, and output mapping. Extracting helpers will improve testability and reduce maintenance risk.

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/content/persistCreateContentRunVideo.ts` around lines 47 - 133,
persistCreateContentRunVideo is doing too much; split into small helpers: (1)
extract eligibility checks into a helper like isEligibleForPersist(run) that
returns the validated CreateContentOutput (moves taskIdentifier/isCompleted and
required fields checks), (2) extract existing-file lookup into
findExistingFile(ownerAccountId, storageKey) which calls selectFileByStorageKey
and createSignedFileUrlByKey and returns the mapped video object if found, (3)
extract download+upload+create-record flow into
downloadUploadCreateVideo(output, storageKey, fileName) which fetches
output.videoSourceUrl, validates response, reads blob, calls uploadFileByKey and
createFileRecord, then calls createSignedFileUrlByKey and returns the mapped
created video object, and (4) extract mapping into buildRunWithVideo(run,
output, videoObj) to return the updated run; then refactor
persistCreateContentRunVideo to orchestrate: call isEligibleForPersist, compute
fileName/storageKey, call findExistingFile and if not found call
downloadUploadCreateVideo, then return buildRunWithVideo. Reference functions:
persistCreateContentRunVideo, selectFileByStorageKey, createSignedFileUrlByKey,
uploadFileByKey, createFileRecord.

Comment on lines +64 to +113
const existingFile = await selectFileByStorageKey({
ownerAccountId: output.accountId,
storageKey,
});

if (existingFile) {
const signedUrl = await createSignedFileUrlByKey({
key: existingFile.storage_key,
});

return {
...run,
output: {
...output,
video: {
fileId: existingFile.id,
storageKey: existingFile.storage_key,
fileName: existingFile.file_name,
mimeType: existingFile.mime_type,
sizeBytes: existingFile.size_bytes,
signedUrl,
},
},
};
}

const response = await fetch(output.videoSourceUrl);
if (!response.ok) {
throw new Error(`Failed to download rendered video: ${response.status} ${response.statusText}`);
}

const videoBlob = await response.blob();
const mimeType = response.headers.get("content-type") || "video/mp4";

await uploadFileByKey(storageKey, videoBlob, {
contentType: mimeType,
upsert: true,
});

const createdFile = await createFileRecord({
ownerAccountId: output.accountId,
// Phase 1: artist account mapping is not wired yet, so we scope to owner account.
artistAccountId: output.accountId,
storageKey,
fileName,
mimeType,
sizeBytes: videoBlob.size,
description: `Content pipeline output for ${output.artistSlug}`,
tags: ["content", "video", output.template ?? "unknown-template"],
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The dedupe path is race-prone and can create duplicate file records.

The selectFileByStorageKey check followed by createFileRecord is non-atomic. Concurrent calls can both miss then insert. Add a DB uniqueness guard on (owner_account_id, storage_key) and handle conflict by selecting the existing row.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/persistCreateContentRunVideo.ts` around lines 64 - 113, Add a DB
uniqueness guard on (owner_account_id, storage_key) and make the create path
resilient to races: ensure the files table has a unique constraint/index on
(owner_account_id, storage_key), then update the createFileRecord flow (or
persistCreateContentRunVideo where selectFileByStorageKey, uploadFileByKey and
createFileRecord are used) to handle conflicts by catching the unique-violation
error (or using an upsert/INSERT ... ON CONFLICT DO NOTHING/DO UPDATE) and then
re-selecting the existing row via selectFileByStorageKey to return its
id/signedUrl; this keeps upload+create idempotent and prevents duplicate records
when concurrent requests race.

Comment on lines +90 to +93
const response = await fetch(output.videoSourceUrl);
if (!response.ok) {
throw new Error(`Failed to download rendered video: ${response.status} ${response.statusText}`);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Validate and constrain videoSourceUrl before fetching (SSRF risk).

fetch(output.videoSourceUrl) accepts untrusted URLs with no protocol/host allowlist checks. This can be abused to reach internal resources. Please enforce HTTPS + allowed hostnames before download.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/persistCreateContentRunVideo.ts` around lines 90 - 93, The code
calls fetch(output.videoSourceUrl) without validation — add explicit URL
validation before fetching: in the persistCreateContentRunVideo flow (the block
using output.videoSourceUrl / the const response = await fetch(...) call)
construct a URL object from output.videoSourceUrl, verify url.protocol ===
'https:', and verify url.hostname is in an allowlist (e.g., ALLOWED_VIDEO_HOSTS
or a config-driven set); reject or throw if validation fails. Also guard against
hostnames that resolve to localhost/private IPs (or use net.isIP + CIDR checks)
and ensure only the validated url.href is passed to fetch (no unvalidated
redirects allowed). Ensure the new allowlist constant and validation logic are
used immediately before the existing fetch call.

.default(DEFAULT_CONTENT_TEMPLATE)
.optional(),
lipsync: z.boolean().default(false).optional(),
account_id: z.string().uuid("account_id must be a valid UUID").optional(),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove account_id from the request contract and auth override flow.

Line 22 and Lines 55–57 allow request-body account_id overrides. This breaks the endpoint contract and should be derived solely from authentication context.

🔒 Suggested contract-safe change
 export const createContentBodySchema = z.object({
@@
-  account_id: z.string().uuid("account_id must be a valid UUID").optional(),
 });
@@
-  const authResult = await validateAuthContext(request, {
-    accountId: result.data.account_id,
-  });
+  const authResult = await validateAuthContext(request);

As per coding guidelines: Never use account_id in request bodies or tool schemas; always derive the account ID from authentication.

Also applies to: 55-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/validateCreateContentBody.ts` at line 22, Remove acceptance of
account_id from the request schema and auth-override logic: delete the
account_id field from the Zod schema (the property named account_id) and remove
the code path that reads/uses request-provided account_id in the
validateCreateContentBody flow (the auth override logic around account_id).
Ensure account ID is obtained only from the authentication context (the existing
auth/account extraction used elsewhere in validateCreateContentBody) and not
from the request body or tool schema.

Comment on lines +8 to +10
lipsync: z.coerce.boolean().default(false),
batch: z.coerce.number().int().min(1).max(100).default(1),
compare: z.coerce.boolean().default(false),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Zod v4 z.coerce.boolean() string input behavior documentation

💡 Result:

In Zod v4, z.coerce.boolean() coerces by calling JavaScript’s built-in Boolean(input). (zod.dev)

That means for string inputs:

If you want "true"/"false" (and similar) parsed logically to booleans, Zod v4 provides z.stringbool() (with configurable truthy/falsy sets). (zod.dev)

Citations:


z.coerce.boolean() will mis-handle "false" query values.

Query parameters arrive as strings. JavaScript's Boolean() function (which z.coerce.boolean() uses) treats any non-empty string—including "false", "0", "no"—as true. This means ?lipsync=false or ?compare=false will be parsed as true, causing unexpected behavior.

For query strings, explicitly parse string values first: use Zod's z.enum(["true", "false"]).transform(v => v === "true") or z.string().pipe(z.coerce.boolean()) with custom logic, rather than relying on direct coercion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/validateGetContentEstimateQuery.ts` around lines 8 - 10, The
schema fields lipsync and compare currently use z.coerce.boolean(), which treats
any non-empty string (e.g. "false") as true; update these fields in
validateGetContentEstimateQuery to explicitly parse string query values by
accepting only "true"/"false" and transforming to boolean (e.g., replace
z.coerce.boolean() for lipsync and compare with
z.enum(["true","false"]).transform(v => v === "true") or equivalent
z.string().pipe/transform logic) while keeping batch as the numeric coercion;
ensure the transformed fields default to false if absent.

Comment on lines +2 to +17
import type { FileRecord } from "@/lib/supabase/files/createFileRecord";

/**
* Select a file record by storage key for an owner account.
*
* @param root0
* @param root0.ownerAccountId
* @param root0.storageKey
*/
export async function selectFileByStorageKey({
ownerAccountId,
storageKey,
}: {
ownerAccountId: string;
storageKey: string;
}): Promise<FileRecord | null> {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use Tables<"files"> for the return type instead of FileRecord.

This query reads DB rows directly; returning FileRecord (from write helper types) risks schema drift and nullability mismatches over time.

As per coding guidelines, lib/supabase/**/*.ts: “Return typed results using Tables<"table_name">”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/supabase/files/selectFileByStorageKey.ts` around lines 2 - 17, The
function selectFileByStorageKey currently returns Promise<FileRecord | null>;
change its return type to use the DB table type (Promise<Tables<"files"> |
null>) to avoid drift with write-helper types—update the function signature and
any related imports/usages (remove or stop using FileRecord in this file) so the
function returns the Tables<"files"> shape directly and callers are updated to
expect that DB table type.

try {
if (validatedQuery.mode === "list") {
const runs = await listTaskRuns(validatedQuery.accountId, validatedQuery.limit);
const hydratedRuns = await Promise.all(runs.map(run => persistCreateContentRunVideo(run)));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hydration failures currently fail the whole GET response.

Line 27 (Promise.all) and Line 43 (await persistCreateContentRunVideo) make response success dependent on hydration success. A single hydration error will return 500 even when run retrieval succeeded. Make hydration best-effort and fall back to the original run payload.

💡 Suggested resilient fallback
-      const hydratedRuns = await Promise.all(runs.map(run => persistCreateContentRunVideo(run)));
+      const hydratedRuns = await Promise.all(
+        runs.map(async (run) => {
+          try {
+            return await persistCreateContentRunVideo(run);
+          } catch {
+            return run;
+          }
+        }),
+      );
@@
-    const hydratedRun = await persistCreateContentRunVideo(result);
+    let hydratedRun = result;
+    try {
+      hydratedRun = await persistCreateContentRunVideo(result);
+    } catch {
+      // Best-effort hydration; return the retrieved run when hydration fails.
+    }

Also applies to: 43-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/tasks/getTaskRunHandler.ts` at line 27, The current use of Promise.all
and direct await on persistCreateContentRunVideo makes the whole GET fail if any
hydration errors occur; change hydration to be best-effort by catching per-run
errors and falling back to the original run payload: replace the
Promise.all(runs.map(run => persistCreateContentRunVideo(run))) with a resilient
pattern (either Promise.allSettled over runs.map(...) and map settled results to
either the fulfilled value or the original run, or map runs to async functions
that try { return await persistCreateContentRunVideo(run) } catch (err) { return
run }), and similarly wrap the single await persistCreateContentRunVideo call in
a try/catch that returns the original run on error; keep function names
persistCreateContentRunVideo and variable hydratedRuns to locate the changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant