Skip to content

feat(creative): add stateful display and coordinated placement formats - #6279

Open
bokelley wants to merge 5 commits into
mainfrom
check-issue-5972-status
Open

feat(creative): add stateful display and coordinated placement formats#6279
bokelley wants to merge 5 commits into
mainfrom
check-issue-5972-status

Conversation

@bokelley

@bokelley bokelley commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the experimental AdCP 3.2 premium display canonicals: seller_rendered_stateful_display and coordinated_placements. Closes #5972.

Design note for reviewers: after the original snapshot-based design was red-teamed against ~35 published specs (IAB NAP/CTV primary docs, Kargo, Playwire, Adnami, GumGum, Teads, NYT, Guardian, Concert, Bloomberg, plus CTV platform specs), the canonicals were reshaped around an executable template contract. Full evidence and rationale: WG memo on #5972. The companion CTV RFC was rewritten accordingly: #6428.

What the contract is

The seller's format declaration — slots with limits, safe zones, breakpoints, and a disclosed state/transition graph — is the shared truth. supply_mode selects who renders:

  • components (default): buyer supplies logo/copy/imagery (with focal_point crop hints)/video; seller assembles every state × breakpoint deterministically. Matches how NYT/Concert/Bloomberg-class sellers ingest today, but makes the template machine-readable so assembly needs no studio loop.
  • rendered_canvases: buyer authors externally against the published contract and supplies the full canvas matrix; seller validates mechanically and brand-reviews.
  • layered_source: transitional for sellers without executable templates (PSD/fonts in, production_window_business_days applies).

Sellers offering the canonical MUST support deterministic preview_creative of every state × breakpoint — the proof round becomes an API round-trip.

Coverage additions from the red team

  • Fluid sizing: width_range, width_mode: full_bleed | gutter_residual, viewport_height_percent, canvas_aspect_ratio (skins, topscrolls, adhesion, IAB flexible units)
  • Single-state reveal units: reveal: clip_window | scroll_parallax + underlay anchoring (interscrollers/understitials — no more fabricated second states)
  • Triggers/inputs: in_view_timer, media_event (video_start/complete), scroll direction, hover
  • Click model: clickthrough: required | optional | none + per-state state_click_urls (single URL remains the default)
  • coordinated_placements: sequence (sequential messaging), per-component serving_policy

Enforcement hardening (red-team fixes)

  • Per-canonical slot asset-type whitelist that params.slots cannot widen — closes buyer-script smuggling (javascript/html/webhook slots) on any canonical
  • Dismissibility floor: overlay/fullscreen anchoring requires dismissible or a close affordance
  • Anti-strobe floor: timer cycles require delay_ms >= 1000
  • scroll_linked pinned to scroll_progress in every transition branch
  • placement_ref publisher-domain normalization (no double-supply via spelling variants)
  • Referenced sibling options schema-validated like inline params; unknown scope values are explicit violations
  • IAB NAP/LEAN-prohibited combinations (auto-expansion into overlay, hover expansion, scrolling overlays) validate but emit non-blocking warnings[] on validate_input results

Test coverage

  • Schema suites, canonical fixtures (22), examples, compliance snippets, doc-drift: green
  • server/tests/unit/training-agent.test.ts: 636 passing incl. 12 new adversarial cases (js smuggling, undismissable strobe, fluid-canvas supply mismatch, sequence gaps, domain-alias double-supply)
  • Compliance storyboard v2.0.0: positive vectors per supply mode + single-state reveal, 8 graded negative vectors, each citing the verifier_constraints key it exercises
  • Worked examples: component-fed flex frame, single-state topscroll, buyer-rendered takeover, sequenced coordinated takeover

🤖 Generated with Claude Code

@bokelley
bokelley requested a review from pkras August 8, 2026 07:56
@bokelley
bokelley marked this pull request as ready for review August 8, 2026 07:58
@bokelley bokelley changed the title [codex] Add premium display canonicals for AdCP 3.2 feat(creative): add premium display canonicals for AdCP 3.2 Aug 8, 2026
@bokelley bokelley closed this Aug 8, 2026
@bokelley bokelley reopened this Aug 8, 2026
Comment thread server/src/training-agent/task-handlers.ts Outdated

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ladon verdict: Escalate to human review

