Skip to content

feat(creative): add CTV experience profiles for AdCP 3.2 - #6591

Open
bokelley wants to merge 5 commits into
mainfrom
spec-6428-ctv-experience-profiles
Open

feat(creative): add CTV experience profiles for AdCP 3.2#6591
bokelley wants to merge 5 commits into
mainfrom
spec-6428-ctv-experience-profiles

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Implements RFC #6428: the six IAB CTV Ad Portfolio experiences (menu, pause, screensaver, overlay, squeezeback, in_scene) become expressible in AdCP 3.2 through an experience × contract matrix on existing canonicals — no channel-named ctv_* formats.

The matrix is deliberately plural because published platform specs show sellers split on ingestion (red-team evidence: WG memo; no fetched platform ingests NonLinear VAST for these placements today — Disney's spec explicitly rejects VAST, Netflix pause is image-only, Fire TV/Samsung/Google TV menu heroes are video-mandatory):

Experience Permitted canonicals
menu native_in_feed (menu profile, video-capable), sponsored_placement (catalog tiles)
pause image (+ copy), video_vast (nonlinear)
screensaver image, video_hosted, video_vast (nonlinear)
overlay native_in_feed (asset bundle), video_vast (nonlinear)
squeezeback video_vast (nonlinear), sponsored_placement
in_scene video_vast (nonlinear), sponsored_placement

What's in it

  • Shared vocabularies: ctv_ad_experience, motion_level (AdCOM attrs 21–23), activation_method (QR / deep link / push / email / tune-in / SMS — engagement events, never impressions)
  • native_in_feed menu profile: Native 1.2 video slot (vasttag), menu_placement (tile / headline_banner → plcmttype 1/3), focus_behavior (none / autoplay_muted / autoplay_sound)
  • video_vast: creative_type (linear | nonlinear | either) superseding linear_required; experiences require nonlinear; per-experience floors from the IAB portfolio guidelines — overlay/squeezeback ≥ 10s, in_scene ≥ 3s + interactivity forbidden, pause unfloored; SIMID explicit-never-inferred
  • Semantic validator (server/src/training-agent/ctv-experience-matrix.ts) enforcing every schema verifier_constraints entry, incl. matrix rejections, duration floors, linear_required contradiction, focus/video pairing
  • Docs: docs/creative/ctv-experiences.mdx with the OpenRTB/AdCOM signaling annex (plcmt 5–9, playbackmethod 8–11, motion attrs, Native plcmttype, VAST Extensions round-tripping). VAST 4.4 stays out of the version enum per the documented gate in vast-version.json (IAB XSD still DRAFT); the nonlinear pattern is valid under 4.2 serialization
  • Examples: StreamHaus menu tile (focus-autoplay video + QR), menu banner, pause image (the Netflix/Hulu-class contract), overlay VAST with push-notification activation copy
  • Compliance: ctv-experience-validate-input.yaml — 6 positive matrix pairings, 7 graded negatives (menu+VAST, pause+native, 5s overlay, linear overlay, interactive in_scene, focus without video, full_motion on image)

Test results

  • 657 server unit tests (21 new CTV cases, incl. the market-critical pause + image → pass)
  • Schema, examples, canonical fixtures, compliance lint/snippet/drift, oneOf-discriminator, enum-drift, projection (24/24), analysis (6/6), typecheck: all green
  • Model-context budget: 379,583 bytes (17.7 KiB headroom)

Notes for review

🤖 Generated with Claude Code

Implements RFC #6428: the six IAB CTV Ad Portfolio experiences map onto
existing canonicals through an experience x contract matrix instead of
channel-named formats. Red-teaming published platform specs (Fire TV,
Samsung, Roku, Disney, Netflix, Google TV) showed no platform ingests
NonLinear VAST for these placements today, so the matrix is deliberately
plural: menu pairs with native_in_feed (new video slot, menu_placement,
focus_behavior) and sponsored_placement (catalog tiles); pause and
screensaver pair with image and video contracts; overlay pairs with a
native asset bundle or nonlinear VAST; squeezeback and in_scene are
seller-composited or VAST.

- Shared vocabularies: ctv_ad_experience, motion_level (AdCOM attrs
  21-23), activation_method (activations are engagement events, never
  impressions)
- video_vast: creative_type (linear|nonlinear|either) supersedes
  linear_required; per-experience floors (overlay/squeezeback >= 10s,
  in_scene >= 3s + no interactivity, pause unfloored)
- Semantic matrix validator (ctv-experience-matrix.ts) enforcing every
  verifier constraint; 21 new unit tests incl. pause+image pass
- OpenRTB/AdCOM signaling annex documents plcmt 5-9, playbackmethod
  8-11, motion attrs, Native plcmttype 1/3; VAST 4.4 stays out of the
  enum per the documented IAB-publication gate
- Worked menu/pause/overlay examples and a graded compliance storyboard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread server/src/training-agent/ctv-experience-matrix.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 schema paths require human/CODEOWNERS review (review_decision: REVIEW_REQUIRED).

This PR modifies static/schemas/source/** (8 files: 3 new enums, 5 modified canonical formats), which is under the repo's deterministic hard approval gate (gated_paths: true). review_decision is REVIEW_REQUIRED, not APPROVED, so per decision-table row 2 the gate is not satisfied and the outcome must be escalate. No critical/high findings were reported, so this is not request-changes.

Medium findings

  • server/src/training-agent/ctv-experience-matrix.ts:245 — ctv_activation_copy_slots hard-fails spec-valid products with no schema basis, contradicting the docs' landing_page_url fallback. A human reviewer should confirm the intended validation semantics before merge.

Escalation reasons

  • Touches gated files under static/schemas/source/** (activation-method.json, ctv-ad-experience.json, motion-level.json plus modified image/native_in_feed/sponsored_placement/video_hosted/video_vast canonical formats); requires human/CODEOWNERS approval — review_decision is currently REVIEW_REQUIRED.

Note: reviewer confirmed schema↔docs↔TS↔storyboard coherence, a correct additive minor changeset, and no oneOf regression — but the human gate on schema-source changes still stands.

Medium findings

  • server/src/training-agent/ctv-experience-matrix.ts:245 — ctv_activation_copy_slots hard-fails spec-valid products with no schema basis

Why human review

  • Modifies gated schema-source files under static/schemas/source/** (3 new enums + 5 modified canonical formats); review_decision is REVIEW_REQUIRED, so the hard approval gate requires human/CODEOWNERS review before merge.
  • One medium finding (ctv-experience-matrix.ts:245): runtime invents a hard ctv_activation_copy_slots failure with no schema basis that contradicts the docs' landing_page_url fallback — worth a human eye on validation semantics.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/enums/activation-method.json (added) matches static/schemas/source/**; static/schemas/source/enums/ctv-ad-experience.json (added) matches static/schemas/source/**; static/schemas/source/enums/motion-level.json (added) matches static/schemas/source/**; static/schemas/source/formats/canonical/image.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/native_in_feed.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/sponsored_placement.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/video_hosted.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/video_vast.json (modified) 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 16, 2026
@lukasz-pubx

Copy link
Copy Markdown

Proposal review — @ 3466904c5 (tree: all 21 changed files; baseline: repo git history, plus AdCOM v1.0 FINAL and the IAB CTV Signaling Implementation Guidelines fetched from IAB GitHub 2026-08-16.)

Does the tree deliver what it claims?

Yes, with two declared deviations from RFC #6428 and one undeclared one. Every claim I could check against the tree holds: the three enums exist as described; all five canonicals carry their matrix row via allOf conditionals + verifier_constraints; the semantic validator mirrors every documented rule and is wired into validateProductTarget; the compliance YAML has exactly the 6 positives and 7 negatives the description lists; the four StreamHaus examples internally satisfy their own constraints (overlay [10000,30000] ≥ floor, pause image static, menu tile pairs autoplay_muted with a required video slot); the new CTV unit tests include the pause + image → pass case; the changeset is minor and the change is genuinely additive (linear_required retained, creative_type new-optional).

IAB accuracy — verified, not taken on trust: AdCOM v1.0 FINAL on IAB main does now carry plcmt 5–9, playbackmethod 8–11 ("Initiated by user pausing content, Sound on/off"; "Initiated by idling"), and creative attributes 21–23 (Static Visual / Limited Motion (Cinemagraph) / Full-Motion Video) — the annex table is accurate, and calling them ratified is correct as of today. The Signaling Guidelines confirm Menu→Native plcmttype 1/3 and the NonLinear-VAST response pattern. The Native 1.2 vasttag claim is also correct.

RFC deviations: (1) RFC #6428 wanted "VAST 4.3/4.4 in vast_version"; the tree instead relies on 4.3 having landed separately (#6141) and keeps 4.4 out per the pre-existing draft-XSD gate — declared here, and the right call. (2) Overlay widened to native_in_feed — declared. (3) Undeclared: the RFC's workflow section promises preview_creative fixtures demonstrating remote focus/navigation, motion behavior, QR presentation, and fallback rendering — nothing in this PR touches preview fixtures, and the deferral isn't stated.

What the proposal gets right

The core design decision — treating the IAB table as an OpenRTB signaling standard rather than a creative-supply contract, and modeling supply on seller ingestion — is well-evidenced and correctly executed as a declared fork: the annex explicitly separates the two, and the enum $comment names the divergence and its rationale. The linear_required migration is a model of how to supersede a boolean: explicit precedence, an explicit reading for both legacy values (falseeither, not nonlinear-only), and rejection of the contradictory combination. The compliance YAML's grading notes are unusually honest about enforcement layers.

Findings

1. warning — image + motion_level: "full_motion": schema, validator, and docs disagree on conditionality

The schema conditional narrows motion only when an experience is declared ("if": { "required": ["ctv_ad_experience"] }, "then": { … "motion_level": { "enum": ["static", "limited_motion"] } }image.json), and the docs match: "image narrows motion_level to static or limited_motion whenever ctv_ad_experience is set" (ctv-experiences.mdx:77). But the annotation says "motion_level full_motion is rejected on image" (unconditional), and imageCtvViolations rejects full_motion regardless of experience (ctv-experience-matrix.ts:212–222).

Failure narrative: a seller authors a plain (non-CTV) image option for an animated-GIF product with motion_level: "full_motion" — JSON-Schema-based tooling passes it; the reference validator returns a violation. Two conformant validators, opposite answers.

Ask: pick one semantic — either make the narrowing unconditional (drop the if; defensible, since AdCOM attr 23 on a static-image canonical is arguably always wrong) or scope the validator to declared experiences — and align all three surfaces.

2. warning — menu_placement/focus_behavior schema conditional is weaker than the stated rule

The verifier constraint says "menu_placement and focus_behavior require ctv_ad_experience menu" (native_in_feed.json:20), and the validator enforces experience !== 'menu' → violation (ctv-experience-matrix.ts:184). But the schema conditional only requires the field to be present: "then": { "required": ["ctv_ad_experience"] } (native_in_feed.json:13–15) — and native_in_feed also permits overlay. So {ctv_ad_experience: "overlay", menu_placement: "tile"} passes JSON Schema and fails the semantic layer; no unit test or compliance vector covers this case (the existing test only probes the absent-experience variant). This one is draft-07-expressible.

Ask: tighten the conditional to "ctv_ad_experience": { "const": "menu" }, or declare the gap the way focus_video_pairing is declared.

3. warning — enforcement-layer attribution is internally inconsistent across docs, YAML, and schemas

Three copies of "who enforces the duration floors" disagree:

  • Docs: the floors and the focus check are "evaluated when the format option itself is authored and validated against the canonical's schema" (ctv-experiences.mdx:92).
  • YAML grading notes: reject_focus_autoplay_without_video_slot is "the only vector with NO JSON Schema enforcement at all" (ctv-experience-validate-input.yaml:31–33).
  • The schema: video_vast.json's allOf (lines 91–114) contains no duration conditional whatsoever — the 10s/3s floors live only in verifier_constraints prose and the semantic validator, so the 5-second-overlay vector also has no JSON Schema enforcement, and the docs' "validated against the canonical's schema" overstates what schema validation catches.

Compounding it, ctv-experiences.mdx:71 promises "see [Validating CTV manifests] for why" focus/video isn't schema-enforced — and that section never gives the why.

Failure narrative: an implementer builds product-authoring validation by compiling the canonical schemas (exactly what the docs describe), accepts a 5s overlay option, then fails the graded compliance storyboard.

Ask: either add the (expressible) duration-floor conditionals to video_vast.json, or correct both prose sites and the YAML note to attribute floors and focus-pairing to the semantic layer explicitly.

4. warning — activation copy-slot contract: undeclared slot-name vocabulary, contradictory example, undeclared blocking severity

The docs' own example contradicts the validator: "for example activation_message for an SMS or push body" (ctv-experiences.mdx:79) vs text_message: ['activation_text_message'] (ctv-experience-matrix.ts:55). A seller who follows the docs and puts SMS copy in activation_message gets a ctv_activation_copy_slots violation from the reference implementation. The slot IDs are only "e.g." prose in activation-method.json yet enforced as exact names; the code comment says the rule is a "SHOULD" (ctv-experience-matrix.ts:225) yet it emits a blocking violation; and ctv_activation_copy_slots appears in no schema's x-adcp-validation — it's the only enforced rule with no normative anchor. The #6279 warnings[] interaction is flagged only in the PR description, which doesn't ship.

Ask: name the copy-slot IDs normatively (schema description + docs table, not "e.g."), state the disposition (SHOULD-warn vs MUST-reject) in the docs, add the verifier_constraints entry if it stays blocking, and record the "downgrade after #6279" plan somewhere durable (changeset or doc note).

5. warning — "rejects on every unlisted canonical" is claimed as schema-enforced, but isn't, and has zero agent-observable coverage

Docs: "Validation rejects any ctv_ad_experience value on a canonical format that isn't listed below — each canonical's schema enforces its own row" (ctv-experiences.mdx:28). But the unlisted canonicals were untouched and carry "additionalProperties": truehtml5 + ctv_ad_experience: "pause" validates cleanly against every schema; rejection exists only in the semantic map's absent-entry branch (ctv-experience-matrix.ts:27–30, unit-tested — which tests the reference impl, not agents). The compliance storyboard has no unlisted-canonical negative, so nothing observable requires any other seller to reject it.

Ask: correct the docs sentence (the row is enforced by schema only for the five listed canonicals), and add one compliance negative (e.g. pause on html5) so the rule has behavioural coverage.

Notes (advisory)

  • Discovery asymmetry (inherited pattern): product-filters.json filters by format_kinds/format_option_refs but no format-param filter exists, so a buyer can't ask "products with pause inventory" — they enumerate and inspect format options. Consistent with how every other format param works today; worth a declared deferral or a follow-up filter.
  • Reporting granularity: activations map to the pre-existing aggregate engagements metric (delivery-metrics.json:325); a product declaring ["qr_code","deep_link"] gives the buyer no per-method readback. Inherited coarseness — declare if intentional.
  • Floors don't travel to sponsored_placement: in_scene's 3s and squeezeback's 10s floors are defined only on video_vast; the sponsored_placement rows have no duration surface at all. Defensible (seller-composited frames), but the docs' constraint-profile section states the floors per-experience, not per-canonical — one clause saying floors apply only where the buyer supplies timed creative would close it.
  • RFC's preview_creative fixtures are absent and undeclared — state the deferral in the PR description or a follow-up issue.

Review pinned at 3466904c54bf80ff58713431f8910342b20e771a; findings reference that tree. AdCOM/IAB claims verified against IAB GitHub main as of 2026-08-16.

Schema fixes:
- image.json: unconditionally narrow motion_level to static/limited_motion
  (full_motion is AdCOM attr 23 / Full-Motion Video, incoherent on any
  static-image canonical, not just when ctv_ad_experience is set)
- image.json: add ctv_activation_copy_slots to x-adcp-validation annotation
- native_in_feed.json: tighten menu-profile conditional to require
  ctv_ad_experience: menu when menu_placement or focus_behavior is present
- native_in_feed.json: add ctv_activation_copy_slots annotation
- activation-method.json: make slot IDs normative with per-method mapping
  (push_notification→activation_message, text_message→activation_text_message,
  email→activation_email_subject+activation_email_body)

Docs fixes (ctv-experiences.mdx):
- Correct enforcement-attribution paragraph: distinguish JSON Schema
  conditionals (categorical) from semantic-validator rules (duration floors,
  unlisted-canonical matrix, focus_video_pairing, activation copy slots)
- Correct activation_methods paragraph: name each method's normative slot
  distinctly (was conflating push/SMS under "activation_message")
- motion_level paragraph: make unconditional narrowing explicit

Compliance storyboard (ctv-experience-validate-input.yaml):
- Grading note: document that duration floors and unlisted-canonical matrix
  checks are also semantic-only (not just focus_video_pairing)
- reject_pause_full_motion_image description: remove "whenever ctv_ad_experience
  is set" — rejection is unconditional
- Add ctv_invalid_pause_html5 invalid product fixture (html5+pause, uses
  html5_bundle/zip slot)
- Add reject_pause_on_html5 defense-in-depth step exercising the
  semantic-only unlisted-canonical matrix check
- Update product/step counts (thirteen→fourteen, seven→eight)

@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 files require human/CODEOWNERS approval.

This is a clean iteration on the CTV experience matrix. The prior medium finding (ctv-experience-matrix.ts:245 — ctv_activation_copy_slots hard-failing spec-valid products) has been resolved: the reviewer now confirms the activation-copy-slots semantics match the reference validator, and all four JSON-Schema-conditional claims plus semantic-only claims verify against the schemas. New activation slot names are consistent across the enum description, image.json, native_in_feed.json, and ACTIVATION_COPY_SLOTS. Changeset present (.changeset/add-ctv-experience-profiles.md), product counts reconcile (6 valid + 8 invalid = 14), and the reviewer surfaced zero findings this run.

However, this PR touches files under a hard, non-overridable approval gate (static/schemas/source/**) and the current review_decision is not APPROVED. Per decision-table row 2 (gated_paths true AND review_decision ≠ APPROVED), the outcome must be escalate — Ladon cannot auto-approve schema-source changes until a human/CODEOWNERS approval is recorded, no matter how clean the diff is.

Escalation reasons

  • Adds 3 new enums (activation-method.json, ctv-ad-experience.json, motion-level.json) and modifies 5 canonical format schemas under static/schemas/source/** — a gated protocol-surface change. review_decision is unknown/not APPROVED, so the hard approval gate stands.

Note: the prior blocking medium finding is gone; this escalation is purely the gated-path/human-ratification requirement, not a code defect.

Why human review

  • Touches gated schema-source files under static/schemas/source/** (3 new enums: activation-method.json, ctv-ad-experience.json, motion-level.json; 5 modified canonical formats) and review_decision is not APPROVED — hard approval gate requires human/CODEOWNERS review before merge.
  • New enum values on published protocol surface are a normative/potentially breaking-class change; ratification is a human act per the constitution's decision-class hard gate — escalate unless review_decision is APPROVED.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/enums/activation-method.json (added) matches static/schemas/source/**; static/schemas/source/enums/ctv-ad-experience.json (added) matches static/schemas/source/**; static/schemas/source/enums/motion-level.json (added) matches static/schemas/source/**; static/schemas/source/formats/canonical/image.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/native_in_feed.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/sponsored_placement.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/video_hosted.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/video_vast.json (modified) 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.

Comment thread server/src/training-agent/ctv-experience-matrix.ts

@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 — hard approval gate (gated schema-source paths) not satisfied, plus new-enum breaking-class governance gate.

This PR adds 3 new enums and modifies 5 canonical formats under static/schemas/source/**, which are under a deterministic hard approval gate (gated_paths: true) and review_decision is not APPROVED (unknown/none). Per decision-rule row 2, the outcome must be escalate until a human/CODEOWNERS approval is recorded. The repo's constitutional hard gate on new enum values (breaking-class ratification is a human act) independently requires the same posture.

Medium findings

  • server/src/training-agent/ctv-experience-matrix.ts:236 — ctv_activation_copy_slots check removed from the semantic validator, but docs/creative/ctv-experiences.mdx:132 still assert the validator enforces it. Schema↔docs coherence gap; align the docs to the SHOULD framing.

Escalation reasons

  • Touches gated schema-source files under static/schemas/source/** (3 new enums: activation-method.json, ctv-ad-experience.json, motion-level.json; 5 modified canonical formats) and review_decision is not APPROVED — human/CODEOWNERS review required before merge.
  • New enum values on the published protocol surface are a normative/breaking-class change; ratification is a human act per the constitution's decision-class hard gate — escalate unless review_decision is APPROVED.

The reviewer surfaced one fresh medium finding this run (docs↔validator drift), so the prior escalation legitimately persists (row 6) in addition to the standing gate.

Medium findings

  • server/src/training-agent/ctv-experience-matrix.ts:236 — ctv_activation_copy_slots check removed but docs still claim the semantic validator enforces it

Why human review

  • Touches gated schema-source files under static/schemas/source/** (3 new enums: activation-method.json, ctv-ad-experience.json, motion-level.json; 5 modified canonical formats) and review_decision is not APPROVED — hard approval gate requires human/CODEOWNERS review before merge.
  • New enum values on the published protocol surface are a normative/breaking-class change; ratification is a human act per the constitution's decision-class hard gate — escalate unless review_decision is APPROVED.
  • docs/creative/ctv-experiences.mdx:132 still claims the semantic validator enforces ctv_activation_copy_slots after the check was removed in ctv-experience-matrix.ts:236 — schema↔docs coherence gap to resolve before merge.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/enums/activation-method.json (added) matches static/schemas/source/**; static/schemas/source/enums/ctv-ad-experience.json (added) matches static/schemas/source/**; static/schemas/source/enums/motion-level.json (added) matches static/schemas/source/**; static/schemas/source/formats/canonical/image.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/native_in_feed.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/sponsored_placement.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/video_hosted.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/video_vast.json (modified) 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.

@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 — hard approval gate on gated schema-source paths not yet satisfied.

This PR touches static/schemas/source/** (3 new enums: activation-method.json, ctv-ad-experience.json, motion-level.json; 5 modified canonical formats). gated_paths: true and review_decision: REVIEW_REQUIRED (not APPROVED), so Decision Row 2 forces escalate. Additionally, new enum values on the published protocol surface are a normative/breaking-class change whose ratification is a human act per the constitution's decision-class hard gate — escalate unless review_decision is APPROVED.

Iteration credit: the prior pass's sole finding (docs at ctv-experiences.mdx claiming the semantic validator enforced ctv_activation_copy_slots after that check was removed) has been resolved — the docs now correctly state the constraint is advisory until a warnings[] channel lands, matching the shipped validator and schema prose. No new findings surfaced this run. The diff is clean; the only reason for escalate is the unsatisfied hard gate, not code defects.

Escalation reasons

  • Gated schema-source files touched (static/schemas/source/enums/*.json + 5 canonical formats under static/schemas/source/formats/canonical/) and review_decision is REVIEW_REQUIRED, not APPROVED — human/CODEOWNERS approval required before merge.
  • New enum values on the published protocol surface are a Breaking/Normative-class change per the constitution's decision-class hard gate — ratification is a human act; escalate unless review_decision is APPROVED.

Why human review

  • Gated paths: touches static/schemas/source/** (3 new enums + 5 modified canonical formats) and review_decision is REVIEW_REQUIRED, not APPROVED — human/CODEOWNERS approval required before merge.
  • New enum values on the published protocol surface are a Breaking/Normative-class change per the constitution's decision-class hard gate — ratification is a human act; escalate unless review_decision is APPROVED.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/enums/activation-method.json (added) matches static/schemas/source/**; static/schemas/source/enums/ctv-ad-experience.json (added) matches static/schemas/source/**; static/schemas/source/enums/motion-level.json (added) matches static/schemas/source/**; static/schemas/source/formats/canonical/image.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/native_in_feed.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/sponsored_placement.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/video_hosted.json (modified) matches static/schemas/source/**; static/schemas/source/formats/canonical/video_vast.json (modified) 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.

@pkras pkras left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewing the implementation of RFC #6428. The plural-matrix design is the right call and largely lands well. One substantive issue (SIMID, inline below) and two design questions worth resolving before merge. Full context also on the RFC: #6428.

A. Buy-time vs. runtime experience selection. The matrix models ctv_ad_experience as a buyer-selected format option. That's correct for guaranteed/takeover deals (you buy the pause takeover deliberately), but for programmatic, CTV players (SpringServe, FreeWheel, Publica) resolve pause vs. screensaver vs. overlay from viewer state at serve time — a fixed per-option experience is a commitment the buyer can't honor. Worth stating whether ctv_ad_experience is a firm buy-time declaration (guaranteed) or an eligibility signal the player resolves at runtime (programmatic), ideally supporting both rather than defaulting to buy-time for all six.

B. Execution model for multi-canonical cells. For cells with two permitted canonicals (pause = image/video_vast, squeezeback = video_vast/sponsored_placement), validation is defined but execution isn't. Model A — seller publishes one format option per canonical it supports, consistent with get_products; buyer selection resolves it, no new wire surface — looks correct and composes with (A). Suggest confirming Model A explicitly so nobody builds a fallback_canonical field for a problem option-selection already solves.

Minor: for squeezeback/in_scene the two-canonical cell is really two delivery contracts — sponsored_placement = seller resolves a catalog reference at serve time; video_vast = seller receives a finished asset (however produced, including pre-baked catalog variants, invisible to AdCP). One doc line would help implementers pick on the resolution boundary rather than an "is there a catalog" test.

Coordination: this PR and #6279 both touch the warnings[]/violation channel and error-code.json; the activation-copy-slot check should downgrade from violation to warning once #6279's warnings channel lands. Flagging so whichever merges second reconciles it.

},
"ctv_ad_experience": {
"$ref": "/schemas/enums/ctv-ad-experience.json",
"description": "CTV experience this option serves. On video_vast only `pause`, `screensaver`, `overlay`, `squeezeback`, and `in_scene` are valid (`menu` routes to native_in_feed or sponsored_placement), and `creative_type` MUST be `nonlinear`. Per-experience floors: `overlay` and `squeezeback` require a 10s minimum duration; `in_scene` requires a 3s minimum brand-exposure duration and forbids interactivity (`vpaid_enabled`/`simid_supported` MUST NOT be true); `pause` has no duration floor and ends on viewer or device action. SIMID support is declared explicitly, never inferred, and NonLinear creatives MUST carry a non-interactive StaticResource or MediaFile fallback."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

SIMID can't ride on nonlinear — this describes a creative VAST can't serialize. This mandates creative_type: nonlinear for all five experiences while the option still exposes simid_supported. But SIMID is carried only by <InteractiveCreativeFile>, which the VAST 4.2 XSD declares under <Linear><MediaFiles> (vast_4.2.xsd line 628); NonLinearAd_Inline_type has no such element. So nonlinear + simid_supported: true cannot be expressed in VAST, and the "NonLinear creatives MUST carry a non-interactive StaticResource/MediaFile fallback" rule describes a shape that can't exist (StaticResource is a NonLinear/companion construct, not a SIMID fallback).

The experiences are correctly nonlinear — the fix is to drop simid_supported from them, not relabel them linear. Interactivity on these surfaces (QR, deep link, push, tune-in) already flows through activation_method, which is the right home. (Nonlinear ads can still be interactive via HTMLResource + the nonlinear apiFramework attribute — this is specific to SIMID, which is Linear-bound.) The in_scene no-interactivity constraint below is correct and unaffected.

@@ -3,7 +3,6 @@
"$id": "/schemas/formats/canonical/video_vast.json",
"title": "Canonical Format: VAST Video",
"description": "VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID extensions for interactive video supported as VAST extensions. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wording: SIMID is a first-class VAST element, not a "VAST extension" — it's signaled as <InteractiveCreativeFile apiFramework="SIMID"> (VAST 4.1+). Suggest rewording "SIMID extensions for interactive video supported as VAST extensions." (VPAID rode on apiFramework on <MediaFile>, which is likely the source of the "extension" phrasing.)

- **`overlay`, `squeezeback`** — minimum duration of 10 seconds (`duration_ms_exact` or the floor of `duration_ms_range` must be at least `10000`).
- **`in_scene`** — minimum 3-second brand-exposure duration, and **no interactivity**: `vpaid_enabled` and `simid_supported` must both be `false` (or absent) on the format option.
- **`pause`** — no duration floor. The unit ends on a viewer or device action (resume, timeout), not a fixed clock.
- **SIMID support is always explicit, never inferred.** A format option sets `simid_supported: true` deliberately; a nonlinear creative that supports SIMID still MUST carry a non-interactive `<StaticResource>` or `<MediaFile>` fallback so players without SIMID support still render something.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same SIMID-on-nonlinear issue as the schema: a nonlinear creative can't carry SIMID at all (<InteractiveCreativeFile> is Linear-only in the VAST 4.2 XSD), so this fallback rule describes a creative that can't be serialized. Recommend dropping simid_supported from these experiences and routing interactivity through activation_method.


## VAST versioning

`vast_version` supports VAST 2.0 through 4.3. VAST 4.3 ships no XSD of its own — IAB's `vast_4.2.xsd` remains the authoritative schema for 4.3 documents, so a validator MUST NOT reject a tag declaring `version="4.3"` for want of a 4.3-specific schema. VAST 4.4 is deliberately absent from the enum: `vast_4.4.xsd` is still annotated "DRAFT for working group discussion," not a published specification, and AdCP adds it only once IAB publishes a final schema. The NonLinearAds pattern every non-`menu` experience relies on remains valid under 4.2 serialization — sellers don't need 4.3 or a future 4.4 to express `overlay`, `squeezeback`, `in_scene`, `pause`, or `screensaver` creative.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This reads as "a SIMID-bearing nonlinear creative serializes fine in 4.2," which isn't true — a plain <NonLinearAds> creative is valid in 4.2, but there's no <InteractiveCreativeFile> slot under NonLinearAds, so a SIMID-bearing one is not. If simid_supported is dropped from these experiences (see schema comment), this line is fine as-is for the non-interactive nonlinear pattern; just make sure it isn't read as endorsing SIMID+nonlinear.

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 IAB CTV experience profiles for AdCP 3.2

4 participants