Instead of letting mid-build surprises vanish into the scrollback, ask the agent to log every deviation from the plan as it happens — so the surprises become inputs to your next attempt.
“Keep an implementation-notes file as you build the export feature. If you hit an edge case that forces you to deviate from the plan, pick the conservative option, log it under ‘Deviations’, and keep going.”
Added ExportJob in apps/api/src/exports/exportJob.ts, enqueued on the existing BullMQ media-tasks queue with a dedicated export job name. Migration 0121_export_jobs matches the plan's schema exactly (status enum, requested_by, review_id, format).
POST /api/v1/reviews/:id/export endpointRequest validation reuses the zod schemas in apps/api/src/validation/ as the plan specified. format accepts mp4-burnin | csv | bundle. Returns 202 with a job id; polling endpoint deferred to step 6 per plan.
Review.duration_ms is denormalized and occasionally staleThree fixture reviews have duration_ms that disagrees with the source asset's probe data (looks like re-uploads never refresh it). Export reads duration from MediaAsset.probe.duration instead — same approach playbackManifest.ts already takes. No plan change, just noting the trap for anyone who touches this later.
frame_ts; burn-in renderer can sort and place them all on the timeline.annotations predate migration 0087 and have frame_ts = NULL — they were general review comments before per-frame annotations existed.frame_ts left blank and a legacy_comment flag. Nothing is silently dropped from the export bundle.created_at offset against the review session — decide whether that's honest enough to show in the video.drawtext/drawbox, as plannedFiltergraph builder in apps/worker/src/exports/burnin.ts. The plan's per-annotation overlay windows (enable='between(t,a,b)') work; verified against the 40-annotation stress fixture — render time 1.4x realtime on the worker instance class, inside the plan's 2x budget.
Date objects as stringsExportJobPayload.requestedAt: Date, passed through to the worker and compared against the review's updated_at to detect stale exports.Date. The staleness comparison silently coerced and passed in unit tests, but failed on a redelivered job in the integration run.requestedAt: string ISO-8601), parse once at the worker boundary in exportConsumer.ts. No custom serializer.media-tasks payloads likely have the same latent bug — a shared zod codec for job payloads would fix the class of problem.archiver dependencyarchiver to build the export bundle (video + CSV + thumbnails) as a zip stream to S3.packages/media/src/zipStream.ts already does streaming zip with backpressure handling — it powers the bulk asset download feature and has been in production since March.zipStream.ts as-is. One fewer dependency, and bundle exports inherit the same tested backpressure behavior as bulk downloads.zipStream lives in packages/media but is now used by two features — worth promoting to packages/shared in a cleanup pass.The transcode pipeline emits media:progress:{assetId} events. Followed the same convention — exports emit export:progress:{jobId} — so the frontend can reuse useProgressChannel() from apps/web/src/hooks/ instead of the polling UI the plan sketched. Kept the polling endpoint too, as the fallback.
guest_reviewer role grants review access with can_download_assets = false — clients use it to let external stakeholders comment without taking the media offsite. A review-ACL-only check would let guests exfiltrate source video via export.403 for guest_reviewer across all formats, mirroring the existing asset-download check in assetAccess.ts. Strictest defensible reading.Current behavior (from Deviation 4): guests get 403 on every export format. If you want guests to export the annotation CSV without media, it's a ~10-line change in exportPolicy.ts plus one test — but it changes what “can't download assets” means to customers, so it should be your call. Blocking neither merge nor QA.
The plan didn't specify a TTL for generated bundles. I put them under the exports/ S3 prefix with a 7-day lifecycle rule, matching share-link expiry — conservative, and nothing customer-visible promises longer. But if contracts or the data-retention policy say otherwise, the lifecycle rule in infra/s3.tf is the one line to change.
Three lines to paste into the next plan or prompt, so the next run doesn't rediscover today's surprises.
frame_ts. Decide interpolate-vs-sidecar before implementation starts, not at 14:41 mid-build.Date — and a shared zod codec for media-tasks payloads would retire the whole bug class.zipStream.ts and useProgressChannel() both existed and both beat the plan's version. Add a “search for prior art” step to the plan template, and settle the guest-reviewer export policy before the next feature touches ACLs.