feat(canonical-formats): v1↔v2 reverse + pixel_tracker + narrowing (#741, half 2 of 2)#845
Conversation
…arrowing (#741, half 2) Closes #741. Lands the v1 → v2 inbound projection, the bidirectional ``pixel_tracker`` contract, the ``FORMAT_DECLARATION_DIVERGENT`` narrowing check, and the upstream reference fixtures + round-trip tests. ## v1 → v2 projection (``v1_to_v2.py``) ``project_v1_format_to_declaration`` walks the 4-step resolution order from ``registries/v1-canonical-mapping.json``: 1. Seller-asserted ``canonical:`` annotation on the v1 file → use declared kind; thread ``asset_source`` and ``slots_override[]`` into the emitted declaration's ``params``. 2. Registry ``format_id_glob`` lookup. 3. Registry ``structural`` match → emit ``FORMAT_DECLARATION_V1_AMBIGUOUS`` (family-level guess) but still produce a usable declaration. 4. Fail closed → emit ``FORMAT_PROJECTION_FAILED``, no declaration. Emitted declarations always carry ``v1_format_ref`` pointing back at the source v1 format_id so the half-1 v2 → v1 path round-trips. ``project_v1_catalog_to_v2`` is the bulk helper. ## Bidirectional ``pixel_tracker`` (``pixel_tracker.py``) Implements the normative downgrade/upgrade table from ``core/assets/pixel-tracker-asset.json``: * ``downgrade_pixel_tracker`` (v2 → v1) collapses 7 event × 2 method combos onto the v1 ``{asset_type: url, url_type: tracker_pixel}`` shape keyed on ``impression_tracker`` / ``viewability_tracker`` / ``click_tracker`` slot ids. Lossy on viewability variants, custom events, and any JS method — those emit ``PIXEL_TRACKER_LOSSY_DOWNGRADE``. * ``upgrade_v1_tracker`` (v1 → v2) inverts via asset_id convention. ALWAYS emits ``PIXEL_TRACKER_UPGRADE_INFERRED``. Batch helpers deduplicate advisories. ## ``FORMAT_DECLARATION_DIVERGENT`` narrowing (``narrowing.py``) ``check_narrows(v2_params, v1_requirements)`` returns divergence records covering four kinds: exceeds-max, below-min, not-subset (enum), not-equal (exact scalar). ``narrowing_advisory`` wraps in the wire-correct ``FORMAT_DECLARATION_DIVERGENT`` ``Error``. ## Vendored fixtures * 14 v2 ``Product`` fixtures from ``adcontextprotocol/adcp@main/static/examples/products/canonical/``. * 50-entry v1 reference catalog from ``adcontextprotocol/adcp@main/server/src/creative-agent/reference-formats.json``. ## Round-trip tests * Every v2 product fixture → ``project_product_to_v1`` emits format_ids that round-trip back to source declarations via ``find_declaration_by_v1_format_id``. * Every v2 product fixture → ``format_options[]`` parseable as the hand-rolled ``ProductFormatDeclaration``. * v1 reference catalog → 50 declarations, zero advisories. * v1 catalog covers 8 distinct ``CanonicalFormatKind`` values (pinned). ## Tests 5190 passed locally (5117 from half 1 + 73 new across 4 new files). Closes #741.
There was a problem hiding this comment.
Request changes. ad-tech-protocol-expert returned unsound with cited wire-shape divergence on FORMAT_DECLARATION_V1_AMBIGUOUS direction-of-use and a SHOULD-NOT batch-dedup violation; code-reviewer separately flagged a Major in _v1_version_constraints that breaks the documented Pydantic-input contract. The downgrade table, upgrade defaults, narrowing semantics, and v1_format_ref shape are all sound — the unsoundness is concentrated in advisory emission and one duck-typing gap.
MUST FIX
-
FORMAT_DECLARATION_V1_AMBIGUOUSis being emitted on the wrong projection direction.src/adcp/canonical_formats/v1_to_v2.py:307-330raises this code when the v1→v2 inbound path hits a family-level structural match. Perschemas/cache/3.1.0-beta.3/enums/error-code.json, the code describes the v2→v1 reverse direction — "an SDK detects that a product's v2 declaration cannot be unambiguously projected back to a single v1 named format." On the inbound side a family-level structural match is the expected 3.1 outcome (the registry ships 7 pure-structural entries, zero literals — see theInitial scope (3.1)note inregistries/v1-canonical-mapping.json). What breaks: every v1 format in a 3.1 catalog without acanonical:annotation produces a spurious AMBIGUOUS advisory advising the seller to add an annotation they don't need on the inbound direction, and buyer-side diagnostic tooling that keys on this code being a v2→v1 problem misclassifies. Either drop the advisory on the structural-match branch or surface a v1→v2-direction code. The half-1 reverse path already uses AMBIGUOUS correctly — don't overload it here. -
_v1_version_constraintssilently returns[]for PydanticFormatinputs.src/adcp/canonical_formats/v1_to_v2.py:160-184gates onif isinstance(v1_format, dict) else Noneand returns immediately. The module docstring andproject_v1_format_to_declarationdocstring both promise duck-typing across "raw dict, a PydanticFormat, or a duck-typed object." Sibling helpers (_v1_format_id:75,_v1_canonical_annotation:99,_v1_asset_types:123) all handle both shapes — this one diverges. What breaks: a Pydantic-typed VAST 4.x format with nocanonical:annotation falls through to step 5 fail-closed instead of matching thevideo_vaststructural entry. Tests don't catch it because everytests/test_canonical_formats_v1_to_v2.pyinput is a raw dict and the vendored 50-entry catalog all exits at step 1. Mirror the dict-or-getattr pattern used by the sibling helpers. -
Batch
downgrade_pixel_trackersdedup violates the spec's SHOULD-NOT.src/adcp/canonical_formats/pixel_tracker.py:336-350deduplicates on(code, source_event, source_method). ThePIXEL_TRACKER_LOSSY_DOWNGRADEmetadata incore/assets/pixel-tracker-asset.jsonis explicit: "One advisory per downgraded asset; SDKs SHOULD NOT collapse multiple downgrades into a single advisory entry — per-asset details let the buyer's measurement-plan owner decide whether each loss is tolerable." The collapsed shape defeats that contract. Drop the dedup on the downgrade side. The upgrade-side dedup at L367-379 isn't spec-prohibited but should probably match for consistency.
Follow-ups (non-blocking — file as issues)
- Advisory
detailskey drift from the spec's SHOULD shapes. Spec keys (format_id,original_event,original_method,original_custom_event_name,lost_fields,downgrade_target: \"url+tracker_pixel\",inferred_event,inferred_method,asset_id,product_id) vs the SDK's invented keys (v1_format_id,source_event,source_method,source_custom_event_name,lost, missingdowngrade_target,source_asset_id, noproduct_id). SHOULD-level so wire-conformant either way, but buyer-side classifiers key on these names — mechanical rename closes the gap. inference_basis=\"fallback_custom_event\"atpixel_tracker.py:271isn't in the spec enum{\"asset_id_convention\", \"default\"}. Restrict to one of the two enumerated literals.- Structural-match call never threads width/height.
v1_to_v2.py:289-294doesn't extract dimensions from the v1 format. Latent today (registry has zerodimensions-constrained entries) — silent miss the moment a future registry entry adds one. load_default_registry()called once per format inproject_v1_catalog_to_v2. Each call deep-copies the cached registry. Hoist the load to the catalog wrapper._v1_to_v2module docstring numbers its steps as 1-5 mirroringregistries/v1-canonical-mapping.jsonbut the registry's normative order has 6 steps with step 1 being the cross-productv1_format_reflink. Either declare step 1 out of scope explicitly or renumber.
Minor nits (non-blocking)
narrowing.py:148-149— fallback short-circuits when v2 declares both forms. If v2 carriesmax_width=350ANDwidth=400against v1'smax_width=300, only the 350 is checked because theif v2_value is Nonefallback gates the bare-form lookup. 400>300 goes undetected. Check both forms or document the contract.narrowing.py:103-109—_is_subsetdoesset(v1)/set(v2)which raisesTypeErrorif a list-of-dicts slips through.v1_requirementsisdict[str, Any]so the shape isn't guaranteed. Catch and treat asnot_subset.pixel_tracker.py:184—hasattr(pixel, \"custom_event_name\")is always True for a generated Pydantic model with the field declared. Drop the guard.pixel_tracker.py:213—event.value if event else 'impression'!remitsevent='impression'when_coerce_eventreturned None on invalid input. Misleading; echo the raw or label as<invalid>.
Verdict ladder: changes requested on items 1-3 above. Items 1 and 3 are direct spec divergences with a concrete adopter-visible failure mode; item 2 breaks a documented duck-typing contract. The architecture is right — three modules, single resolution-order walker, advisories typed via make_sdk_advisory — the bugs are localized.
8 must-fix items from code-reviewer / ad-tech-protocol-expert /
security-reviewer on the half-2 PR.
## NORMATIVE
- **Narrowing field gap.** ``_MAX_FIELDS`` / ``_MIN_FIELDS`` /
``_EXACT_FIELDS`` were missing required fields the canonical-format
schemas declare: ``max_file_size_mb``, ``max_bitrate_kbps`` /
``min_bitrate_kbps``, ``max_wrapper_depth``, ``max_cpu_load_percent``,
``max_response_time_ms``, and singular ``vast_version`` /
``daast_version``. Without these, real video/audio/html5 v1↔v2
pairings with divergent declarations silently pass.
- **Step-1 registry-params bypass.** ``project_v1_format_to_declaration``
step 1 used to drop the registry's ``parameters`` when a seller
annotated only ``kind``. Now step 1 looks up the matching registry
glob first and threads its params; the seller annotation wins on
``kind`` / ``slots_override`` / ``asset_source`` but registry
defaults fill in everything else. Docstring step numbers re-aligned
to the registry's normative numbering.
## SECURITY
- **Seller-controlled strings echoed without scrubbing.** Three half-2
paths now use the half-1 ``_echo_identifier`` (128-char cap + control-
char escape):
- ``upgrade_v1_tracker`` ``source_asset_id`` + message + fallback
``inferred_custom_event_name``
- ``downgrade_pixel_tracker`` ``source_custom_event_name``
- ``narrowing_advisory`` ``v1_format_id`` (message + details)
- **Unbounded list echo in narrowing.** Capped at
``_ECHO_SET_CAP=32`` with ``"…N more"`` marker.
- **URL scheme gate on pixel-tracker upgrade.** ``upgrade_v1_tracker``
restricted to ``{"http","https"}``; rejected URLs produce
``pixel_tracker=None`` + advisory carrying the scheme. Batch
helper drops rejected entries from ``items``.
## CORRECTNESS
- Bare ``except Exception`` narrowed to ``pydantic.ValidationError``.
- Downgrade dedup key extended to include ``source_custom_event_name``
so distinct custom events don't collapse.
- ``bool`` excluded from numeric guard (``True == 1`` corruption).
- ``_is_subset`` tolerates unhashable elements via ``in``-based
containment fallback.
## ADOPTER ERGONOMICS
- ``quiet_inference`` flag on ``upgrade_v1_trackers`` suppresses
asset_id-convention advisories for high-volume buyer adopters.
Rejections and fallback custom events still fire.
- ``V1Tracker`` renamed to ``V1UrlTracker`` to dodge cognitive
collision with registry's ``V1Pattern``.
- ``tests/fixtures/canonical/VENDOR.md`` documents upstream source
paths + refresh procedure.
## TESTS
5219 passed locally (5190 from initial + 29 new across the three
new modules).
Deferred per Argus framing:
- ``projection.py`` → ``v2_to_v1.py`` rename (cosmetic).
- Typed ``Divergence`` dataclass (adopter-tuning).
- ``adcp.canonical_formats.fixtures`` public module (adopter-tuning).
Refs: #741, #845
There was a problem hiding this comment.
Approving on the strength of the fix-commit's response to prior review plus the clean expert verdicts. The right architectural call: half-2 lands the inbound projection alongside the half-1 outbound path, the bidirectional pixel_tracker contract closes the asset round-trip, and the resolution order is documented at module level so future contributors don't have to reverse-engineer it.
Things I checked
- Public-API surface — 14 new exports on
adcp.canonical_formats(verified againstsrc/adcp/canonical_formats/__init__.pyL87-117). Additive only,feat:prefix correct, no semver signal needed. _echo_identifiercoverage on every seller-controlled echo path:pixel_tracker.pyL219, L305, L311, L358, L364, L369;narrowing.pyL293, L304;v1_to_v2.pyL350, L376._echo_setcap=32 on the only unbounded list echo (narrowing.pyL243-244).security-reviewer: clean with one Low defense-in-depth note (see Follow-up 4).- URL scheme allowlist on
upgrade_v1_trackerrejectsjavascript:/file:/data:/ftp:/vbscript:/"". Batch helper drops rejected entries fromitems(pixel_tracker.pyL465-466) — not just the advisory. - Advisory codes (
FORMAT_DECLARATION_V1_AMBIGUOUS,FORMAT_PROJECTION_FAILED,PIXEL_TRACKER_LOSSY_DOWNGRADE,PIXEL_TRACKER_UPGRADE_INFERRED,FORMAT_DECLARATION_DIVERGENT) all upstream-registered inschemas/cache/3.1.0-beta.3/enums/error-code.jsonL79-85.source=sdk+sdk_idper the multi-hop dedup contract. pixel_tracker.pydowngrade/upgrade tables matchcore/assets/pixel-tracker-asset.jsonbyte-for-byte.viewable_mrc_50default on the viewability upgrade is spec-prescribed, not a coin flip._is_numericbool exclusion (narrowing.pyL125) —boolis the only int subclass that would corrupt comparisons; no other path uses rawisinstance(_, (int, float))on seller values.ad-tech-protocol-expert: sound-with-caveats (caveats in Follow-ups).code-reviewer: no blockers.
Follow-ups (non-blocking — file as issues)
-
Narrowing field-list coverage gap (
narrowing.pyL52-92). The fixup commit addedmax_file_size_mb,max_bitrate_kbps,max_wrapper_depth, etc. — but missed the carousel/responsive/native parameter surface:max_cards,min_cards,max_items,min_items,max_chars,max_size_kb,max_image_file_size_kb, and the flatheadline_max_chars/description_max_chars/card_*_max_charsfamily onresponsive_creative.json. Same class of bug the fixup was supposed to close — real v1↔v2 carousel/native pairings with divergent per-card caps will silently pass. Conversely_MAX_FIELDS/_MIN_FIELDSshipmax_dpi/min_dpiand_ENUM_SUBSET_FIELDSships pluralvast_versions/daast_versionswhich don't appear anywhere in the 3.1.0-beta.3 canonical schemas — harmless (v1.get(field)returns None and the check skips) but odd. Worth filing. -
Step-1 annotation/registry-params precedence inversion (
v1_to_v2.pyL212-218, L286-295). Docstring says "seller annotation wins on kind/slots_override/asset_source but registry defaults fill in everything else." The merge logic does the opposite forasset_sourceandslots—_build_declarationonly threads the annotation's values when the key is NOT already inbody(which was pre-populated fromregistry_params). Latent today (3.1 ships zero literal globs) but will fire silently the moment a literal glob lands. -
MIGRATION_v5_to_v6.md L223 still says
V1Tracker. The fixup commit renamed it toV1UrlTracker(rationale: dodge cognitive collision withV1Pattern). Copy-paste of the migration doc's import block willImportError. One-line fix. -
Defense-in-depth: cap
fid.idecho in advisory messages (v1_to_v2.pyL342, L370).FormatId.idis regex-constrained to^[a-zA-Z0-9_-]+$so_echo_identifier's control-char scrub is structurally unnecessary, but there's nomax_lengthonFormatId.idand nomax_lengthonError.message— a seller publishing a 100KB alphanumeric id balloons every emitted advisory._echo_identifier(fid.id)in the message f-string closes it. Same for theasset_typesecho at L378 (per-element from raw dict input, no bound). -
_v1_canonical_annotationsilently swallowsValidationError(v1_to_v2.pyL124-130). A seller typo (kind: \"imag\"instead of\"image\") degrades to a structural-ambiguous advisory instead of surfacing as aFORMAT_PROJECTION_FAILEDwith the validation error in details. Sellers won't know their annotation didn't take effect. -
Round-trip test parametrizes over all 14 fixtures but is vacuously true on fixtures without
v1_format_ref.tests/test_canonical_formats_roundtrip.pyL98-145 —expected_refsbecomes[]and matchesresult.format_idstrivially for v1-untranslatable canonicals. Either split the parametrize or assert at least N fixtures contributed non-empty refs. -
tests/fixtures/canonical/VENDOR.mddoesn't pin upstream SHAs. For a 14-fixture + 50-entry conformance corpus, omitting the source commit SHA means upstream drift is invisible until a test happens to fail.
Minor nits (non-blocking)
-
pixel_tracker.pyL418 dedup-key fallback is unreachable.source_methodis always set on L212 —details.get(\"source_method\", \"img\")never hits the default. Harmless. -
v1_to_v2.pydocstring numbers steps 1-4 but L364 says "Step 5: fail closed". The fixup commit re-aligned to the registry's numbering — the inline comment didn't move with it.
The third drift-cleanup commit in a row in this area still didn't catch the V1Tracker → V1UrlTracker rename in the migration doc. Worth a grep pass on rename PRs going forward.
Safe to merge.
…ixtures module, group helper, version fwd-compat Bundles the deferred follow-ups from #845's expert review. Five small changes; no behavioral surprise. ## B — projection.py → v2_to_v1.py Symmetric with v1_to_v2.py. ``git mv`` preserves history. Imports continue to work via the package re-export. Adopters who reached into the private path ``from adcp.canonical_formats.projection`` must switch to ``from adcp.canonical_formats.v2_to_v1``. ## C — Typed Divergence dataclass ``check_narrows`` returns ``list[Divergence]`` instead of ``list[dict[str, Any]]``. ``Divergence.to_dict()`` preserves the original advisory ``details.divergences`` key vocabulary (``v1_max`` / ``v1_min`` / ``v1_allowed`` / ``v1_value``) so wire parsers aren't affected. Adopters calling ``check_narrows`` and indexing ``d["field"]`` must switch to ``d.field``. ## D — adcp.canonical_formats.fixtures public module 14 v2 + 50 v1 fixtures bundled under ``src/adcp/canonical_formats/_fixtures/`` and exposed via ``load_reference_product(name)``, ``load_v1_reference_catalog()``, ``REFERENCE_PRODUCT_NAMES``. Adopters reuse without re-vendoring. ## E — group_declarations_by_product After ``project_v1_catalog_to_v2``, buckets declarations into per- product ``format_options[]`` lists given a ``{product_id: [v1_format_id, ...]}`` mapping. Bucket key is the first ``v1_format_ref`` (matches ``find_declaration_by_v1_format_id`` semantics). ## F — _versions_overlap forward-compat Unknown DSL operator prefixes (``~>``, ``^``) log WARNING and treat as non-matching, rather than raising. Registry MAY publish operators ahead of SDK support; crashing cached sessions is worse than missing a match. ## Tests 5271 pass. New ``test_canonical_formats_fixtures.py`` covers the public loader. ``test_canonical_formats_v1_to_v2.py`` extended for ``group_declarations_by_product``. Narrowing + registry tests updated for typed Divergence and forward-compat tone. Refs: #741, #845
…ixtures module, group helper, version fwd-compat (#849) Bundles the deferred follow-ups from #845's expert review. Five small changes; no behavioral surprise. ## B — projection.py → v2_to_v1.py Symmetric with v1_to_v2.py. ``git mv`` preserves history. Imports continue to work via the package re-export. Adopters who reached into the private path ``from adcp.canonical_formats.projection`` must switch to ``from adcp.canonical_formats.v2_to_v1``. ## C — Typed Divergence dataclass ``check_narrows`` returns ``list[Divergence]`` instead of ``list[dict[str, Any]]``. ``Divergence.to_dict()`` preserves the original advisory ``details.divergences`` key vocabulary (``v1_max`` / ``v1_min`` / ``v1_allowed`` / ``v1_value``) so wire parsers aren't affected. Adopters calling ``check_narrows`` and indexing ``d["field"]`` must switch to ``d.field``. ## D — adcp.canonical_formats.fixtures public module 14 v2 + 50 v1 fixtures bundled under ``src/adcp/canonical_formats/_fixtures/`` and exposed via ``load_reference_product(name)``, ``load_v1_reference_catalog()``, ``REFERENCE_PRODUCT_NAMES``. Adopters reuse without re-vendoring. ## E — group_declarations_by_product After ``project_v1_catalog_to_v2``, buckets declarations into per- product ``format_options[]`` lists given a ``{product_id: [v1_format_id, ...]}`` mapping. Bucket key is the first ``v1_format_ref`` (matches ``find_declaration_by_v1_format_id`` semantics). ## F — _versions_overlap forward-compat Unknown DSL operator prefixes (``~>``, ``^``) log WARNING and treat as non-matching, rather than raising. Registry MAY publish operators ahead of SDK support; crashing cached sessions is worse than missing a match. ## Tests 5271 pass. New ``test_canonical_formats_fixtures.py`` covers the public loader. ``test_canonical_formats_v1_to_v2.py`` extended for ``group_declarations_by_product``. Narrowing + registry tests updated for typed Divergence and forward-compat tone. Refs: #741, #845
Summary
Closes #741. Second half of the canonical-formats projection layer — lands the v1 → v2 inbound projection, the bidirectional
pixel_trackercontract, theFORMAT_DECLARATION_DIVERGENTnarrowing check, and 65 upstream reference fixtures + round-trip tests.v1 → v2 inbound projection (
adcp/canonical_formats/v1_to_v2.py)project_v1_format_to_declaration(v1_format)walks the 4-step resolution order fromregistries/v1-canonical-mapping.json:canonical:annotation on the v1 file → use declared kind; threadasset_sourceandslots_override[]into the emitted declaration'sparams.format_id_globlookup — usesformat_id.id(3.1 ships zero literal entries; future literals land here).structuralmatch — asset_types + VAST/DAAST versions + dimensions. Family-level match emitsFORMAT_DECLARATION_V1_AMBIGUOUSbut still produces a usable declaration.FORMAT_PROJECTION_FAILED, no declaration.Emitted declarations always carry
v1_format_refpointing back at the source v1 format_id so the half-1 v2 → v1 path round-trips cleanly.Bidirectional
pixel_tracker(pixel_tracker.py)Implements the normative downgrade/upgrade table from
core/assets/pixel-tracker-asset.json:downgrade_pixel_tracker(v2 → v1) collapses 7 event × 2 method combos onto the v1{asset_type: url, url_type: tracker_pixel}shape keyed onimpression_tracker/viewability_tracker/click_trackerslots. Lossy on viewability variants, custom events, and any JS method — those emitPIXEL_TRACKER_LOSSY_DOWNGRADE.upgrade_v1_tracker(v1 → v2) inverts via asset_id convention. ALWAYS emitsPIXEL_TRACKER_UPGRADE_INFERREDbecause the v1 wire shape carries no explicit event/method.Batch helpers (
downgrade_pixel_trackers/upgrade_v1_trackers) deduplicate advisories so a manifest with many same-kind pixels surfaces one advisory per kind.FORMAT_DECLARATION_DIVERGENTnarrowing check (narrowing.py)check_narrows(v2_params, v1_requirements)returns divergence records covering four kinds:exceeds_max— v2 numeric value exceeds v1 maxbelow_min— v2 numeric value below v1 minnot_subset— v2 enum-typed set has values outside v1 allowed setnot_equal— exact-scalar disagreementnarrowing_advisory(declaration, *, v1_requirements, v1_format_id)wraps the divergence list in a wire-correctFORMAT_DECLARATION_DIVERGENTErrorwithdetails.divergencesenumerating each failing field.v2 silently omitting a v1-declared field is NOT a divergence (narrows into unconstrained space).
Vendored fixtures
Vendored from
adcontextprotocol/adcp@mainupstream:Productfixtures attests/fixtures/canonical/<name>.json(fromstatic/examples/products/canonical/). Real-world seller catalogs exercising the v2 → v1 path.tests/fixtures/canonical/v1-reference-formats.json(fromserver/src/creative-agent/reference-formats.json). Every entry carries an explicitcanonical:annotation, exercising the v1 → v2 step-1 path.Round-trip tests (
tests/test_canonical_formats_roundtrip.py)project_product_to_v1MUST emit format_ids that round-trip back to the source declaration viafind_declaration_by_v1_format_id.format_options[]MUST be parseable as the hand-rolledProductFormatDeclaration.CanonicalFormatKindvalues (pinned so an upstream catalog drift trips CI).20 new public-API names on
adcp.canonical_formatsV1ToV2Projection,V1CatalogProjection,V1Tracker,PixelTrackerDowngrade,PixelTrackerUpgrade,PixelTrackerBatchResult, plus theproject_v1_format_to_declaration,project_v1_catalog_to_v2,downgrade_pixel_tracker,downgrade_pixel_trackers,upgrade_v1_tracker,upgrade_v1_trackers,check_narrows,narrowing_advisoryfunctions.Test plan
ruff check src/— all checks passedmypy src/adcp/— 901 files, no issuespytest tests/— 5190 passed (5117 from half 1 + 73 new across 4 test files)FORMAT_DECLARATION_V1_AMBIGUOUS,FORMAT_PROJECTION_FAILED,PIXEL_TRACKER_LOSSY_DOWNGRADE,PIXEL_TRACKER_UPGRADE_INFERRED,FORMAT_DECLARATION_DIVERGENT) verified againstcore/error.jsonenumCloses #741.