Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 164 additions & 3 deletions MIGRATION_v5_to_v6.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,167 @@ existing `pip install adcp` calls in production continue resolving to

## Per-release entries

(Add sections here as breaking changes land. Format: heading per
released beta increment, bullet list of breaking surfaces with
migration recipe.)
### Canonical-formats public surface (#741)

The v2 catalog-side canonical-formats vocabulary is now reachable from
`adcp.types`, and a new `adcp.canonical_formats` module ships the v2 →
v1 projection layer + closed-set `format_options[]` validator.

**New public-API names on `adcp.types`:**

- Discriminator + projection: `CanonicalFormatKind`,
`ProductFormatDeclaration`, `ProductFormatSellerPreference`,
`CanonicalProjectionReference`, `CanonicalAssetSource`,
`CanonicalSlotOverride`.
- 13 canonical format classes (one per `CanonicalFormatKind` value):
`CanonicalFormatImage`, `CanonicalFormatHtml5Banner`,
`CanonicalFormatDisplayTag`, `CanonicalFormatImageCarousel`,
`CanonicalFormatHostedVideo`, `CanonicalFormatVastVideo`,
`CanonicalFormatHostedAudio`, `CanonicalFormatDaastAudio`,
`CanonicalFormatNativeInFeed`, `CanonicalFormatResponsiveCreative`,
`CanonicalFormatAgentPlacement`, `CanonicalFormatSponsoredPlacement`
(the last two have shorter names than the codegen output —
`CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement` and
`CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven` are
collapsed).
- Pixel tracker asset: `PixelTrackerAsset`, `PixelTrackerEvent`,
`PixelTrackerMethod`.
- v1↔v2 registry types: `V1V2CanonicalFormatMappingRegistry`,
`V1CanonicalMapping`, `V1CanonicalGlobPattern`,
`V1CanonicalStructuralPattern`, `V1CanonicalStructural`,
`V1CanonicalV2Projection`, `V1CanonicalDimensions`.

**`ProductFormatDeclaration` is hand-rolled, not codegen.**
`datamodel-code-generator` flattens the upstream schema's discriminated
`oneOf` and drops the `format_kind` / `params` fields entirely. The
public class lives at `adcp.types.canonical_decl.ProductFormatDeclaration`
and carries both discriminator + open `params` dict with `extra='allow'`.
The codegen output is preserved under `adcp.types.canonical_decl._GeneratedProductFormatDeclaration`
for code that needs the raw shared-properties view.

**New module `adcp.canonical_formats`:**

```python
from adcp.canonical_formats import (
project_declaration_to_v1,
project_product_to_v1,
validate_format_kind_in_options,
find_declaration_by_kind,
load_default_registry,
FormatKindNotInClosedSetError,
)
```

- `project_declaration_to_v1(declaration, *, field_path, product_id)`
walks one `ProductFormatDeclaration` through the resolution order
documented in `registries/v1-canonical-mapping.json` and returns a
`V2ToV1Projection` carrying the projected `format_ids[]` plus any
SDK-source advisories the resolution emitted
(`FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE`,
`FORMAT_DECLARATION_V1_AMBIGUOUS`).
- `project_product_to_v1(product, *, product_index)` fans out across
the product's `format_options[]`, accumulating refs + advisories with
product-indexed field paths for multi-product responses.
- `validate_format_kind_in_options(format_kind, format_options)` is
the seller-side pre-call guard: raises
`FormatKindNotInClosedSetError` when the kind is outside the
product's published closed set. Sellers MUST pair this with
`UNSUPPORTED_FEATURE` on the wire response.
- `find_declaration_by_kind(format_kind, format_options, *, capability_id)`
looks up the matching declaration, disambiguating by `capability_id`
when the closed set carries multiple declarations of the same kind.

**Recipe — emit `format_ids[]` from `format_options[]`:**

```python
from adcp.canonical_formats import project_product_to_v1

projection = project_product_to_v1(product, product_index=i)
response.products[i].format_ids = projection.format_ids
response.errors = (response.errors or []) + projection.advisories
```

**Recipe — reject out-of-set `format_kind` at `create_media_buy`:**

```python
from adcp.canonical_formats import (
FormatKindNotInClosedSetError,
validate_format_kind_in_options,
)

try:
validate_format_kind_in_options(
manifest.format_kind,
product.format_options,
)
except FormatKindNotInClosedSetError as e:
return CreateMediaBuyErrorResponse(errors=[e.to_wire_error()])
```

The `e.to_wire_error()` helper builds the wire-correct
`UNSUPPORTED_FEATURE` `Error` with `details.rejected_value` +
`details.accepted_values` per the canonical rejection shape in
`error.json`. Override `field=` when the rejection isn't at the
default `manifest.format_kind` pointer.

**Recipe — seller-side v1 inbound lookup:**

When a v1-only buyer's `create_media_buy` arrives with a `format_id`
rather than a v2 `format_kind`, sellers walk the product's
`format_options[]` looking for the declaration that asserted that
v1 ref:

```python
from adcp.canonical_formats import find_declaration_by_v1_format_id

decl = find_declaration_by_v1_format_id(
manifest.format_id,
product.format_options,
)
if decl is None:
return CreateMediaBuyErrorResponse(errors=[Error(
code="UNSUPPORTED_FEATURE",
message="v1 format_id not in product format_options[]",
field="manifest.format_id",
)])
# Use decl.format_kind + decl.params_as(...) from here.
```

**Recipe — recover typed canonical body from `params`:**