Escalate — gated-path change requires human/CODEOWNERS approval.

This PR modifies and adds files under static/schemas/source/**, which is a deterministic hard approval gate (gated_paths: true). The current review_decision is REVIEW_REQUIRED, not APPROVED, so the gate is not satisfied. Per decision-table row 2, the outcome must be escalate.

No critical/high findings were reported. The reviewer confirms schema↔docs coherence holds across canonical-format-kind, product-format-declaration, creative-manifest/asset, _base, and the two docs pages; the minor changeset matches the additive/experimental surface; the new page_takeover oneOf carries a format_kind discriminator; and no dist artifacts were touched. One Medium robustness gap and one Low remain.

Medium findings

  • server/src/training-agent/task-handlers.ts:2859 — New format_options.forEach in collectCanonicalFormatAdvisories runs before the Array.isArray guard (potential runtime error on non-array input in the get_products advisory path).

Escalation reasons

  • Touches gated paths under static/schemas/source/** (multiple modified files plus new canvas-constraint.json, creative-assets.json, multi_state_display.json, page_takeover.json) while review_decision is REVIEW_REQUIRED. Human/CODEOWNERS approval is required before merge.

Medium findings

  • server/src/training-agent/task-handlers.ts:2859 — New format_options.forEach in collectCanonicalFormatAdvisories runs before the Array.isArray guard

Why human review

  • Touches gated paths under static/schemas/source/** (schema source of truth) while review_decision is REVIEW_REQUIRED — human/CODEOWNERS approval required before merge.
  • Medium robustness gap: server/src/training-agent/task-handlers.ts:2859 — format_options.forEach runs before the Array.isArray guard on the get_products advisory path.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/asset-group-vocabulary.json (modified) matches static/schemas/source/**; static/schemas/source/core/assets/image-asset.json (modified) matches static/schemas/source/**; static/schemas/source/core/canonical-format-kind.json (modified) matches static/schemas/source/**; static/schemas/source/core/canvas-constraint.json (added) matches static/schemas/source/**; static/schemas/source/core/creative-asset.json (modified) matches static/schemas/source/**; static/schemas/source/core/creative-assets.json (added) matches static/schemas/source/**; static/schemas/source/core/creative-manifest.json (modified) matches static/schemas/source/**; static/schemas/source/core/format-shape-vocabulary.json (modified) matches static/schemas/source/**; static/schemas/source/core/product-format-declaration.json (modified) matches static/schemas/source/**; static/schemas/source/creative/list-creatives-response.json (modified) matches static/schemas/source/**; static/schemas/source/enums/error-code.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/_base.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/multi_state_display.json (added) matches static/schemas/source/**; static/schemas/source/formats/canonical/page_takeover.json (added) matches static/schemas/source/**) and the current GitHub review decision is 'REVIEW_REQUIRED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@aao-secretariat aao-secretariat Bot added the ladon/needs-human-review Ladon has escalated this PR for human review. label Aug 8, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ladon verdict: Escalate to human review

Escalate — gated schema-source paths require human/CODEOWNERS approval.

This PR modifies and adds files under static/schemas/source/**, which are under a hard, non-overridable approval gate (gated_paths: true). The current review_decision is not APPROVED (unknown/none), so decision rule row 2 fires: the gate is not satisfied and Ladon cannot auto-approve regardless of how clean the diff is.

The developer resolved the single prior Medium finding: collectCanonicalFormatAdvisories now guards if (!Array.isArray(product.format_options)) continue; before the FORMAT_SHAPE_PROMOTED forEach (task-handlers.ts:486-487), with the format_ids guard split out, plus a direct regression test seeding a non-array format_options. That is a net improvement — no current blocking or medium findings remain.

The only remaining reason this PR is not approvable is the gated-paths approval gate. Once a human/CODEOWNERS approval is recorded (review_decision: APPROVED), a re-run with no findings would fall through to approve.

Escalation reasons

  • Touches gated paths under static/schemas/source/** (schema source of truth, including added/modified core, enum, and canonical format schemas) while review_decision is not APPROVED — human/CODEOWNERS approval required before merge.

Why human review

  • Touches gated paths under static/schemas/source/** (schema source of truth) while review_decision is not APPROVED — human/CODEOWNERS approval required before merge.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/asset-group-vocabulary.json (modified) matches static/schemas/source/**; static/schemas/source/core/assets/image-asset.json (modified) matches static/schemas/source/**; static/schemas/source/core/canonical-format-kind.json (modified) matches static/schemas/source/**; static/schemas/source/core/canvas-constraint.json (added) matches static/schemas/source/**; static/schemas/source/core/creative-asset.json (modified) matches static/schemas/source/**; static/schemas/source/core/creative-assets.json (added) matches static/schemas/source/**; static/schemas/source/core/creative-manifest.json (modified) matches static/schemas/source/**; static/schemas/source/core/format-shape-vocabulary.json (modified) matches static/schemas/source/**; static/schemas/source/core/product-format-declaration.json (modified) matches static/schemas/source/**; static/schemas/source/creative/list-creatives-response.json (modified) matches static/schemas/source/**; static/schemas/source/enums/error-code.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/_base.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/multi_state_display.json (added) matches static/schemas/source/**; static/schemas/source/formats/canonical/page_takeover.json (added) matches static/schemas/source/**) and the current GitHub review decision is 'unknown', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@bokelley bokelley changed the title feat(creative): add premium display canonicals for AdCP 3.2 feat(creative): add stateful display and coordinated placement formats Aug 12, 2026
@aao-secretariat

Copy link
Copy Markdown
Contributor

Ladon cannot review this PR until merge conflicts are resolved.

@pkras

pkras commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Went through seller_rendered_stateful_display.json — few little things, nothing that blocks:

1. Nothing stops two transitions fighting over the same state. Each transition is locked to one trigger, love that. But you could still declare two arrows out of collapsed — say two timers with the same delay pointing at different states — and there's nothing saying which one wins. The verifier checks reachability, unique IDs, range ordering… but not "hey, are this state's exits actually unambiguous." Do we wanna reject that, or is it on purpose and the seller's runtime just sorts it out?

2. duration_ms_range never checks that min < max. Every other range has an ordering rule (height_range_order, scroll_progress_bounds) but this one doesn't — so [5000, 1000] sails right through. Guessing that's just a miss?

3. That discriminator on transitions doesn't actually do anything. It's an OpenAPI thing, not draft-07, so Ajv just skips it — the oneOf + const right underneath is doing all the real work. Not a bug, just a bit of a head-scratcher for whoever reads it next. Wire it up or yeet it, either way.

@bokelley

Copy link
Copy Markdown
Contributor Author

Fixed 2 and 3 in the follow-up commit.

1. Ambiguous exits — by design. The schema's contract is graph disclosure: declared states are valid, all endpoints resolve, every non-initial state is reachable. Runtime arbitration among competing exits is seller territory. Two timer arrows from collapsed at the same delay is a valid disclosed graph; the seller's runtime picks the winner. We'd add transition_exit_uniqueness (at most one exit per from_state_id × trigger bucket) only if implementation evidence shows sellers need the guarantee for preview/compliance fidelity. Not there yet.

2. duration_ms_range_order — was a miss; added to verifier_constraints alongside height_range_order. [5000, 1000] will now be rejected.

3. discriminator — removed. Ajv silently ignores it in draft-07; the oneOf + const was already doing all the real work. No behavior change.


Generated by Claude Code

@pkras

pkras commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Kept going into coordinated_placements.json + one cross-cutting thing. Again, nothing blocking:

4. The discriminator cleanup missed this file. You pulled the noop discriminator out of seller_rendered_stateful_display.json in the follow-up, but the identical one is still sitting on the inline-component branch here (discriminator: {propertyName: "format_kind"}, ~line 62). Same no-op — the oneOf + const underneath does the real work. Just for consistency.

5. The inline format menu is a hardcoded list that'll drift. The allowed inline format_kinds are spelled out as 13 fixed oneOf entries. There's already a master canonical-format list elsewhere, and nothing keeps the two in sync — so whenever someone adds a new canonical down the line, they have to remember to also add it here or it just silently can't be used inside a bundle. Worth deriving from the master list instead of copy-pasting? Not a bug, just future foot-gun.

6. Small heads-up, not a change request: nesting/custom is blocked structurally on the inline path (bad formats aren't in the menu), but on the format_option_ref path it's the runtime verifier (component_kind) doing the blocking since the schema can't follow the ref. Totally fine, just flagging where the guardrail actually lives.

7. Cross-cutting device question. Canvases are breakpoint-scoped, but transitions, anchoring, and initial_state_id aren't — they're shared across every device. So "scroll on mobile, tap on desktop" either means declaring every trigger live everywhere (and leaning on the seller runtime to pick the right one per device — same shape as your answer on #1), or splitting into two creatives once the experience actually diverges (different destination state, different starting state, or different anchoring per device). Is "one behavior graph, art just resizes" the intended model, or should a single creative be able to branch behavior by breakpoint? Genuine question, not a bug.

@bokelley

Copy link
Copy Markdown
Contributor Author

Fixed 4 in a follow-up commit. On 5, 6, and 7:

4. discriminator in coordinated_placements.json — removed. Same noop as in seller_rendered_stateful_display.json; Ajv silently ignores it in draft-07. The oneOf + const was already doing all the real work.

5. Hardcoded inline format menu — the enumeration is intentional rather than derived: draft-07 has no mechanism to project a sublist dynamically from the canonical registry, and the inline path needs to explicitly gate which formats can appear inside a bundle (bundles can't embed bundles; custom formats can't be inline). The drift risk @pkras names is real. The right mitigation is a build-time check that flags when a new canonical/*.json file appears without a corresponding entry here — worth a follow-up issue rather than a schema change now.

6. Guardrail asymmetry — correctly diagnosed, no action needed. The schema can prove format_kind is in the menu on the inline path; it can't follow a ref across products on the format_option_ref path. component_kind in verifier_constraints is the correct load-bearing guardrail for the ref path. Good to have this documented in the thread for anyone reading the schema later.

7. Device behavior model — "one behavior graph, art just resizes" is the intended model for 3.2. Canvases are breakpoint-scoped so artwork adapts, but transitions, anchoring, and initial_state_id are shared — the seller runtime applies the relevant triggers per device context, same as the ambiguous-exits answer in #1. "Scroll on mobile, tap on desktop" is a valid declared graph: both triggers live in it; the runtime fires the one appropriate to the device. Genuine behavioral divergence — different destination state, different initial state, or device-specific anchoring — is the boundary for two separate creatives (separate seller_rendered_stateful_display instances, or separate coordinated_placements components targeting device-scoped placements). No spec change needed for 3.2; worth a sentence in the format docs if it's not already covered.


Generated by Claude Code

@bokelley

Copy link
Copy Markdown
Contributor Author

@pkras heads up before you review: this PR was substantially reshaped in be83f786b5 after a red-team of the original design against ~35 published specs (evidence: WG memo on #5972). The core change: the format declaration is now an executable template contract with supply_mode selecting who renders (components / rendered_canvases / layered_source), plus fluid sizing, single-state reveal units, and enforcement hardening (slot asset-type whitelist, dismissibility and anti-strobe floors). The PR description reflects the new design. If you'd started reviewing the earlier snapshot-based shape, the schema files are the right place to restart — the two decisions that most deserve scrutiny are the two-supply-ends model and the preview-required rule.

@bokelley
bokelley force-pushed the check-issue-5972-status branch from 59c1362 to 1e5f711 Compare August 15, 2026 21:20
@bokelley
bokelley enabled auto-merge (squash) August 15, 2026 21:20
@bokelley
bokelley disabled auto-merge August 16, 2026 05:09
bokelley and others added 3 commits August 16, 2026 15:58
…ext snapshot

Merge daily-budget-caps and synthetic-depictions from main; combine the
test:schemas chains (premium-display + synthetic-depiction + geo) and
re-pin the standalone context-bytes snapshot for the merged tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…acros), re-pin snapshot

Combine the test:schemas chains and re-pin the standalone context-bytes
snapshot for the merged tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-pin snapshot

Combine the test:schemas chains and re-pin the standalone context-bytes
snapshot for the merged tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ladon/needs-human-review Ladon has escalated this PR for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(creative): add multi_state_display and page_takeover canonical formats (premium web display)

2 participants