Skip to content

fix(canonical-formats): compare every parameter a format reference declares - #1518

Merged
bokelley merged 2 commits into
adcontextprotocol:mainfrom
KonstantinMirin:fix/canonical-format-params
Oct 22, 2026
Merged

bokelley merged 2 commits into
adcontextprotocol:mainfrom
KonstantinMirin:fix/canonical-format-params

Conversation

@KonstantinMirin

Copy link
Copy Markdown
Collaborator

Closes #1517.

core/format-id.json declares four parameters beyond the (agent_url, id) identity —
width, height, duration_ms and pixel_ratio. Three helpers in
adcp/canonical_formats/compat_helpers.py each named three of them in a hand-written
tuple, so none of them ever looked at pixel_ratio.

What was wrong

Measured on d5195a952, before this change:

from adcp.canonical_formats import (
    format_is_supported, formats_are_equivalent, upgrade_legacy_format_id,
)
from adcp.types.legacy import LegacyFormatId as FormatId

a = "https://creative.adcontextprotocol.org"
base = dict(agent_url=a, id="display_image", width=300, height=250)

format_is_supported(FormatId(**base), FormatId(**base, pixel_ratio=2))
# -> True    a product requiring 2x accepts a creative that claims no density

format_is_supported(FormatId(**base, pixel_ratio=3), FormatId(**base, pixel_ratio=2))
# -> True    and accepts one that claims a conflicting density

formats_are_equivalent(FormatId(**base, pixel_ratio=3), FormatId(**base, pixel_ratio=2))
# -> True    two references that explicitly disagree compare equal

legacy = FormatId(agent_url=a, id="display_300x250", width=300, height=250, pixel_ratio=2)
upgrade_legacy_format_id(legacy).pixel_ratio
# -> None    the legacy-size upgrade discards the density it was handed

The first line is the one with a delivery consequence: a seller publishing a format that
requires a 2x asset accepts a creative that never claims one, so a 1x image is served into
a 2x slot. The directional rule format_is_supported already implements correctly for the
other three parameters — if the supported side fixes a parameter, the request must state and
match it — simply never ran for this one.

What changed

The parameter set is now derived: set(FormatId.model_fields) - {"agent_url", "id"},
read once into _PARAMETER_FIELDS and walked by all three sites.

Deriving rather than appending a fourth name is the point of the change. An independent
server-side implementation of this same comparison hit the identical bug from its own
hand-written list of the same three names — two implementations, one list each, the same
parameter missing from both. A derived set means the next parameter added to
core/format-id.json is compared without anyone remembering to name it.

One rule stays per-field, because the schema gives it one. On pixel_ratio:

Omit for the backward-compatible 1x variant.

So an absent density is a value, not silence — unlike an absent width, which means
"template, unparameterized". _IMPLIED_WHEN_ABSENT carries that: a product fixed at 2x
refuses a request that says nothing, while one fixed at 1x still serves it. Without it,
closing the gap would have introduced the mirror-image false rejection, refusing every
buyer who uses the omitted form against a seller who spells pixel_ratio: 1 explicitly.

Relationship to #1364

#1364 was a different defect in format_is_supported — it refused the
FormatReferenceStructuredObject that generated model fields carry — and its fix landed on
2026-10-04. These gaps survived it untouched: the input coercion changed, the comparison
body did not.

Tests

tests/test_canonical_formats_compatibility.py gains five tests. Four of them are the
measured cases above; the fifth is the guard that would have caught the original bug:

@pytest.mark.parametrize("parameter", sorted(_CONFLICTING_VALUES))
def test_the_helpers_refuse_a_conflict_in_every_declared_parameter(parameter: str) -> None:

Its parameter list comes from get_named_schema_document("core/format-id.json"), not from
the model, so it fails when a helper stops comparing something the spec declares rather
than only when helper and model disagree. A companion test asserts the conflict table
covers every parameter the schema declares, so a new one cannot slip past the parametrize.

Red before the change, on the commit this branch is based on:

$ python -m pytest tests/test_canonical_formats_compatibility.py -q
FAILED ...::test_the_helpers_refuse_a_conflict_in_every_declared_parameter[pixel_ratio]
FAILED ...::test_format_is_supported_refuses_a_request_that_omits_a_required_pixel_ratio
FAILED ...::test_upgrade_legacy_format_id_keeps_every_parameter_it_was_given
3 failed, 20 passed in 10.99s

Green after:

$ python -m pytest tests/test_canonical_formats_compatibility.py -q
23 passed in 8.87s

test_format_is_supported_accepts_the_omitted_form_of_a_1x_product passes on both sides —
it passed vacuously before (the field was ignored), and it is here to hold the 1x rule
above in place, not as evidence of the fix.

