Skip to content

feat(v2): prototype v2 → v1 Product projection layer#1815

Draft
bokelley wants to merge 8 commits into
mainfrom
bokelley/v2-to-v1-prototype
Draft

feat(v2): prototype v2 → v1 Product projection layer#1815
bokelley wants to merge 8 commits into
mainfrom
bokelley/v2-to-v1-prototype

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Draft. Companion to PR #1809 (8.0 design proposal). This implements and stress-tests the v2 → v1 projection layer the design proposes — to see if the "v2-only public API, project to v1 at the boundary" stance is actually operable today.

What it does

Takes a V2 Product (with format_options[]) and produces a v1 Product (with format_ids[]) suitable for sending to a v1-only seller. The resolution order mirrors v1-canonical-mapping.json, inverted:

Step Path Outcome
1 format_kind: "custom" + canonical_formats_only: true No v1 emit. FORMAT_DECLARATION_V1_UNREACHABLE diagnostic.
2 v1_format_ref present on the declaration Use verbatim (seller-asserted equivalence — highest confidence).
3 Registry reverse-lookup finds an invertible literal entry Synthesize a v1 format_id.
4 Registry has entries but none invert (wildcarded / structural) No v1 emit. FORMAT_DECLARATION_V1_AMBIGUOUS diagnostic.
5 Registry has no entries for this canonical No v1 emit. FORMAT_DECLARATION_V1_UNREACHABLE diagnostic.

All diagnostics on a structured channel — never logger-only, per the spec's resolution-order amendment.

Coverage against 13 spec reference fixtures

Vendored from adcontextprotocol/adcp#3307 static/examples/products/canonical/. The fixtures span 12 of the 13 canonical format_kinds.

=== v2 → v1 projection coverage (13 spec fixtures) ===

Clean v1 emit (1/13):
  ✓ nytimes_homepage_mrec

Explicit canonical_formats_only opt-out (1):
  ✗ nytimes_homepage_takeover_custom (custom)

Ambiguous registry match — family exists but not invertible (7):
  ? gam_3p_display_tag (display_tag)
  ? meta_reels_us (video_hosted)
  ? nytimes_homepage_html5 (html5)
  ? the_daily_30s_host_read (audio_hosted)
  ? triton_daast_audio_30s (audio_daast)
  ? veo_generative_video_15s (video_hosted)
  ? youtube_vast_preroll (video_vast)

No registry entry for the canonical (4):
  ✗ amazon_sponsored_products (sponsored_placement)
  ✗ chatgpt_brand_mention (agent_placement)
  ✗ google_performance_max (responsive_creative)
  ✗ meta_carousel (image_carousel)

Headline findings

1. Only 8% of v2 fixtures project cleanly without seller intervention

92% of the spec's own reference fixtures can't be downgraded today. Implications for the 8.0 design:

  • The "V2-only public API, projection at the boundary" stance still works — products that can't downgrade surface via diagnostics; the v1 buyer doesn't see them. Honest.
  • But the "make v1 sellers look V2-shaped" promise depends on sellers actively adding v1_format_ref to their declarations. The current spec reference fixtures don't model this — none of them carry v1_format_ref.
  • 4 of the 13 canonicals (sponsored_placement, agent_placement, responsive_creative, image_carousel) are genuinely new concepts in v2 with no v1 form possible. Those products simply won't exist on the v1 wire — and that's correct.

2. The registry's IAB-named entries produce wire-invalid format_ids

The only fixture that projects cleanly (nytimes_homepage_mrec) synthesizes format_id.id: "iab/mrec_300x250" — but format-id.json requires ^[a-zA-Z0-9_-]+$, which rejects slashes. Same cross-schema mismatch we flagged upstream when surveying the registry earlier. The clean-emit path itself currently produces wire-invalid output. The projection layer surfaces this rather than papering over it.

3. Registry needs an explicit "v1-unreachable" signal per canonical

Four canonicals (sponsored_placement, agent_placement, responsive_creative, image_carousel) have NO registry entries. The projection emits reason: "no_v1_format_ref_or_registry_match" — but really, the spec could declare these canonicals as inherently v1-unreachable, the same way canonical_formats_only: true declares per-product unreachability. Would let adopters distinguish "spec says no v1 form exists for this canonical" from "spec hasn't filled in this entry yet."

What's in scope vs not

In scope (this PR):

  • src/lib/v2/projection/{types,registry,v2-to-v1}.ts — projection module.
  • test/lib/v2-to-v1-projection.test.js — 16 cases across 4 suites.
  • Vendored fixtures at test/lib/v2-projection-fixtures/.
  • Diagnostic surface (typed).
  • Hand-rolled TypeScript types matching schema shape at projection precision.

Explicit non-scope:

  • v1 → v2 (upgrade) direction — symmetric counterpart, follow-up.
  • Full versioned codegen — separate piece of the 8.0 enablement; types here are minimal hand-rolls.
  • Public barrel export — projection is internal-only until auto-negotiation lands.
  • Auto-negotiation per agent — design-doc territory; not implemented here.
  • fetchFormatSchema for custom shapes — separate piece.

