Register the five-second streaming pre-roll format (work package A) - #272
Merged
Merged
Conversation
Schema and format registration for Crawlproof video pre-roll ads. No
rendering, selection or serving — those are packages B and D. The exit
condition this carries is the narrow one from the spec: existing formats
still work, and an unrendered video cannot serve.
Registering a media format in AD_FORMATS is not enough on its own, and is
actively unsafe: creativesFromCopy() fanned out over the whole registry
into one common design object, so the format would have minted a
video_preroll_5s creative carrying a headline and a palette, marked ready
beside its siblings, and handed an HTML renderer something it would draw
as a banner. So the fan-out is now DESIGN_FORMAT_IDS — the complement of
the new STREAMING_FORMAT_IDS — and the dashboard preview strips and format
editors iterate DESIGN_FORMATS for the same reason. They skip formats with
no creative row today, which hides the problem only until the render
pipeline lands.
Three places now refuse video rather than relying on configuration:
* fitAdFormat(), the single gate in front of serveAd(), refuses any
streaming format outright — so the refusal holds even for a slot that
has somehow been given one.
* cleanCreative()'s allowlist is the design formats, so a crafted
updateCreatives payload cannot write a medialess video creative. The
DB would have refused the write anyway; this makes it a clean
rejection rather than a constraint violation.
* ad_creatives.published_revision is null until ffprobe has validated an
encode, so a video creative is unservable by construction rather than
by remembering to check.
The migration deliberately breaks from the terminal_ascii and feed_item
precedents: it widens the creative CHECK but adds nothing to slot
inventory and backfills no creatives. A backfill can clone a headline; it
cannot clone a 150-frame H.264 encode.
Adds ad_video_jobs, ad_video_assets, ad_streaming_properties,
ad_video_decisions and ad_video_events. The decision table's unique
(property, session, placement) key is the whole per-session pre-roll rule:
a reconnect, a playlist reload or a retry returns the row that exists
instead of stacking a second ad.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan48 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 31 | LOW: 15
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
marked this pull request as ready for review
September 24, 2026 12:16
This was referenced Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work package A of the Crawlproof five-second streaming ads spec: schema and format registration only. Nothing here renders, selects or serves an MP4 — those are packages B and D. Draft because the migration is unapplied and B–F remain.
The spec's exit condition for A: existing formats still work; unrendered video cannot serve.
Why registering the format wasn't enough on its own
creativesFromCopy()fanned out over the whole registry into one common design object. Addingvideo_preroll_5stoAD_FORMATSalone would have minted a video creative carrying a headline and a palette, markedreadybeside its siblings, and handed an HTML renderer something it would draw as a banner — the one outcome a pre-roll must never have.So the fan-out is now
DESIGN_FORMAT_IDS, the complement of the newSTREAMING_FORMAT_IDS. The dashboard preview strips and format editors iterateDESIGN_FORMATSfor the same reason: they skip formats with no creative row today, which hides the problem only until the render pipeline lands.Three refusals that don't depend on configuration
fitAdFormat()serveAd()refuses any streaming format outright — so it holds even for a slot that has somehow been given one.cleanCreative()updateCreativespayload can't write a medialess video creative. The DB would refuse it anyway; this makes it a clean rejection rather than a constraint violation.ad_creatives.published_revisionThe migration breaks from precedent on purpose
terminal_asciiandfeed_itemeach widened the creative CHECK, added themselves to every slot's inventory, and backfilled a creative per campaign. Steps 2 and 3 are wrong for a media format:serveAd(), which renders HTML and ASCII. Adding the format there would offer a video to the display path.readywith no bytes behind them.New tables:
ad_video_jobs,ad_video_assets,ad_streaming_properties,ad_video_decisions,ad_video_events. The decision table's unique(property, session, placement)key is the per-session pre-roll rule — a reconnect, playlist reload or retry returns the row that exists instead of stacking a second ad.Verification
npx tsc --noEmitclean.tests/contract/tracker-geo.test.ts, pre-existing and environmental (node_modules/@ip-location-db/geolite2-city-mmdbisn't installed, solookupGeoreturns undefined). Untouched by this diff.npm run lintis broken repo-wide (next lintwas removed in Next 16), unrelated to this change.tests/ads-video-format.test.tspins the exit condition;tests/ads-template-copy.test.tsupdated where it asserted the fan-out equalled the whole registry.Not done here
The migration is not applied. Per the repo's own standing note prod history diverged, so it needs
psqlover the pooler, notsupabase db push. The spec authorizes no database deployment itself. Migration was timestamped20260924130000to sit after master's existing20260924120000_email_tracking.Packages B–F (renderer, dashboard workflow, decisions/accounting, surface integration, rollout inventory) are untouched.