fix(codegen): restore downstream model contracts - #1134
Conversation
|
|
||
|
|
||
| class SyncGovernanceResponse2(AdcpVersionEnvelope, ProtocolEnvelope): | ||
| class SyncGovernanceResponse(AdcpVersionEnvelope, ProtocolEnvelope): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Ladon verdict: Request changes
Request changes — 1 blocking finding.
Blocking findings
- client.py:3867/3955/3999/5035/5420 —
restore_constructible_response_basesconverts five response union aliases into field-less base classes._validate_union_typethen takes the non-union path, andmodel_validateon the base drops all arm fields underextra='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_pocchanges 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)) |
There was a problem hiding this comment.
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_type → Base.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.
There was a problem hiding this comment.
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.py—TypeAdapter(union_type)is rebuilt permodel_validate/model_validate_jsoncall on the response-parse hot path; memoizing perclswould 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.
Summary
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