Checks run locally

  • ruff check src/ — clean
  • black — clean on both changed files
  • mypy src/adcp/ — no issues in 1534 source files
  • mypy --strict tests/type_checks/ — no issues in 73 source files
  • scripts/run_pyright_type_checks.py — 0 errors over 62 fixtures
  • scripts/check_type_ignore_contract.py — clean
  • make validate-generated — generated types up to date
  • pytest -k 'format or canonical or compat or projection or creative' — 2953 passed, 310
    skipped, 0 failed

Noted, deliberately not included

_looks_like_legacy_format_tuple in adcp/types/canonical_creative.py carries the same
three-name list as a key-subset test, so sanitize_canonical_schema leaves agent_url in
properties and required for a node that also carries pixel_ratio. Its only caller is
pydantic schema generation, where the node always has a class-name title containing
"format" and the first branch matches — so there is a latent inconsistency but no
reachable consequence I could demonstrate, and no behavioral test to pin a change by. It is
described in #1517 for a separate look rather than patched speculatively here.

…clares

`core/format-id.json` declares four parameters beyond the `(agent_url, id)`
identity — `width`, `height`, `duration_ms` and `pixel_ratio`. Three helpers in
`compat_helpers.py` each named three of them in a hand-written tuple, so none of
them looked at `pixel_ratio`:

- `format_is_supported` accepted a creative that claimed no pixel density for a
  product requiring 2x, and accepted one claiming a conflicting density;
- `formats_are_equivalent` called two references with conflicting densities
  equal;
- `_upgrade_legacy_format_id` discarded the `pixel_ratio` it was handed.

Derive the set from the model's fields instead of listing it, so a parameter
added to the schema is compared without anyone remembering to name it. A fourth
literal would only reset the clock: an independent implementation of the same
comparison hit this bug from its own hand-written list of the same three names.

`pixel_ratio` keeps one deliberate per-field rule, because the schema gives it
one: omitting it selects "the backward-compatible 1x variant", so an absent
density reads as 1x rather than as unspecified. A product fixed at 2x therefore
refuses a silent request while one fixed at 1x still serves it — without that,
closing the gap would introduce the mirror-image false rejection.

The new parametrized test reads the parameter list from the bundled schema
rather than from the model, so it fails when a helper stops comparing something
the spec declares.

Closes adcontextprotocol#1517
Comment thread src/adcp/canonical_formats/compat_helpers.py

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean, well-tested fix with one non-blocking medium.

The change derives the format-parameter set from FormatId.model_fields and walks it at all three comparison sites, correctly closing the pixel_ratio gap (1x asset served into a 2x slot, conflicting densities compared equal, legacy-upgrade dropping the density). Commit prefix is correctly fix:; no public model shape changes, no import-layering violations, no generated-code hand-edits.

Medium findings

  • src/adcp/canonical_formats/compat_helpers.py:191 — Implied-1x rule applied only to the requested side of format_is_supported, so a supported format omitting pixel_ratio still accepts a 2x request (mirror of the fixed bug, untested).

Decision path: no critical/high findings (row 1 no); gated_paths false (row 2 no); high_risk false (rows 3/5 no); the single medium is not data-loss/schema/infra (row 4 no); no prior escalation (row 6 no); no no-auto-approve team match (row 7 no); only 1 medium, fewer than 3 (row 8 no) → row 9 approve. Worth a follow-up to apply the implied-1x rule symmetrically, but not blocking.

Medium findings

  • src/adcp/canonical_formats/compat_helpers.py:191 — Implied-1x rule applied only to requested side of format_is_supported

Comment thread src/adcp/canonical_formats/compat_helpers.py

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — subsequent pass, net improvement over prior review.

This PR derives the format parameter set from the model rather than a hand-written list, so pixel_ratio is now correctly compared in formats_are_equivalent, format_is_supported, and the legacy-size upgrade path. A schema-driven test guard fails when a new parameter goes uncovered — good regression protection.

Medium findings (1, non-blocking):

  • src/adcp/canonical_formats/compat_helpers.py:191 — Implied-1x rule applied only to the requested side of format_is_supported; a supported product omitting pixel_ratio (a 1x product per schema) fail-opens on a 2x request.

This single prior Medium persists but no new Critical/High surfaced. With only 1 medium finding, no blocking issues, no high-risk/gated-path/team-gate triggers, this falls through to row 9. Worth a follow-up to apply the implied-1x rule symmetrically on both sides, but not merge-blocking.

Medium findings

  • src/adcp/canonical_formats/compat_helpers.py:191 — Implied-1x rule applied only to requested side of format_is_supported

@bokelley
bokelley enabled auto-merge (squash) October 21, 2026 09:40
bokelley added a commit that referenced this pull request Oct 22, 2026
…quivalent too

`formats_are_equivalent` still treated an omitted `pixel_ratio` as
unspecified, so it called 300x250 and 300x250 at 2x the same format while
`format_is_supported` refused one for the other. An omitted width or duration
names the template, a family covering every variant, so "unspecified" is
right for those. An omitted density on a sized reference names one variant —
the 1x render of a 300x250 image, not the 600x500 one — so read it as 1x here
as well. A size-less template still has no density and stays broad.

