Skip to content

canonical_formats: the narrows check is not run on a seller-asserted v1_format_ref, and check_narrows misses three pinned conflict vectors #1749

Closed

Description

@KonstantinMirin

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:

  1. 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, and project_product_to_v1 has 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.
  2. check_narrows reports no conflict for three of the pinned narrowing_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 ProductFormatDeclaration on the same product carries v1_format_ref pointing 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's params and the referenced v1 format's requirements; on conflict, surface FORMAT_DECLARATION_DIVERGENT on the get_products response errors[]. Without the narrowing check, v1_format_ref is a hint rather than a contract.

static/schemas/source/core/product-format-declaration.json, properties.v1_format_ref.description:

The v2 declaration's params MUST narrow (be compatible with) each referenced v1 format's requirements — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (Product.format_ids[] ⊇ entries from v1_format_ref AND Product.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's requirements.

docs/creative/canonical-formats.mdx, § "Narrows" — formal definition (normative):

  • Pixel-density constraints: pixel_ratios is 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_vectors in static/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_v1 could accept a lookup from a v1_format_ref entry to that v1 format's requirements (built from a catalogue snapshot, for example), call narrowing_advisory for 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_narrows to cover pixel-density sets, range containment (*_range against a v1 range or a v1 min/max pair, and a v2 exact value against a v1 range), and exact-value equality for width / height / duration_ms_exact.
  • Add the pinned narrowing_vectors to 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

  1. bokelley commented on Nov 8, 2026

    @bokelley
    Contributor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions