feat(v2): prototype v2 → v1 Product projection layer#1815
Conversation
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>
Re-run after upstream landed all five asksUpstream What landed upstream:
Coverage post-upstream-fixes (13 spec fixtures):
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:
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>
Post-reconciliation: 7/13 project cleanly. Zero ambiguous, zero gaps.Upstream landed the registry↔catalog↔fixtures reconciliation (`03e10e83`):
Coverage shifted dramatically:
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 worksOf the 5 seller-asserted projections:
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`
What this leaves usThe v2 → v1 projection prototype is essentially complete for the 3.1 reference fixture set. The remaining work is:
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. |
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>
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
Resolution order
Key design call: refused to shoehornFirst-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)
Symmetric story across both directions
Same pattern: high signal on the annotated subset, honest fail-closed for the un-annotated tail. Upstream observation worth filingThe 25 un-annotated catalog entries cluster into 5 categories that all need v2-canonical decisions:
Worth filing as five separate roadmap items for the canonicals catalog beyond 3.1. Tests30 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>
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:
Updated coverage report
The remaining 16 un-annotated entries cluster into the 3 categories that still need v2 canonical decisions:
SDK code unchangedThe architectural shift is transparent to the projection layer at the prototype's scope:
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 owesThe publisher-scoped catalog opens up real adopter value that the prototype doesn't yet deliver:
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. Tests30 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>
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>
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>
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(withformat_options[]) and produces a v1Product(withformat_ids[]) suitable for sending to a v1-only seller. The resolution order mirrorsv1-canonical-mapping.json, inverted:format_kind: "custom"+canonical_formats_only: trueFORMAT_DECLARATION_V1_UNREACHABLEdiagnostic.v1_format_refpresent on the declarationformat_id.FORMAT_DECLARATION_V1_AMBIGUOUSdiagnostic.FORMAT_DECLARATION_V1_UNREACHABLEdiagnostic.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#3307static/examples/products/canonical/. The fixtures span 12 of the 13 canonical format_kinds.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:
v1_format_refto their declarations. The current spec reference fixtures don't model this — none of them carryv1_format_ref.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) synthesizesformat_id.id: "iab/mrec_300x250"— butformat-id.jsonrequires^[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 emitsreason: "no_v1_format_ref_or_registry_match"— but really, the spec could declare these canonicals as inherently v1-unreachable, the same waycanonical_formats_only: truedeclares 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.test/lib/v2-projection-fixtures/.Explicit non-scope:
fetchFormatSchemafor custom shapes — separate piece.Test plan
npm run format:checkclean (vendored fixtures added to.prettierignoreper the same rule aswebhook-signing-vectors/).format_ids.length + diagnostics.length === format_options.length.Reviewer focus
Treat this as a design-prototype PR, not a feature PR. The key questions:
FORMAT_DECLARATION_V1_UNREACHABLE,FORMAT_DECLARATION_V1_AMBIGUOUS) the right vocabulary, or should they unify?🤖 Generated with Claude Code