Skip to content

feat(adcp/v3): generate list_products request and response types - #616

Merged
torbenbrodt merged 2 commits into
adcontextprotocol:mainfrom
affinity-com:feat/v3-list-products-types
Nov 11, 2026
Merged

torbenbrodt merged 2 commits into
adcontextprotocol:mainfrom
affinity-com:feat/v3-list-products-types

Conversation

@torbenbrodt

Copy link
Copy Markdown
Collaborator

Generates list_products request and response types, as agreed on #600. Types and tests only: no Config hook, no AddTool/Register wiring, no response builder, so tools/list doesn't change. Same basis as #589 and #596.

What changes

Generator rule (flat_required_union). A top-level response oneOf becomes one flat struct when:

  • the schema has a discriminator whose property is a string enum in the top-level properties;
  • each branch holds only required (naming the discriminator), a const for the discriminator, and optionally not: {required} naming declared properties;
  • the branch constants equal the enum;
  • the only top-level allOf is a bare $ref to core/protocol-envelope.json or core/version-envelope.json;
  • a top-level dependencies lists property names (its array form), which Validate() enforces;
  • no other top-level keyword is present. Schema-form dependencies, if/then, patternProperties and the like mean no match, so a rule the generator doesn't model is refused rather than silently not enforced.

Across every schema in the pinned 3.2.2 bundle (nested directories included), and across all of 3.3.0-beta.3, only list-products-response.json matches; get_reporting_status and list_account_changes keep their current shape, and tests pin both. For #602: 3.3.0-beta.3 adds the envelope allOf and dependencies: {"collections": ["products"]} to this response, and its real schema still matches, as does 3.3.0-beta.5's. A test applies both changes to the 3.2.2 schema and expects a match, with Validate() requiring products when collections is present.

ListProductsResponse. One struct with a typed Outcome (listed, unchanged). Fields every branch requires are required in the struct. Products is tagged omitzero: nil omits the key, a non-nil empty slice sends []. Validate():

  • listed without products, or unchanged with them (even an empty list), fails;
  • a missing outcome, feed_version or cache_scope fails; an empty string counts as missing, which is stricter than the schema for feed_version and matches validation.go;
  • an unknown outcome is accepted, and reported as UNKNOWN_VARIANT under WithStrictEnums, which also reports a cache_scope outside public/account.

For an empty listed page a seller sends Products: []CanonicalProduct{}; nil omits the key and Validate() reports it. The field's doc says so.

ListProductsRequest. adcp_major_version is *int like the other requests, and fields is []ProductField, an enum generated from media-buy/product-fields.json. ListProductsIncompleteItem is its own type because its scope enum has no proposals, unlike GetProductsIncompleteItem.

Tests

Schema-valid fixtures, validated against the pinned bundle before decoding, round-trip for listed (all optional fields), an empty page, a last page and unchanged, and for the request. Every Validate() failure case is also checked against the schema. A struct embedding ListProductsResponse keeps its own fields. Generator tests cover the rule, the excluded responses, the envelope allOf, refused keywords and the sweep. Each new check was broken in a copy to show its test fails.

Not in this PR

  • No seller or buyer helper, hook or registration; no get_products fallback (x-legacy-fallback) and no translation, since list_products is new in 3.2.
  • Validate() is opt-in and doesn't check the product items or incomplete.scope.
  • No request-side Validate() for its dependencies/if-then rules, max_results bounds or the fields item constraints.
  • list_products is not in mutatingTools: its idempotency_key is optional.
  • The 3.3.0-beta.3 fields themselves (include_collections, collections, two new ProductField values, the envelope status) arrive with feat(adcp/v3): adopt the signed AdCP 3.3.0-beta.3 bundle (WIP) #602's bundle bump.

Checks run locally

Generator tests (163); regeneration reproduces types_gen.go; lint.py --strict; --coverage-max-unreviewed-any 0; go vet and go test -race ./... in adcp/v3 on Go 1.26 and 1.27; the list_products tests under GOEXPERIMENT=jsonv2; golangci-lint.

Related: #600, #564.

🤖 Generated with Claude Code

Add media-buy/list-products-request.json and -response.json to
TOOL_SCHEMAS: ListProductsRequest, ListProductsResponse and the inline
ListProductsIncompleteItem. This adds generated types only; no tool is
registered or wired, so nothing changes for a server's tool list.

