Repository navigation
Description
Summary
The spec asks SDKs to check that a product format declaration's params narrow every v1 format its v1_format_ref[] points at, and to report a conflict as FORMAT_DECLARATION_DIVERGENT. The SDK ships the check as adcp.canonical_formats.check_narrows / narrowing_advisory, but:
- Nothing in the projection path calls it.
project_declaration_to_v1(step 2, "v1_format_ref[]set — emit those refs") returns the seller's refs as they are, andproject_product_to_v1has no way to receive the referenced v1 formats'requirements. A seller or consumer using the projection helpers ships or accepts a divergent pairing with no advisory. check_narrowsreports no conflict for three of the pinnednarrowing_vectors(pixel density, range containment, exact value), so a caller that wires it in by hand still misses those conflicts.
Observed on adcp 9.3.0 (spec 3.2.7). I read src/adcp/canonical_formats/v2_to_v1.py and narrowing.py on main today and both still look the same.
What the spec says
All quotes are from tag v3.2.7 of adcontextprotocol/adcp.
static/schemas/source/registries/v1-canonical-mapping.json, resolution order step 1:
Authoritative v2→v1 link: if any v2
ProductFormatDeclarationon the same product carriesv1_format_refpointing at this v1 format_id, use that v2 declaration. Highest priority — seller asserts the link directly. SDKs SHOULD run the narrows check (canonical-formats.mdx 'Narrows — formal definition') between the v2 declaration'sparamsand the referenced v1 format'srequirements; on conflict, surfaceFORMAT_DECLARATION_DIVERGENTon theget_productsresponseerrors[]. Without the narrowing check,v1_format_refis a hint rather than a contract.
static/schemas/source/core/product-format-declaration.json, properties.v1_format_ref.description:
The v2 declaration's
paramsMUST narrow (be compatible with) each referenced v1 format'srequirements— see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (Product.format_ids[]⊇ entries fromv1_format_refANDProduct.format_options[]carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file'srequirements.
docs/creative/canonical-formats.mdx, § "Narrows" — formal definition (normative):
- Pixel-density constraints:
pixel_ratiosis an acceptance set. A v2 set narrows a v1 set when every v2 ratio appears in the v1 set. [...]- Range constraints: v2.range narrows v1.range when v2's lower bound ≥ v1's lower bound AND v2's upper bound ≤ v1's upper bound. [...]
- Conflict: any v2 parameter value that falls outside the corresponding v1 constraint is a conflict, not narrowing. SDKs MUST treat conflict between dual-emitted shapes as divergence and surface via
FORMAT_DECLARATION_DIVERGENT.SDK conformance for the specified scalar, enum/set, pixel-density, range, inheritance, open-baseline, and conflict cases MUST follow
narrowing_vectorsinstatic/test-vectors/v1-canonical-mapping.json.
Reproduction
Run check_narrows over the 11 narrowing_vectors in static/test-vectors/v1-canonical-mapping.json at v3.2.7. Each baseline is written as the v1 requirement it describes: exact becomes {field: value}, set becomes {field: values}, and range becomes {field: [min, max]} plus min_<field> / max_<field>, which is the form check_narrows reads.
import json
from adcp.canonical_formats import check_narrows
vectors = json.load(open("v1-canonical-mapping.json"))["narrowing_vectors"] # test-vectors file
def requirements(baseline):
out = {}
for field, c in baseline.items():
if c["kind"] == "exact":
out[field] = c["value"]
elif c["kind"] == "set":
out[field] = c["values"]
elif c["kind"] == "range":
out[field] = [c["min"], c["max"]]
out[f"min_{field}"] = c["min"]
out[f"max_{field}"] = c["max"]
return out
for v in vectors:
narrows = not check_narrows(v["v2_params"], requirements(v["v1_baseline"]))
print(v["id"], "expected", v["expected"]["narrows"], "got", narrows)Eight vectors agree. These three do not; each expects a conflict, and check_narrows returns []:
| Vector | v1 baseline | v2 params | Expected |
|---|---|---|---|
pixel-density-outside-v1-set-conflicts |
pixel_ratios set [1, 2] |
pixel_ratios: [1, 3] |
conflict on pixel_ratios |
range-escaping-v1-upper-bound-conflicts |
duration_ms_range [3000, 90000] |
duration_ms_range: [5000, 120000] |
conflict on duration_ms_range |
exact-v1-value-mismatch-conflicts |
width exact 300 |
width: 320 |
conflict on width |
Spelling the v1 side differently doesn't change these results. {"min_duration_ms": 3000, "max_duration_ms": 90000} against duration_ms_range: [5000, 120000] still returns [], and so does duration_ms_exact: 120000 against a [3000, 90000] range. The exact-width conflict is caught only when v1 spells it as min_width/max_width. The cause is the field tables in narrowing.py: pixel_ratios, width/height as exact values, and any *_range pair are in none of _MAX_FIELDS, _MIN_FIELDS, _ENUM_SUBSET_FIELDS or _EXACT_FIELDS, so the check skips them.
Suggested fix
- Run the narrowing check on the step-2 path. For example,
project_declaration_to_v1/project_product_to_v1could accept a lookup from av1_format_refentry to that v1 format'srequirements(built from a catalogue snapshot, for example), callnarrowing_advisoryfor each ref, and add the advisory next to the emitted refs. With no lookup, keep the current behaviour, but document that no check ran. - Extend
check_narrowsto cover pixel-density sets, range containment (*_rangeagainst a v1 range or a v1 min/max pair, and a v2 exact value against a v1 range), and exact-value equality forwidth/height/duration_ms_exact. - Add the pinned
narrowing_vectorsto the SDK's conformance tests, as the definition requires.
Context
The Prebid Sales Agent (a seller) runs its own narrows check before it derives format_ids from v1_format_ref. It leaves out a ref that its declaration doesn't narrow and reports FORMAT_DECLARATION_DIVERGENT with source: "producer". That covers the producer side of the rule. The SHOULD above is the SDK's side: consumers, and sellers that use project_product_to_v1, should get the same check from the SDK.
Activity
docs(rfcs): restate RFC 002 Parts 10 and 11 as the design built on in…
bokelley commentedon Nov 8, 2026
Done in Python #1758, forwarded by Python #1759: seller-asserted ref narrowing and the three missing conflict-vector families. TS counterpart uses its dual-declaration/narrowing runtime; TS #3642 is the separate public per-product API request.
Closed in a 3.3 triage pass. Reopen if anything here is still outstanding.
🤖 Generated with Claude Code