The schema sanitizer's legacy-tuple shape check listed the reference's
properties by hand and missed `pixel_ratio`; derive the set from the model.

Refs #1518, #1517

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@bokelley bokelley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Independently reviewed at 7e6e706. The parameter set derived from FormatId.model_fields is exactly duration_ms/height/pixel_ratio/width. The new tests fail against main's helper (3 of 23). 281 format tests and 111 public-API/layering/canonical tests pass, and ruff and mypy are clean. Int vs float and implied-1.0 comparisons behave correctly, and the legacy upgrade keeps density.

The non-blocking items will land in a follow-up PR rather than here:

  • the implied-1x rule on the supported side of format_is_supported (only when that side declares width/height, so size-less templates keep accepting any density)
  • whether formats_are_equivalent should follow the same rule, since the new test currently asserts omitted ≡ 2x
  • the hard-coded parameter names in _looks_like_legacy_format_tuple (canonical_creative.py:549)

@KonstantinMirin

Copy link
Copy Markdown
Collaborator Author

@bokelley auto-merge can't complete here. Every required check passes and every thread is resolved, but the ipr policy ruleset requires CodeQL results, and CodeQL has no analysis for this head. It also has none for #1522, which I pushed from the fork yesterday, while #1540 and #1546 have one. A direct merge works, as it did for #1539.

@bokelley
bokelley merged commit d073712 into adcontextprotocol:main Oct 22, 2026
41 checks passed
bokelley added a commit that referenced this pull request Oct 22, 2026
…orted side too

`format_is_supported` applied the implied-1x rule only to the requested
reference. A supported 300x250 that omits `pixel_ratio` was skipped as
"unfixed", so it accepted a 2x creative — the mirror image of the gap #1518
closed for a 2x product receiving a request that omits the field.

`core/format-id.json` describes the omitted field as "the backward-compatible
1x variant" without naming a side, so read it the same way on both. The
reading applies only to a dimensioned reference: `pixel_ratio` "Requires
`width` and `height`", so a bare `display_image` template names no variant to
be 1x of and stays broad. A legacy `display_300x250` upgrades to a
dimensioned reference and is therefore the 1x variant.

`formats_are_equivalent` keeps its documented loose contract (an omitted
parameter is unspecified on either side), which is already symmetric.

Refs #1518, #1517

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Oct 22, 2026
…quivalent too

`formats_are_equivalent` still treated an omitted `pixel_ratio` as
unspecified, so it called 300x250 and 300x250 at 2x the same format while
`format_is_supported` refused one for the other. An omitted width or duration
names the template, a family covering every variant, so "unspecified" is
right for those. An omitted density on a sized reference names one variant —
the 1x render of a 300x250 image, not the 600x500 one — so read it as 1x here
as well. A size-less template still has no density and stays broad.

The schema sanitizer's legacy-tuple shape check listed the reference's
properties by hand and missed `pixel_ratio`; derive the set from the model.

Refs #1518, #1517

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Oct 23, 2026
…orted side too (#1553)

* fix(canonical-formats): read an omitted pixel_ratio as 1x on the supported side too

`format_is_supported` applied the implied-1x rule only to the requested
reference. A supported 300x250 that omits `pixel_ratio` was skipped as
"unfixed", so it accepted a 2x creative — the mirror image of the gap #1518
closed for a 2x product receiving a request that omits the field.

`core/format-id.json` describes the omitted field as "the backward-compatible
1x variant" without naming a side, so read it the same way on both. The
reading applies only to a dimensioned reference: `pixel_ratio` "Requires
`width` and `height`", so a bare `display_image` template names no variant to
be 1x of and stays broad. A legacy `display_300x250` upgrades to a
dimensioned reference and is therefore the 1x variant.

`formats_are_equivalent` keeps its documented loose contract (an omitted
parameter is unspecified on either side), which is already symmetric.

Refs #1518, #1517

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(canonical-formats): apply implied-1x pixel_ratio in formats_are_equivalent too

`formats_are_equivalent` still treated an omitted `pixel_ratio` as
unspecified, so it called 300x250 and 300x250 at 2x the same format while
`format_is_supported` refused one for the other. An omitted width or duration
names the template, a family covering every variant, so "unspecified" is
right for those. An omitted density on a sized reference names one variant —
the 1x render of a 300x250 image, not the 600x500 one — so read it as 1x here
as well. A size-less template still has no density and stays broad.

The schema sanitizer's legacy-tuple shape check listed the reference's
properties by hand and missed `pixel_ratio`; derive the set from the model.

Refs #1518, #1517

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (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.

The canonical format helpers compare three of the four parameters a format reference declares

2 participants