Skip to content

fix(types): resolve capability sub-models via field annotation, not numbered names#745

Merged
bokelley merged 2 commits into
mainfrom
bokelley/fix-codegen-numbered-class-drift
May 19, 2026
Merged

fix(types): resolve capability sub-models via field annotation, not numbered names#745
bokelley merged 2 commits into
mainfrom
bokelley/fix-codegen-numbered-class-drift

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

  • Restores Validate schemas are up-to-date on main (failing since 49c4b5ab, May 14).
  • Roots out the cause: capabilities.py was importing Features2 as SignalsFeatures and Idempotency3 as IdempotencyUnsupported — both are codegen-internal numbered names that drift across regens.
  • Resolves them via parent field annotation (Signals.features / Adcp.idempotency) instead, so the imports survive future renumbering.

Root cause

datamodel-code-generator 0.56.1 numbers inline-schema classes from a global counter whose traversal order depends on filesystem iteration. Reproduced in a Linux 3.11-slim Docker container against schemas 3.0.7:

  • macOS regen → Features + Features2, Idempotency + Idempotency3
  • Linux regen → Features + Features1, Idempotency + Idempotency1

The committed bundled file was generated when the counter produced Features2/Idempotency3. Today's regen (same pinned generator) produces Features1/Idempotency1, so capabilities.py:117 blows up with ImportError: cannot import name 'Features2' during the Generate models step, taking the whole adcp package import down with it.

The fix

Reach the classes via the parent's field annotation — Signals and Adcp are stable wire-spec class names; the union arm types live directly on the field annotations:

_signals_features_arms = [
    arm for arm in _get_args(_Signals.model_fields["features"].annotation)
    if arm is not type(None)
]
SignalsFeatures: type = _signals_features_arms[0]

_idempotency_arms = [
    arm for arm in _get_args(_Adcp.model_fields["idempotency"].annotation)
    if arm is not IdempotencySupported and arm is not type(None)
]
IdempotencyUnsupported: type = _idempotency_arms[0]

Both lookups raise loudly if the annotation shape ever changes — silent mis-resolution beats import-time bombs, but a clear RuntimeError at boot beats both.

Also in this PR (regen drift)

The same python scripts/generate_types.py produced minor cosmetic drift the previous regen never committed:

  • SCHEMA_DELTAS.md — previously reported extension_meta delta is already merged upstream.
  • _ergonomic.pylist[...]Sequence[...] in three BeforeValidator annotations.
  • enum + media_buy bundled files — duplicate-import cleanup.

Test plan

  • pytest tests/ -x → 4731 passed, 34 skipped, 1 xfailed
  • mypy src/adcp/types/capabilities.py src/adcp/decisioning/capabilities.py → clean
  • ruff check src/adcp/types/capabilities.py → clean
  • Reproduced the CI failure in Linux Docker; verified the fix resolves SignalsFeatures → Features1 and IdempotencyUnsupported → Idempotency1 cleanly after fresh regen.

🤖 Generated with Claude Code

bokelley and others added 2 commits May 19, 2026 14:54
…umbered names

``capabilities.py`` imported ``Features2 as SignalsFeatures`` and
``Idempotency3 as IdempotencyUnsupported`` from the bundled
``get_adcp_capabilities_response`` module. Those numbered class names
are an internal codegen detail: ``datamodel-code-generator`` 0.56.1
assigns numbered suffixes (``Features1``, ``Features2``…) to inline
schemas from a global counter whose traversal order shifts with both
upstream schema layout and filesystem-iteration order (APFS-on-macOS
vs ext4-on-Linux). The committed bundled file was generated when the
counter produced ``Features2`` and ``Idempotency3``; today's regen
(on the same pinned generator) produces ``Features1`` and
``Idempotency1``, breaking ``capabilities.py`` at import time and
taking the whole ``adcp`` package down with it — which is what
``Validate schemas are up-to-date`` CI was reporting.

Reach the classes via their parents' field annotation instead.
``Signals.features`` and ``Adcp.idempotency`` are stable wire-spec
class names; their field annotations carry the union arms directly,
independent of whatever number the codegen assigned this regen. Pin
both lookups with explicit assertions so future schema changes that
break the shape fail loudly at boot rather than silently mis-resolving.

Also commits the cosmetic regen drift the same run produced:
- ``SCHEMA_DELTAS.md``: previously reported ``extension_meta`` delta
  is already merged upstream; deltas list is now empty.
- ``_ergonomic.py``: ``list[...]`` → ``Sequence[...]`` in three
  ``BeforeValidator`` annotations (regen normalization).
- enum + media_buy bundled files: duplicate-import cleanup.

Test plan:
- pytest tests/ -x → 4731 passed, 34 skipped
- mypy + ruff clean
- Reproduced the CI failure in a Linux 3.11-slim Docker container
  against schemas 3.0.7; verified the fix resolves SignalsFeatures
  → Features1 and IdempotencyUnsupported → Idempotency1 cleanly
  after fresh regen.

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

The AdCP storyboard runner (``@adcp/sdk`` on npm) validates response
shapes with ``zod``. ``zod.string().datetime()`` rejects the ``+00:00``
offset by default — only the Zulu form (``...Z``) passes. Python's
``datetime.isoformat()`` produces ``+00:00`` for tz-aware UTC values,
so every response builder that auto-stamped a timestamp (``confirmed_at``,
``canceled_at``, ``expires_at``, ``reporting_period.start/end``,
``created_date`` / ``updated_date`` on listed creatives) emitted output
that failed schema validation in both the ``examples/seller_agent.py``
and ``sales-proposal-mode (proposal_finalize)`` storyboard jobs:

  Schema validation: /confirmed_at: Invalid ISO datetime

Reproduced locally:

  $ node -e 'const {z}=require("zod"); z.string().datetime().parse("2026-05-19T21:56:22.349222+00:00")'
  ZodError: [{"code":"invalid_string","validation":"datetime",...}]

  $ node -e 'const {z}=require("zod"); z.string().datetime().parse("2026-05-19T21:56:22.349222Z")'
  # succeeds

Add ``_rfc3339_now()`` in ``server/responses.py`` and route the three
auto-stamp sites inside that file through it; inline the equivalent
``.isoformat().replace("+00:00", "Z")`` shim in the four other touch
points (``server/helpers.py`` canceled_at, ``server/proposal.py``
expires_at, ``sales_proposal_mode_seller`` confirmed_at and expires_at).
ajv-formats and python-rfc3339 both still accept the new form, and
``test_server_helpers.py:373`` was already tolerant of either suffix —
no test changes needed.

Test plan:
- pytest tests/ -x → 4731 passed, 34 skipped, 1 xfailed
- Verified ``_rfc3339_now()`` output ends in ``Z`` and parses cleanly
  in zod ``string().datetime()`` (against ``@adcp/sdk`` 's pinned zod).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit b63c7e6 into main May 19, 2026
16 checks passed
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.

1 participant