Skip to content

fix(codegen): restore downstream model contracts - #1134

Merged
bokelley merged 2 commits into
mainfrom
fix-codegen-runtime-contracts
Sep 5, 2026
Merged

fix(codegen): restore downstream model contracts#1134
bokelley merged 2 commits into
mainfrom
fix-codegen-runtime-contracts

Conversation

@bokelley

@bokelley bokelley commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • restore constructible Pydantic response classes changed into union aliases by datamodel-code-generator 0.64
  • restore schema validation and metadata for signal references, creative representations, and transformer outputs
  • make the compatibility repairs durable in the post-generation pass and cover public downstream paths

Context

Follow-up to #1132. A three-part expert audit of downstream API behavior, generated model fidelity, and compatibility CI found these runtime regressions even though the exported-name snapshot remained unchanged.

Validation

  • fresh full type regeneration
  • 7,630 passed, 42 skipped, 9 deselected, 1 expected xfail
  • mypy: clean across 1,307 source files
  • pre-commit: all hooks passed



class SyncGovernanceResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
class SyncGovernanceResponse(AdcpVersionEnvelope, ProtocolEnvelope):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MUST FIX: This field-less base class silently drops the entire response payload on the client parse path. Before this PR SyncGovernanceResponse was the union SyncGovernanceResponse1 | SyncGovernanceResponse2, and _validate_union_type (response_parser.py:35-56) discriminated across the arms. Now it is a concrete class with no arm fields. get_origin() on a class is None, so _validate_union_type takes the non-union branch (response_parser.py:60) and calls SyncGovernanceResponse.model_validate(payload); AdCPBaseModel defaults to extra='ignore', so accounts/context/ext are dropped and .data comes back an empty envelope.

Same regression hits all five bases produced by restore_constructible_response_bases — every one is a live _parse_response type: CreateContentStandardsResponse (client.py:3867), ListContentStandardsResponse (3955), UpdateContentStandardsResponse (3999), SyncGovernanceResponse (5035), ComplyTestControllerResponse (5420). The client tests mock _parse_response, so nothing catches it. Fix in scripts/post_generate_fixes.py:restore_constructible_response_bases: keep the discriminating union alongside the constructible base (e.g. retain SyncGovernanceResponse = SyncGovernanceResponse1 | SyncGovernanceResponse2 under a separate name the client parses through), or add a round-trip test through _parse_response for each of the five.

@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: Request changes

Request changes — 1 blocking finding.

Blocking findings

  • client.py:3867/3955/3999/5035/5420 — restore_constructible_response_bases converts five response union aliases into field-less base classes. _validate_union_type then takes the non-union path, and model_validate on the base drops all arm fields under extra='ignore' — silent payload loss on five public client methods. This is a Critical data-loss/wire-fidelity defect on load-bearing public API paths.

Other notes (non-blocking)

  • The three field-restoration passes (signal_ref, format_kind, transformer output contract) correctly restore 3.2 schema fidelity and are idempotent.
  • generated_poc changes are pipeline output, not hand-edits, so the "generated code is not source" gate does not fire.
  • Semver is clean: the 0.64 regression is unreleased, so no breaking-change footer is required.

The Critical finding (row 1 of the decision table) blocks. Fix the base-class restoration so union arms retain their fields (or route validation through the union path) before merge.

Blocking findings

  • client.py:3867/3955/3999/5035/5420 — restore_constructible_response_bases converts five response union aliases into field-less base classes; model_validate on the base drops all arm fields under extra='ignore', causing silent payload loss on five public client methods (Critical)

for arm in arms[1:]:
union_type |= arm

return cast(Self, TypeAdapter(union_type).validate_python(obj, **kwargs))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: TypeAdapter(union_type) is rebuilt on every model_validate call (and again at L135 in model_validate_json). TypeAdapter construction compiles a fresh pydantic-core SchemaValidator — Pydantic's own docs flag this as a hot-path footgun ("create the TypeAdapter once and reuse it"). These bases are on the response-parsing path: parse_json_or_text_validate_union_typeBase.model_validate runs for every A2A response of these five task types, so each parse pays the full union-validator compile. _response_arm_models() returns a stable per-cls tuple — memoize the adapter (e.g. a cls-keyed cache or functools.cache on a helper) and reuse it.

@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 delta pass, no blocking findings.

This is a subsequent review; the prior decision was request-changes with no recorded critical/high/medium findings, and the current run also surfaces no blocking findings. The developer's new commits (ResponseArmDispatchMixin in src/adcp/types/response_dispatch.py and its wiring into the five constructible response bases via post_generate_fixes.py) were reviewed for import layering, generated-code pairing, public-surface/semver stability, MRO/recursion safety, and test coverage — all clean.

Medium findings

  • src/adcp/types/response_dispatch.pyTypeAdapter(union_type) is rebuilt per model_validate/model_validate_json call on the response-parse hot path; memoizing per cls would avoid repeated adapter construction. Non-blocking (single medium, performance, not correctness).

Decision-table walkthrough: no critical/high (row 1 no); gated_paths false (row 2 no); high_risk false, no deletions (rows 3/5 no); the single medium is category performance, not data-loss/schema/infra (row 4 no); prior decision was request-changes, not escalate (row 6 no); no no-auto-approve team match (row 7 no); only one medium finding, fewer than three (row 8 no). Falls through to row 9 → approve.

Note: review_decision is CHANGES_REQUESTED, but that only forces a non-approve outcome when gated_paths is true (row 2), which is false here. No auto-approve gate applies.

@bokelley
bokelley merged commit 64725d7 into main Sep 5, 2026
27 checks passed
@bokelley
bokelley deleted the fix-codegen-runtime-contracts branch September 5, 2026 22:04
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