The response oneOf only switches which top-level properties are
required (outcome: listed or unchanged), so the generator now takes a
narrow new shape as ONE flat struct: a top-level response oneOf with a
discriminator whose branches add only required, a const on the
discriminator and not.required, over top-level properties. The match
refuses rather than skips: the top-level keywords are an allowlist, so
a rule the generator does not model (schema-form dependencies, if/then,
patternProperties, a not that names a declared field) means no match
and a bundle that adds one fails loudly. Array-form dependencies are
modelled (3.3 adds collections requiring products): Validate reports
each listed field missing when the key field is present. A top-level allOf that only
composes core/protocol-envelope.json or core/version-envelope.json by
whole-file reference is tolerated, so the rule survives the 3.3
bundles that compose the envelope into every response.
get_reporting_status and list_account_changes do not match and keep
their existing paths. The rule applies to tool responses only.

- Outcome is a generated enum (ListProductsResponseOutcome), and a
  generated Validate(opts ...ValidationOption) []ValidationIssue states
  the per-outcome rules: feed_version and cache_scope always, products
  for listed, no products for unchanged. An empty string counts as
  missing (stricter than the schema, as in validation.go). Under
  WithStrictEnums an unknown outcome and a cache_scope outside the
  schema enum are reported.
- products is tagged omitzero, not omitempty, so a non-nil empty slice
  is sent as [] (an empty page for listed) and nil omits the key. The
  field doc says so. There is no MarshalJSON, so a type that embeds the
  response keeps its own fields.
- ListProductsRequest.adcp_major_version is *int like the other
  requests; fields is []ProductField, an enum generated from the items
  of media-buy/product-fields.json.
- incomplete gets its own item type: its scope enum has no proposals
  value, so GetProductsIncompleteItem does not fit.
- Tests: schema-valid fixtures round-tripped for the request and both
  outcomes (including an empty page and an embedding wrapper), Validate
  cases checked against the schema as well, and generator tests for the
  rule, its refusals, the two excluded responses, an allOf-envelope
  fixture and a sweep that only list_products matches.

list_products takes no required idempotency_key, so it is not in
mutatingTools.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@torbenbrodt
torbenbrodt marked this pull request as ready for review November 11, 2026 10:07

@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 — additive v3 codegen with no blocking or medium findings.

What I checked:

  • Schema↔generated-types coherence: generate.py changed AND types_gen.go regenerated in the same PR; VERSION stays 3.2.2; schema bundle downloaded (not committed); generated banner not hand-edited.
  • flat_required_union is fail-closed — top-level allowlist returns None on unmodeled keywords so Validate never silently drops a rule; _flat_union_presence raises on undetectable zero-value checks.
  • Generated ListProductsResponse.Validate (types_gen.go L1848-1880) matches schema branches; all helper signatures exist; omitzero distinguishes nil from empty slices.
  • ListProductsIncompleteItem optionality matches GetProductsIncompleteItem.
  • No TMP/TEE/skills changes; additive new task (no breaking marker required); DR-0032/0033 pathway-back inapplicable (no version-translation layer here).

Gate checks: gated_paths=false, high_risk=false, no no-auto-approve team match, no prior decision. review_decision is REVIEW_REQUIRED but the gated-paths hard gate (row 2) only fires when gated_paths is true. With zero findings, no row 1–8 triggers — falls through to row 9.

@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 pass. This PR adds a generator rule that flattens a discriminated required-only oneOf (list_products' outcome: listed | unchanged) into a single struct with a generated Validate, regenerates types_gen.go, and adds thorough tests.

Verified by the reviewer:

  • Schema↔generated-type coherence maintained — generate.py TOOL_SCHEMAS and types_gen.go regenerated together (mandatory coherence rule satisfied).
  • The Validate switch matches the schema's per-branch required/not:{required} (types_gen.go:1867-1878).
  • omitzero correctly preserves the empty-vs-nil products distinction.
  • All referenced types/validation helpers exist; the recognizer is a refuse-not-skip allowlist.

No Critical/High/Medium findings. high_risk is false; gated_paths is false; review_decision is APPROVED; no no-auto-approve team match. Prior decision was also approve. None of decision-table rows 1–8 fire, so the outcome is approve (row 9).

@torbenbrodt
torbenbrodt merged commit a9754a5 into adcontextprotocol:main Nov 11, 2026
17 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