Test plan

  • Typecheck clean.
  • All 16 test cases pass (4 suites: per-fixture verdict, opt-out, IAB image, ecosystem coverage).
  • npm run format:check clean (vendored fixtures added to .prettierignore per the same rule as webhook-signing-vectors/).
  • Structural invariant verified for every fixture: format_ids.length + diagnostics.length === format_options.length.
  • CI green.

Reviewer focus

Treat this as a design-prototype PR, not a feature PR. The key questions:

  1. Does the projection algorithm produce honest output for the messy fixtures? (Read the coverage report.)
  2. Are the diagnostic codes (FORMAT_DECLARATION_V1_UNREACHABLE, FORMAT_DECLARATION_V1_AMBIGUOUS) the right vocabulary, or should they unify?
  3. Does this confirm the 8.0 design (PR docs(development): draft 8.0 design proposal for AdCP 3.1 support #1809) is workable, or does it surface a flaw?

🤖 Generated with Claude Code

bokelley and others added 2 commits May 17, 2026 08:40
Working prototype of the v2 → v1 projection from the 8.0 design
proposal (PR #1809). The SDK's job in 8.0 is to make every seller —
including v1-only ones — look V2-shaped to the adopter. This is the
downgrade direction: take a V2 Product (format_options) and produce
a v1 Product (format_ids) for sellers that only speak 3.0.

Components:
- src/lib/v2/projection/types.ts — minimal hand-rolled types
  for V2 Product, ProductFormatDeclaration (discriminated union over
  12 format_kinds), V1 format_id, ProjectionDiagnostic.
- src/lib/v2/projection/registry.ts — loads
  schemas/cache/3.1.0-beta.0/registries/v1-canonical-mapping.json
  and implements best-effort reverse-lookup. Distinguishes
  invertible matches (literal globs), ambiguous matches (wildcarded
  globs + structural), and no-match.
- src/lib/v2/projection/v2-to-v1.ts — the projection algorithm.
  Resolution order mirrors v1-canonical-mapping.json's forward
  order, inverted: canonical_formats_only → v1_format_ref →
  registry match → diagnostic.

Tests: 16 cases across 4 suites in test/lib/v2-to-v1-projection.test.js.
Run against all 13 spec reference fixtures from
adcontextprotocol/adcp#3307 (vendored at
test/lib/v2-projection-fixtures/). The structural invariant assertion
proves every declaration produces either a v1 emit or a diagnostic
— never both, never neither.

Coverage report from real data:
- 1/13 clean v1 emit (nytimes_homepage_mrec)
- 1/13 explicit canonical_formats_only opt-out
- 7/13 ambiguous registry match (display_tag, video_hosted ×2,
  html5, audio_hosted, audio_daast, video_vast)
- 4/13 no registry coverage (sponsored_placement, agent_placement,
  responsive_creative, image_carousel — genuinely new in v2)

Scope: hand-rolled types (not full versioned codegen yet); v1 → v2
upgrade direction not in this iteration; not wired into the public
barrel (internal-only until auto-negotiation lands). All public
shapes are explicitly EXPLORATORY — may change as the 8.0 design
firms up. See PR #1809 for the design proposal this prototype tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream adcontextprotocol/adcp landed all five asks the prototype
surfaced. Re-syncs the local 3.1.0-beta cache and updates the
projection layer to honor the new normative rules.

What the spec added:
- v1_translatable boolean on _base.json (default true) with overrides
  on the 4 inherently-v2 canonicals (image_carousel, sponsored_placement,
  responsive_creative, agent_placement). Spec is explicit: SDKs MUST
  NOT emit FORMAT_PROJECTION_FAILED on these — they're structural
  unreachability, not registry-coverage gaps.
- FORMAT_DECLARATION_V1_AMBIGUOUS error code with normative prose and
  drift disposition (errors[] augmentation with source: "sdk",
  sdk_id, etc.).
- "MUST NOT synthesize v1_format_ref" rule: SDKs MAY do best-effort
  inversion when a registry entry has a literal glob (no wildcard,
  no contradicting seller v1_format_ref) but the projection is
  explicitly non-normative — downstream consumers MUST NOT depend on it.
- Direction-of-truth statement in v1-canonical-mapping.json:
  registry is v1→v2 authoritative only.
- v1_format_ref added to 2 reference fixtures (nytimes_homepage_mrec,
  triton_daast_audio_30s).
- IAB-named registry globs respelled with underscores
  (iab_mrec_300x250) — wire-valid per format-id.json pattern, closes
  the cross-schema mismatch we flagged.

SDK changes:
- New src/lib/v2/projection/canonical-properties.ts reads
  v1_translatable from formats/canonical/<kind>.json schemas with
  inheritance from _base.json default.
- v2-to-v1.ts gains a Step 2 (canonical-level v1_translatable check)
  before the seller-asserted v1_format_ref step. v1_translatable: false
  emits CANONICAL_NOT_V1_TRANSLATABLE (SDK-local code) per the spec's
  "different diagnostic" carve-out.
- Diagnostic shape updated to match the spec's errors[] augmentation
  contract: source: "sdk", sdk_id read from version.ts at module load,
  field pointing at the offending declaration, error.details
  per-code.
- Diagnostic codes:
  * FORMAT_PROJECTION_FAILED (spec code): registry-coverage gap on a
    v1_translatable canonical
  * FORMAT_DECLARATION_V1_AMBIGUOUS (spec code): family known, specific
    v1 not pickable
  * FORMAT_DECLARATION_V1_NOT_APPLICABLE (SDK-local): seller-asserted
    canonical_formats_only opt-out
  * CANONICAL_NOT_V1_TRANSLATABLE (SDK-local): v1_translatable: false

Coverage report post-upstream-fixes (13 spec fixtures):
- Clean v1 emit via v1_format_ref (NORMATIVE): 2/13
- Clean v1 emit via registry synthesis (NON-NORMATIVE): 0/13
- canonical_formats_only opt-out: 1/13
- Inherently v2 (v1_translatable: false): 4/13
- Ambiguous family: 6/13
- Registry-coverage gaps: 0/13

Tests: 21 pass across 5 suites. Structural invariants assert every
diagnostic carries spec-mandated source + sdk_id and a field pointing
at the offending declaration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

Re-run after upstream landed all five asks

Upstream adcontextprotocol/adcp committed all five spec changes the prototype surfaced. Re-synced the local 3.1.0-beta cache and updated the projection layer to honor the new normative rules. Pushed at `61e043f6`.

What landed upstream:

  • `v1_translatable` boolean on `_base.json` (default true) with overrides on the 4 inherently-v2 canonicals (`image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`). MUST NOT emit `FORMAT_PROJECTION_FAILED` on these.
  • New error code `FORMAT_DECLARATION_V1_AMBIGUOUS` with full normative prose + drift disposition + `errors[]` augmentation contract.
  • Direction-of-truth statement: registry is v1→v2 authoritative only.
  • Resolution-order step 5 (ambiguous family) before fail-closed.
  • `v1_format_ref` added to 2 reference fixtures (`nytimes_homepage_mrec` → `iab_mrec_300x250`, `triton_daast_audio_30s` → `daast_audio_30s_v1_1`).
  • "SDKs MUST NOT synthesize v1_format_ref" rule (best-effort literal-glob inversion is MAY-do but non-normative).
  • Bonus: IAB-named registry globs respelled with underscores. Closes the wire-validity cross-schema mismatch.

Coverage post-upstream-fixes (13 spec fixtures):

Bucket Pre-fix Post-fix
Clean via `v1_format_ref` (normative) 0 2
Clean via registry synthesis (non-normative) 1 0
`canonical_formats_only` opt-out 1 1
Inherently v2 (`v1_translatable: false`) n/a (conflated) 4
Ambiguous family 7 6
Registry-coverage gap 4 0

The spec fixes did real work: the four inherently-v2 canonicals are now correctly separated from registry gaps; the registry covers every v1_translatable canonical the fixture set exercises; the two seller-annotated fixtures project cleanly via the normative path.

SDK changes in this commit:

  • New `src/lib/v2/projection/canonical-properties.ts` reads `v1_translatable` from the canonical schemas with `_base.json` inheritance.
  • `v2-to-v1.ts` gains a canonical-level check before the seller-asserted `v1_format_ref` step.
  • Diagnostic shape updated to spec's `errors[]` augmentation contract (`source: "sdk"`, `sdk_id`, `field`, `error.details`).
  • Diagnostic codes split into 4 buckets: 2 spec codes (`FORMAT_PROJECTION_FAILED`, `FORMAT_DECLARATION_V1_AMBIGUOUS`) + 2 SDK-local (`FORMAT_DECLARATION_V1_NOT_APPLICABLE`, `CANONICAL_NOT_V1_TRANSLATABLE`).

Tests: 21 pass across 5 suites. New invariants assert every diagnostic carries spec-mandated `source` + `sdk_id`.

The remaining 6 ambiguous-family fixtures (display_tag, video_hosted ×2, html5, audio_hosted, video_vast) need sellers to add `v1_format_ref` to project cleanly. That's the expected migration pattern — the upstream-added fixtures (mrec + daast) now model it.

Upstream landed the registry↔catalog↔fixtures reconciliation:
- Registry replaced 13 IAB-prefixed entries with 17 catalog-actual
  entries (8 image sizes + 2 html5 sizes + display_js + video_vast
  family + 3 audio_standard + 2 video_standard + meta_reels)
- AAO catalog (reference-formats.json) gained `canonical` annotations
  on 32 entries
- 5 reference fixtures now carry `v1_format_ref` pointing at real
  catalog ids; triton_daast moved to canonical_formats_only: true
  (no DAAST entry in the catalog yet)

SDK projection updates:
- Step 1 (canonical_formats_only) broadened to apply to any
  format_kind, not just `custom`. The 3.1 spec uses it on
  `audio_daast` (triton_daast fixture) as a "no v1 form yet"
  signal — semantically distinct from `format_kind: custom` but
  taking the same code path.
- Synthesized agent_url uses `creative.adcontextprotocol.org/`
  (trailing slash) to match the upstream-canonicalized form used by
  the seller-asserted fixtures.

Coverage post-reconciliation (13 spec fixtures):

| Bucket                                            | Before | After |
|---------------------------------------------------|-------:|------:|
| Clean via v1_format_ref (NORMATIVE)               |      2 |     5 |
| Clean via registry synthesis (NON-NORMATIVE)      |      0 |     2 |
| canonical_formats_only opt-out                    |      1 |     2 |
| Inherently v2 (v1_translatable: false)            |      4 |     4 |
| Ambiguous family                                  |      6 |     0 |
| Registry-coverage gap                             |      0 |     0 |

**7/13 (54%) clean projections, up from 2/13 (15%).** The remaining 6
are inherently v1-unreachable by design (4 v2-only canonicals +
2 explicit opt-outs) — they SHOULDN'T project; the SDK correctly
emits structured diagnostics. Every fixture in the "ambiguous" bucket
moved to either v1_format_ref (4) or registry synthesis (2).

Of the 5 seller-asserted projections:
- 4 point at the AAO canonical agent_url (creative.adcontextprotocol.org/)
  with catalog ids: display_300x250_image, display_300x250_html,
  display_js, video_vast_30s. Same canonical shape across N publishers
  produces the SAME format_id — convergent, not divergent.
- 1 points at the publisher-specific agent_url (meta.adcp/meta_reels)
  because Meta Reels is a proprietary format with no IAB-standard
  equivalent.

This is the pattern the spec now models: converge on AAO ids for
industry-standard formats; keep publisher namespacing for proprietary
platforms. Solves the per-publisher format-id sprawl concern from the
prior round.

Tests: 25 pass across 5 suites. Includes per-fixture structural
invariant assertions for all 13 plus per-bucket coverage tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

Post-reconciliation: 7/13 project cleanly. Zero ambiguous, zero gaps.

Upstream landed the registry↔catalog↔fixtures reconciliation (`03e10e83`):

  • Registry: 13 IAB-prefixed entries → 17 catalog-actual entries (8 image sizes + 2 html5 + display_js + video_vast family + audio_standard ×3 + video_standard ×2 + meta_reels).
  • AAO catalog: 32 entries gained `canonical` annotations (image, html5, display_tag, video_hosted, video_vast, audio_hosted, sponsored_placement).
  • 5 fixtures now carry `v1_format_ref` pointing at real catalog ids; triton_daast moved to `canonical_formats_only: true` since the catalog has no DAAST entry yet.

Coverage shifted dramatically:

Bucket Round 1 Round 2 (v1_translatable) Round 3 (reconciliation)
Clean via `v1_format_ref` (NORMATIVE) 0 2 5
Clean via registry synthesis (NON-NORMATIVE) 1 0 2
`canonical_formats_only` opt-out 1 1 2
Inherently v2 (`v1_translatable: false`) n/a 4 4
Ambiguous family 7 6 0
Registry-coverage gap 4 0 0
Clean total 1 (8%) 2 (15%) 7 (54%)

The remaining 6 are inherently v1-unreachable by design — 4 v2-only canonicals and 2 explicit opt-outs. They SHOULDN'T project; the SDK correctly emits structured diagnostics. Every fixture in the prior "ambiguous" bucket either got `v1_format_ref` (4) or now synthesizes via the new literal registry entries (2).

The convergence pattern works

Of the 5 seller-asserted projections:

  • 4 point at the AAO canonical agent_url `creative.adcontextprotocol.org/` with catalog ids: `display_300x250_image`, `display_300x250_html`, `display_js`, `video_vast_30s`. Same canonical shape across N publishers produces the SAME format_id — convergent, not divergent. Solves the per-publisher format-id sprawl concern.
  • 1 points at the publisher-specific agent_url (`meta.adcp/meta_reels`) because Meta Reels is a proprietary format with no IAB-standard equivalent.

This is exactly the pattern the spec now models: converge on AAO ids for industry-standard formats; keep publisher namespacing for proprietary platforms.

SDK changes in `03e10e83`

  • Step 1 (`canonical_formats_only`) broadened to apply to any format_kind, not just `custom`. The 3.1 spec uses it on `audio_daast` (triton_daast) as a "no v1 form yet" signal.
  • Synthesized agent_url uses `creative.adcontextprotocol.org/` (trailing slash) to match upstream's canonicalized form.
  • Tests updated: 25 pass, including round-trips against all 5 seller-asserted fixtures and structural-invariant assertions for all 13.

What this leaves us

The v2 → v1 projection prototype is essentially complete for the 3.1 reference fixture set. The remaining work is:

  1. v1 → v2 projection (the upgrade direction) — symmetric counterpart, not yet implemented.
  2. Wire into auto-negotiation — projection layer is internal-only until the per-agent negotiation surface from PR docs(development): draft 8.0 design proposal for AdCP 3.1 support #1809 lands.
  3. Versioned codegen — separate piece of the 8.0 enablement; current types are hand-rolled.

The honest design takeaway: the 8.0 "v2-only public API, project at the boundary" stance works for ~54% of products today (the realistic ceiling once 4 inherently-v2 canonicals + opt-outs are accounted for, it's actually 7/7 = 100% of projectable products). The adoption curve depends on sellers annotating `v1_format_ref` on their declarations — which the upstream reference fixtures now demonstrate.

bokelley and others added 2 commits May 17, 2026 10:09
CI doesn't sync the 3.1.0-beta.0 cache — it's gitignored and only
exists in workspaces that have manually built the unreleased spec PR.
The projection layer's registry + canonical-properties loaders depend
on schemas/cache/3.1.0-beta.0/, so attempting to project without it
throws and the tests fail.

Skip cleanly with a clear reason via `{ skip: SKIP_REASON }` on every
describe. Locally (cache present) all 25 tests still pass; in CI all
5 suites report `# SKIP requires schemas/cache/3.1.0-beta.0/`.

Same pattern as `adcp-version-release-precision.test.js`'s wire-
prerelease guard. Will start running in CI when upstream
adcontextprotocol/adcp#3307 merges and `npm run sync-schemas` can
pull a real 3.1.0 tarball.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symmetric counterpart to the v2 → v1 projection. The 8.0 design's
"V2-only public API" stance needs both directions: v2 → v1 lets a V2
buyer talk to a v1 seller; v1 → v2 lets a V2 buyer see a v1 seller's
existing products as V2-shaped. v1 → v2 is the practically more
valuable direction today because most inventory in the wild is v1.

Resolution order (mirrors v1-canonical-mapping.json forward):
1. Catalog entry with explicit `canonical:` annotation → NORMATIVE clean
   projection. The AAO publishes 32/57 entries with this annotation in
   reference-formats.json at 3.1 GA.
2. Catalog entry WITHOUT `canonical:` → fail-closed with new diagnostic
   reason `catalog_lacks_canonical_annotation`. The AAO's deliberate
   absence of annotation is the spec saying "no v2 mapping yet for
   this category" — symmetric to v1_translatable: false on the v2 side.
   Native, DOOH, broadcast, and card-scaffolding sit in this bucket at
   3.1 GA. Honest fail-closed prevents semantically-wrong structural
   shoehorning (e.g., DOOH billboard → display_tag because of url
   asset would be wrong).
3. Not in catalog → try registry glob match against format_id.id.
4. No registry hit but catalog provides assets → structural match.
   Only fires for publisher-bespoke formats not in the AAO catalog.
5. Fail-closed with reason `no_match`.

New modules:
- src/lib/v2/projection/catalog.ts — loads reference-formats.json,
  keyed by agent_url (trailing-slash normalized) + id.
- src/lib/v2/projection/v1-to-v2.ts — the projection.
- src/lib/v2/projection/registry.ts gains `forwardLookupByGlob` and
  `forwardLookupByStructural` exports.
- Diagnostic shape extended: FORMAT_PROJECTION_FAILED.resolution_failure
  now accepts `no_registry_match | catalog_lacks_canonical_annotation
  | no_match` to distinguish v1↔v2 failure modes.

Vendored: test/lib/v2-projection-fixtures/aao-reference-formats.json
copied from .context/adcp-3307/server/src/creative-agent/. Same precedent
as the v2 reference fixtures — byte-fidelity preserved via .prettierignore.

Coverage report (full 57-entry AAO catalog):
- Step 1 NORMATIVE: 32/57 (56%) — all catalog entries with `canonical:`
  annotations project cleanly to v2. Spans image, html5, display_tag,
  video_hosted, video_vast, audio_hosted, sponsored_placement.
- catalog_lacks_canonical_annotation: 25/57 (44%) — explicitly the 5
  categories the spec flagged as un-annotated: 8 display_*_generative,
  3 broadcast_spot, 2 native_*, 4 dooh_*, 7 *_card_*, 1 native_mention.
  These need future canonical assignments (or new canonicals).
- no_match: 0/57. Every catalog entry either projects or surfaces a
  precise reason it can't.

Scope notes (deliberate prototype gaps):
- AAO catalog only. Seller-specific catalogs (publisher's own
  list_creative_formats) deferred to the auto-negotiation surface.
- Param extraction is dimensions/duration only. Full canonical-specific
  params (slots, codecs, char limits, platform_extensions) deferred.
- Asset-slot translation (v1 assets[] → v2 slots[] via
  asset_group_vocabulary aliases) deferred — most adopter-relevant
  piece for actual creative submission flows; lands in follow-up.

Tests: 5 cases (every-catalog-entry projection, round-trip, registry
fallback, fail-closed, coverage report). Same skip-on-CI pattern as
v2-to-v1: skips with clear reason when 3.1 cache absent. v2 → v1
fixture loader now filters non-Product JSON files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

v1 → v2 projection landed (`c3a7d84d`)

Symmetric counterpart to v2 → v1. The 8.0 design's "V2-only public API" needs both directions; v1 → v2 is the practically more valuable one today because most inventory in the wild speaks v1, and this is what makes V2-written buyers see v1 sellers as V2.

Coverage: 57/57 catalog entries land in a precise bucket

Bucket Count What it means
Step 1 — NORMATIVE clean projection 32/57 (56%) Catalog entry has explicit `canonical:` annotation. Image, html5, display_tag, video_hosted, video_vast, audio_hosted, sponsored_placement.
`catalog_lacks_canonical_annotation` 25/57 (44%) AAO catalog knows the format but hasn't blessed a v2 canonical yet. Symmetric to `v1_translatable: false` on the v2 side. Native, DOOH, broadcast, generative, card-scaffolding.
`no_match` 0/57 Every catalog entry either projects cleanly or surfaces a precise reason.

Resolution order

  1. Catalog entry with `canonical:` annotation → use it (NORMATIVE).
  2. Catalog entry without `canonical:` → fail-closed with `catalog_lacks_canonical_annotation`. Don't shoehorn. The AAO's deliberate absence of annotation is the spec saying "no v2 form yet for this category" — same intent as `v1_translatable: false` on the v2 side.
  3. Not in catalog → registry glob match.
  4. No registry hit but catalog provides assets → structural match (only fires for non-catalog ids).
  5. Fail-closed with `no_match`.

Key design call: refused to shoehorn

First-pass let structural-match catch everything. Produced semantically-wrong projections — DOOH billboards → `display_tag`, native mentions → `display_tag`, AI-generative banners → `display_tag`. All because the registry's `url` structural entry matches any format with a tracking url.

The right call: when the AAO catalog explicitly knows about a format but hasn't annotated it, the SDK respects that silence. Fail-closed with a distinct diagnostic so buyers see "category not yet v2-mapped" rather than a wrong canonical that misroutes downstream creative submission flows.

This mirrors the spec's stance on `v1_translatable: false` (v2 → v1 direction): the spec is explicit about "no v1 form possible"; the SDK respects it. Now the v1 → v2 direction has the same honesty principle.

Scope (deliberate prototype gaps)

  • AAO catalog only. Seller-specific catalogs (publisher's own `list_creative_formats`) deferred to the auto-negotiation surface that PR docs(development): draft 8.0 design proposal for AdCP 3.1 support #1809 proposes.
  • Param extraction is dimensions + duration only. Full canonical-specific params (slots, codecs, char limits, platform_extensions) deferred.
  • Asset-slot translation deferred. v1 `assets[]` → v2 `slots[]` via the `asset_group_vocabulary` aliases is the most adopter-relevant piece for actual creative submission and lands in a follow-up. The projection here only produces `format_kind` + `params` + `v1_format_ref`; `slots` come from the canonical's base shape.

Symmetric story across both directions

Direction Clean projections Honest fail-closed What it enables
v2 → v1 7/13 (54%) 6/13 V2 buyers writing to v1-only sellers. Limited by what sellers annotate as `v1_format_ref`.
v1 → v2 32/57 (56%) 25/57 V2 buyers reading v1 sellers' existing inventory as V2. Limited by what the AAO catalog annotates.

Same pattern: high signal on the annotated subset, honest fail-closed for the un-annotated tail.

Upstream observation worth filing

The 25 un-annotated catalog entries cluster into 5 categories that all need v2-canonical decisions:

  • Generative variants (8 entries) — display_*_generative are parameterized AI-generation formats. Could be a new canonical `display_generative` or extend `html5` with a `generative: true` mode.
  • Native (3 entries: native_standard, native_content, native_mention) — native ads have their own structural shape. Worth a `native` canonical, or `native_mention` could project to `agent_placement`.
  • DOOH (4 entries: dooh_billboard_*, dooh_transit_screen) — out-of-home is structurally different from display. Needs a `dooh` canonical.
  • Broadcast (3 entries: broadcast_spot_15s/30s/60s) — broadcast TV could be `video_hosted` with a `channel: linear_tv` parameter, or its own canonical.
  • Card scaffolding (7 entries: product_card_, format_card_, proposal_card_*, native_product_card) — these aren't ad formats, they're UI scaffolding for browsing. Probably shouldn't be in `reference-formats.json` at all; maybe move to a separate UI-scaffolding registry.

Worth filing as five separate roadmap items for the canonicals catalog beyond 3.1.

Tests

30 cases across both projection directions. Skip-in-CI pattern with the clear reason marker — same as the v2 → v1 tests.

…2cfc5)

Upstream restructured the format-discovery model: registry shrank from
17 entries to 7 pure-structural fallbacks; literal globs moved into
per-publisher `adagents.json#/formats` declarations, with an AAO
community mirror tier at `mirror.adcontextprotocol.org/translated/`
for publishers who haven't adopted yet. Meta moved into the community
mirror with full Reels + Feed + Stories + WhatsApp coverage.

Catalog also grew 9 new `canonical:` annotations: 8 display_*_generative
→ image (parameterized AI generators that produce image output) plus
native_mention → agent_placement (matches the roadmap suggestion in
my prior PR comment).

SDK changes:
- test fixtures refreshed from latest spec PR HEAD
- meta_reels_us v1_format_ref test updated to the new community-mirror
  agent_url (mirror.adcontextprotocol.org/translated/meta)
- registry-literal-fallback test reframed as
  publisher-bespoke-fail-closed (the registry no longer has literal
  globs at 3.1 GA; the SDK's forwardLookupByGlob stays in place for
  forward-compat)

Projection code itself unchanged. The architectural shift (publisher-
scoped catalog, registry pure-structural) is transparent to the
projection layer at the prototype's scope:
- v2 → v1 uses v1_format_ref verbatim; the agent_url change to the
  community mirror is opaque.
- v1 → v2 reads catalog `canonical:` annotations; the source-of-truth
  shift from registry to publisher catalog is transparent.

The full 8.0 enablement still owes:
- Loading `adagents.json#/formats` (with property_id/property_tag
  scoping) for publishers we encounter.
- Resolving `capability_id` references between
  `placement.format_options[]` and `adagents.formats[]`.
- Fetching at the auto-negotiation boundary, not at projection time.

Coverage report post-reconciliation (full AAO catalog, 57 entries):
- Step 1 NORMATIVE clean projection: 41/57 (72%, up from 32/57)
- catalog_lacks_canonical_annotation: 16/57 (down from 25/57)
  Remaining: 3 broadcast_spot, 2 native_*, 4 dooh_*, 7 *_card_*
- no_match: 0/57

Tests: 30 pass across 9 suites in both directions. Skip-in-CI pattern
preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

Reconciled with publisher-scoped catalog (`9b312cc2`)

Upstream restructured the format-discovery model — registry shrank to 7 pure-structural fallbacks, literal globs moved into per-publisher `adagents.json#/formats` declarations, AAO community mirror tier at `mirror.adcontextprotocol.org/translated/` handles publishers who haven't adopted (Meta uses it now with full Reels + Feed + Stories + WhatsApp coverage).

Catalog grew 9 new annotations — including 2 that match my prior roadmap suggestions:

  • `display_*_generative` (×8) → `image` — parameterized AI generators that produce image output (vs my earlier `display_tag` shoehorning attempt, which was wrong)
  • `native_mention` → `agent_placement` — exact match for my proposal

Updated coverage report

Bucket Round 3 Round 4 (now)
Step 1 NORMATIVE clean 32/57 (56%) 41/57 (72%)
catalog_lacks_canonical_annotation 25/57 16/57
no_match 0/57 0/57

The remaining 16 un-annotated entries cluster into the 3 categories that still need v2 canonical decisions:

  • 3 broadcast_spot (15s/30s/60s) — broadcast linear TV
  • 2 native (`native_standard`, `native_content`)
  • 4 dooh (`dooh_billboard_*`, `dooh_transit_screen`)
  • 7 cards (`product_card_`, `format_card_`, `proposal_card_*`, `native_product_card`) — these are UI scaffolding, probably shouldn't be in the format catalog at all

SDK code unchanged

The architectural shift is transparent to the projection layer at the prototype's scope:

  • v2 → v1 uses `v1_format_ref` verbatim; the agent_url change to the community mirror is opaque to the SDK.
  • v1 → v2 reads catalog `canonical:` annotations; the source-of-truth shift from registry to publisher catalog doesn't change the SDK code path.

The literal-glob registry shrinkage means the SDK's `forwardLookupByGlob` never fires for catalog-known formats at 3.1 GA — but the function stays in place for forward-compat (the registry MAY grow literal entries again).

What the full 8.0 enablement still owes

The publisher-scoped catalog opens up real adopter value that the prototype doesn't yet deliver:

  1. Loading `adagents.json#/formats` for publishers we encounter (with `property_id`/`property_tag` scoping). Today we only load the AAO catalog.
  2. Resolving `capability_id` references between `placement.format_options[]` and `adagents.formats[]`.
  3. Fetching at the auto-negotiation boundary, not at projection time — projection should be sync; the AgentClient should populate the per-agent catalog cache on first contact.

These are real 8.0 work items, not projection-prototype scope. The projection layer is now structurally correct for the new world; the data sources just need to grow.

Tests

30 pass across 9 suites in both directions. Skip-in-CI pattern preserved. Meta Reels test updated to the community-mirror agent_url; registry-literal-fallback test reframed as publisher-bespoke-fail-closed.

Upstream added three orthogonal size modes to image/html5/display_tag
canonicals:
- Fixed: (width, height)
- Multi-size: sizes: [{width, height}, ...]  (mirrors OpenRTB
  banner.format[])
- Responsive: min_width/max_width/min_height/max_height

Lint enforces the three modes are mutually exclusive. The NYTimes
homepage fixture was rewritten as a flex-display product: one product
with 3 format_options (image / html5 / display_tag), each declaring
sizes: [300x250, 728x90, 970x250]. The single v1_format_ref points at
the 300x250 rep size — N-1 sizes are silently dropped on the v1 wire.

SDK changes:
- New SDK-local diagnostic FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE
  emitted ALONGSIDE the v1 format_id (not instead of) when a v2
  declaration uses multi-size or responsive params. Buyer sees the
  product on v1 + a structured warning about what coverage was lost.
- Structural invariant on tests relaxed from "exactly one of (emit,
  diagnostic) per declaration" to "at least one" — multi-size
  produces both.
- v2-to-v1 detection: only image/html5/display_tag (the canonicals
  that grew the new size modes); other canonicals carry their own
  param shapes that v1 format_id can express inline.

Upstream URL update:
- Meta community-mirror agent_url moved from
  mirror.adcontextprotocol.org/translated/meta to
  creative.adcontextprotocol.org/translated/meta (AAO consolidating
  community-tier hosting under their canonical domain).

Test updates:
- nytimes_homepage_mrec fixture now has product_id
  nytimes_homepage_flex_display with 3 format_options.
- New dedicated test for the multi-size lossy advisory.
- Removed nytimes_homepage_mrec from single-size happy-path list
  (it's now multi-size).
- nytimes_homepage_html5, gam_3p_display_tag, youtube_vast_preroll,
  meta_reels_us keep single-size happy-path coverage with zero
  diagnostics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 17, 2026
Lessons from the v1↔v2 projection prototype at PR #1815 land as a
new top-of-doc section, plus three subsections updated inline:

1. New "Lessons from the prototype" section captures the 8
   architectural refinements:
   - Catalog source-of-truth is 4-tier (AAO base + community mirror
     + adagents.json#/formats + per-placement format_options[]).
     The doc had treated it as 1-tier ("fetch from
     list_creative_formats").
   - Multi-size lossy advisory is a new diagnostic class emitted
     alongside the v1 emit, not instead of. Structural invariant
     relaxed.
   - Symmetric "not yet" buckets: v1_translatable: false (v2 side)
     and catalog_lacks_canonical_annotation (v1 side).
   - canonical_formats_only semantically overloaded.
   - Quantified coverage: 41/57 (72%) clean v1→v2; 5/5 single-size
     +7-emit multi-size v2→v1.
   - Registry reverse-lookup dormant at 3.1 GA (registry shrank to
     7 pure-structural fallbacks).
   - AAO convergence pattern resolved per-publisher format-id sprawl.
   - format_kind orthogonal to dimensional identity; affects
     codegen and adopter mental model.

2. Projection-layer section rewritten:
   - Documents the 4-tier catalog model with merge order (most-
     specific-wins).
   - Full resolution order for both v1→v2 and v2→v1 directions.
   - Diagnostic union expanded to include all 7 codes (3 spec, 4
     SDK-local) with prose for each.

3. V2-shaped Product table updated:
   - Single-size vs multi-size vs responsive vs canonical_formats_only
     vs v1_translatable: false outbound behavior all spelled out.
   - Multi-size fan-out documented as the productized path.

4. Migration path gains a "Coverage adopters can expect at 8.0 GA"
   subsection citing the prototype's coverage report.

Doc shape preserved (option C of the three options I offered):
Lessons section + sharpen affected subsections inline; no wholesale
rewrite of unaffected sections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a v2 declaration carries `sizes: [...]` and `v1_format_ref`,
the SDK now fans out via the AAO catalog: for each declared size,
look up an entry sharing the seller-asserted ref's
`<prefix>_WxH_<suffix>` template. v1 buyers see ALL the sizes the
v2 declaration covers, not just the rep — when the catalog has
the sibling per-size entries (image / html5 in 3.1 GA do;
display_tag's display_js doesn't).

The lossy advisory still fires with both
`declared_sizes_count` and `emitted_sizes_count` so the buyer
sees how many sizes actually made it onto the v1 wire vs how
many the v2 declaration covered.

Disambiguation: the catalog has multiple entries sharing the
same `canonical:` annotation but belonging to different families
(e.g., `display_300x250_image` and `display_300x250_generative`
both annotated `canonical: image`). Without a template filter
the fan-out would collide families. New `parseSizedIdTemplate`
+ `findCatalogEntryByCanonicalAndSize` with prefix/suffix
options preserves family identity.

The spec doesn't forbid this — the "SDKs MUST NOT synthesize
v1_format_ref" rule is about the registry, not catalog lookups.
This expansion is non-normative per the spec's direction-of-
truth statement; downstream consumers MUST NOT depend on it.
The lossy advisory is the normative surface.

Coverage with the new flex-display fixture:
- Old behavior: 3 v1 emits (one rep per format_option)
- New behavior: 7 v1 emits (image: 3, html5: 3, display_tag: 1)
  + 3 lossy advisories (each tagged with emitted_sizes_count)

Tests: 30 pass across 10 suites in both directions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 18, 2026
Lessons from the v1↔v2 projection prototype at PR #1815 land as a
new top-of-doc section, plus three subsections updated inline:

1. New "Lessons from the prototype" section captures the 8
   architectural refinements:
   - Catalog source-of-truth is 4-tier (AAO base + community mirror
     + adagents.json#/formats + per-placement format_options[]).
     The doc had treated it as 1-tier ("fetch from
     list_creative_formats").
   - Multi-size lossy advisory is a new diagnostic class emitted
     alongside the v1 emit, not instead of. Structural invariant
     relaxed.
   - Symmetric "not yet" buckets: v1_translatable: false (v2 side)
     and catalog_lacks_canonical_annotation (v1 side).
   - canonical_formats_only semantically overloaded.
   - Quantified coverage: 41/57 (72%) clean v1→v2; 5/5 single-size
     +7-emit multi-size v2→v1.
   - Registry reverse-lookup dormant at 3.1 GA (registry shrank to
     7 pure-structural fallbacks).
   - AAO convergence pattern resolved per-publisher format-id sprawl.
   - format_kind orthogonal to dimensional identity; affects
     codegen and adopter mental model.

2. Projection-layer section rewritten:
   - Documents the 4-tier catalog model with merge order (most-
     specific-wins).
   - Full resolution order for both v1→v2 and v2→v1 directions.
   - Diagnostic union expanded to include all 7 codes (3 spec, 4
     SDK-local) with prose for each.

3. V2-shaped Product table updated:
   - Single-size vs multi-size vs responsive vs canonical_formats_only
     vs v1_translatable: false outbound behavior all spelled out.
   - Multi-size fan-out documented as the productized path.

4. Migration path gains a "Coverage adopters can expect at 8.0 GA"
   subsection citing the prototype's coverage report.

Doc shape preserved (option C of the three options I offered):
Lessons section + sharpen affected subsections inline; no wholesale
rewrite of unaffected sections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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