```python
from adcp.types import CanonicalFormatImage, CanonicalFormatKind

# decl.params is dict[str, Any] for cross-kind compatibility — narrow
# it via params_as(...) once you've discriminated on format_kind.
if decl.format_kind is CanonicalFormatKind.image:
img = decl.params_as(CanonicalFormatImage)
for size in img.sizes:
... # typed: size.width, size.height
```

**Wire-shape enforcement landed in `ProductFormatDeclaration`:**

- `params` is now required (matches `required: ["format_kind", "params"]` on the schema).
- `canonical_formats_only=True` and `v1_format_ref[]` are rejected at
construction when combined (the schema's `allOf.not` clause).
- Credential-shaped keys in `params` or model extras raise at
construction. Same suffix list and rationale as the dispatcher's
`ctx_metadata` gate (`credential`, `token`, `secret`, `api_key`,
`apikey`, `password`, `bearer`).

**SDK-source advisory provenance:**

All advisories emitted by the projection layer carry `source="sdk"` and
`sdk_id="adcontextprotocol-adcp-python@<version>"`. The distribution-name
prefix is fixed (independent of `pyproject.toml`'s `[project].name`)
so wheel installs and dev installs emit the same attribution string —
the multi-hop `(code, field, sdk_id)` dedup contract in `core/error.json`
keys on this and would corrupt under drift. Adopters relying on a
particular `sdk_id` for multi-hop dedup should pin to a specific SDK
release rather than parsing the string.

**Not yet shipped (later beta increments):** v1 → v2 reverse projection,
`pixel_tracker` bidirectional contract, the 14 reference fixtures and
round-trip tests, `FORMAT_DECLARATION_DIVERGENT` narrowing check between
v2 `params` and the referenced v1 format's `requirements`.
2 changes: 1 addition & 1 deletion src/adcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,8 +869,8 @@ def get_adcp_version() -> str:
# Signal pricing types
"SignalPricingOption",
# Configuration types
"PushNotificationConfig",
"NotificationConfig",
"PushNotificationConfig",
"WholesaleFeedEvent",
"WholesaleFeedWebhook",
# Adagents validation
Expand Down
86 changes: 86 additions & 0 deletions src/adcp/canonical_formats/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Pythonic v1↔v2 canonical-formats projection layer.

AdCP 3.1 introduced canonical formats as the v2 catalog-side vocabulary
that replaces v1's per-publisher format proliferation. A seller publishes
``Product.format_options[]`` carrying ``ProductFormatDeclaration`` entries
(one per accepted canonical kind); buyer agents reason about creative
compatibility against the closed canonical set rather than the open v1
``format_ids`` namespace.

During the migration window both wire shapes coexist: 3.0-era buyers
read ``Product.format_ids[]`` (v1) and 3.1-aware buyers read
``Product.format_options[]`` (v2). This module supplies the projection
layer the SDK needs to bridge them without adopters hand-rolling
translation per integration.

Public surface
==============

* :func:`project_declaration_to_v1` — single ``ProductFormatDeclaration``
→ ``format_ids[]`` (with advisory ``errors[]`` emission on ambiguity
/ multi-size lossy fan-out).
* :func:`project_product_to_v1` — fan-out helper across a product's
``format_options[]``, accumulating refs and advisories.
* :func:`validate_format_kind_in_options` — closed-set guard: rejects a
``format_kind`` that isn't published in the seller's ``format_options[]``.
Sellers call this before accepting a ``create_media_buy``.
* :func:`find_declaration_by_kind` — looks up the matching declaration
(with optional ``capability_id`` disambiguation).
* :func:`load_default_registry` — loads the AAO-published v1↔v2 mapping
registry from the bundled schema cache.
* :class:`SdkAdvisory` — typed wrapper around the SDK-source ``Error``
entries the projection emits on ``errors[]``.

Resolution-order semantics for v2 → v1 follow ``registries/v1-canonical-mapping.json``:

1. ``canonical_formats_only=True`` or ``format_kind=custom`` → no v1 emit, no advisory.
2. ``v1_format_ref[]`` set → emit those refs; if ``params.sizes[]`` count exceeds
``v1_format_ref[]`` count, emit ``FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE``.
3. Canonical's ``v1_translatable=False`` (``agent_placement``, ``sponsored_placement``,
``responsive_creative``, ``image_carousel``) → no v1 emit, no advisory — the
canonical is structurally v1-unreachable by design.
4. Canonical's ``v1_translatable=True`` but no ``v1_format_ref[]`` → emit
``FORMAT_DECLARATION_V1_AMBIGUOUS``. SDKs MUST NOT synthesize a v1 ``format_id``
from registry structural matches; the registry is authoritative for v1→v2
projection only.
"""

from __future__ import annotations

from adcp.canonical_formats.advisory import SDK_ID, SdkAdvisory, make_sdk_advisory
from adcp.canonical_formats.format_options import (
FormatKindNotInClosedSetError,
find_declaration_by_kind,
find_declaration_by_v1_format_id,
validate_format_kind_in_options,
)
from adcp.canonical_formats.projection import (
V1_TRANSLATABLE,
V2ToV1Projection,
project_declaration_to_v1,
project_product_to_v1,
)
from adcp.canonical_formats.registry import (
RegistryLoadError,
glob_match,
load_default_registry,
structural_match,
)

__all__ = [
"FormatKindNotInClosedSetError",
"RegistryLoadError",
"SDK_ID",
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
"SdkAdvisory",
"V1_TRANSLATABLE",
"V2ToV1Projection",
"find_declaration_by_kind",
"find_declaration_by_v1_format_id",
"glob_match",
"load_default_registry",
"make_sdk_advisory",
"project_declaration_to_v1",
"project_product_to_v1",
"structural_match",
"validate_format_kind_in_options",
]
Loading
Loading