Skip to content

feat(schemas): improve SDK generation ergonomics#5178

Merged
bokelley merged 1 commit into
mainfrom
adcp-5168
May 30, 2026
Merged

feat(schemas): improve SDK generation ergonomics#5178
bokelley merged 1 commit into
mainfrom
adcp-5168

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 30, 2026

Closes #5168.

Summary

  • Extract inline schema objects/array items into named core schemas so SDK generators can emit stable, reusable types.
  • Add and document x-adcp-open-payload for intentionally open JSON payload surfaces, including explicit true, false, and omitted semantics.
  • Document the nullable scalar convention and clarify how nullability differs from optional field presence.
  • Open compliance scenario strings on comply_test_controller request/response and get_adcp_capabilities.compliance_testing.scenarios so the schema matches the open-for-extension compliance contract.
  • Add schema validation coverage for custom compliance scenarios and boolean x-adcp-open-payload annotations.
  • Register the new named schemas and add a changeset for the schema ergonomics update.

Expert Review

  • Protocol review: no blockers.
  • Docs review: initial clarity findings were addressed by defining annotation semantics, clarifying mixed payload fields, and spelling out required-vs-optional nullable fields.
  • Automated review follow-ups were addressed in this PR where small and tracked separately where larger.

Validation

  • npm run build:schemas
  • npm run test:schema-utf8
  • npm run test:schemas
  • npm run test:extension-schemas
  • npm run test:docs-nav
  • npm run test:oneof-discriminators
  • npm run test:composed
  • npm run lint:schema-links
  • git diff --check
  • precommit hook: npm run test:unit, npm run test:test-dynamic-imports, npm run test:callapi-state-change, npm run typecheck
  • push hook: version sync, schema link convention, Mintlify broken-links check

Coverage Note

@bokelley bokelley marked this pull request as ready for review May 30, 2026 01:24
@bokelley bokelley changed the title Improve schema ergonomics for SDK generation feat(schemas): improve SDK generation ergonomics May 30, 2026
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 30, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

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

Approving. Clean inline-to-named extraction — every new schema is byte-equivalent to the shape it replaced, registered in index.json, and the parent $refs through cleanly. Right shape for SDK generators.

Things I checked

  • All seven extracted shapes match the inline originals field-for-field. forecast-dimension-signal.json preserves the anyOf requiring signal_ref OR signal_id and the allOf gating signal_value by presence. forecast-dimension-geo.json preserves the four if/then blocks per geo_level.
  • committed-metric.json and delivery-metric-aggregate.json carry discriminator: { propertyName: "scope" } + oneOf, matching the existing accepted patterns in core/assets/asset-union.json and core/activation-key.json. The vendor branch on delivery-metric-aggregate.json keeps qualifier (closed) and measurable_impressions (minimum: 0).
  • forecast-point-dimensions.json outer oneOf is now $ref-only across the six dimension schemas; each target has properties.kind = { const: ... } + kind in required, so the audit walker's const-property-discriminator path should classify it as discriminated. CI is the gate — scripts/oneof-discriminators.baseline.json has no new entries, so any new narrowable/dangerous finding fails rather than silently ratcheting.
  • Enum opening on comply_test_controller.scenario, list_scenarios.scenarios[], and compliance_testing.scenarios[] is the schema catching up to existing spec language — both comply-test-controller-request.json:528 and comply-test-controller-response.json:51 already mandated "Runners and sellers MUST accept unknown scenario strings." minor is the correct bump.
  • Server-side: server/src/training-agent/comply-test-controller.ts derives its tool-input enum from a local SCENARIO_ENUM, not the spec schema's closed enum. No code path depends on the removed enum.
  • All new schemas registered in static/schemas/source/index.json. Changeset present at .changeset/schema-ergonomics-sdk-codegen.md declaring minor.
  • Test 4B (x-adcp-open-payload boolean-only) and Test 11B (custom compliance scenarios) verify what they claim — 11B explicitly asserts the absence of scenario.enum on the three opened surfaces before round-tripping a custom value.

Follow-ups (non-blocking — file as issues)

  • docs/protocol/get_adcp_capabilities.mdx:525 still inlines the scenario list as the "current values." Informational only after this PR, but a whats-new-in-3-1.mdx note would help SDK consumers downgrade their codegen target from a typed enum to string.
  • x-adcp-open-payload: false on get-products-response.filter_diagnostics: the new schema-extensions.mdx table says omission already covers "unclassified." Reserve false for genuinely extension-tolerant fields with known structure, otherwise it's documentary noise.
  • Test 11B could also assert scenario.type === \"string\" to harden against a future shape change that drops the enum AND the type.

Minor nits (non-blocking)

  1. account-with-authorization.json additionalProperties: true. Draft-07 default is permissive, so the explicit true is a no-op. The forecast-dimension extractions use additionalProperties: false; the explicit true here is inconsistent house style but not a defect. Either drop it or keep — purely cosmetic.

Safe to merge once CI greenlights the oneOf audit.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 30, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

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

LGTM. Pure SDK-codegen refactor — extracts 13 inline shapes into named core schemas without changing wire output, and the one real behavior change (compliance scenario enum → open string) brings the schema back into agreement with the spec contract that was already MUST-open.

Things I checked

  • Extraction fidelity. Walked the 7 modified parents (package.json, get-media-buy-delivery-response.json, signal-targeting-rules.json, vendor-metric-optimization.json, canonical-projection-ref.json, forecast-point-dimensions.json, list-accounts-response.json) against the 13 new core/*.json files. Same required, additionalProperties, nested oneOf/allOf/if-then, descriptions. Discriminators preserved: committed-metric.json:7 and delivery-metric-aggregate.json:7 carry top-level discriminator: { propertyName: "scope" } matching the inline shapes they replaced.
  • Index registration. Every new schema registered in static/schemas/source/index.json (13 new entries — 29, 41, 85, 213, 217, 221, 225, 229, 233, 265, 465, 509 — plus the existing ones still resolve).
  • Open-payload annotation count. x-adcp-open-payload: true at exactly 4 sites: core/protocol-envelope.json:57, core/vendor-metric-value.json:33, governance/check-governance-request.json:36, compliance/comply-test-controller-response.json:506. Wire-irrelevant, draft-07 §6 unknown-keyword semantics — same precedent as x-adcp-hoist.
  • Schema/docs coherence on compliance scenarios. docs/protocol/get_adcp_capabilities.mdx:525 now mirrors the open-string contract in static/schemas/source/protocol/get-adcp-capabilities-response.json:1507-1530. Pre-PR docs already said "Runners MUST accept unknown scenario strings" — the closed enum was the spec/schema mismatch, not the broadening. minor changeset is the right classification.
  • oneOf audit walker. scripts/audit-oneof.mjs does one-hop ref resolution; oneOf: [{ $ref: ... }] where each target carries const/enum on the discriminator property resolves to discriminated, identical to the inline form. No baseline change expected, and the PR confirms npm run test:oneof-discriminators passes.
  • Test coverage. tests/schema-validation.test.cjs:217-240,300-310 recursively walks the schema graph asserting every x-adcp-open-payload is boolean; the compliance-open-string test compiles the three schemas async and validates seller_custom_fixture_reset round-trips through request, list-scenarios response, and capabilities.

Follow-ups (non-blocking — file as issues)

  • static/schemas/source/media-buy/get-media-buy-delivery-response.json:357-432by_package[].missing_metrics.items is still inline and uses the same scope: standard | vendor discriminator. Can't directly reuse committed-metric.json (different required set, no committed_at), but worth extracting to core/missing-metric.json so the trio committed/aggregate/missing has the same SDK-stable shape.
  • static/schemas/source/media-buy/get-media-buy-delivery-response.json:454-558by_catalog_item, by_creative, by_keyword, by_geo items are allOf [delivery-metrics + { type: object, ... }] wrappers that fit the same extraction pattern.
  • Changelog note worth adding: SDKs that previously generated a TS string-literal union from the compliance scenario enum will lose exhaustiveness on regeneration. Existing strings still match; the call-out helps integrators understand why their type narrowing changed.
  • docs/spec-guidelines.md:299-326 — the nullable scalar section is correct for plain scalars but doesn't cover nullable enums, where draft-07 requires both \"enum\": [..., null] and \"type\": [\"string\", \"null\"]. Common authoring trap; one example would prevent it.

Minor nits (non-blocking)

  1. Positional oneOf[0] access in the open-string test. tests/schema-validation.test.cjs:687 reads complyResponseSchema.oneOf?.[0]?.properties?.scenarios?.items and assumes branch index 0 is ListScenariosSuccess. Stable today, fragile under future reordering — find(b => b.title === 'ListScenariosSuccess') is cheap insurance.
  2. whats-new-in-3-1.mdx:242 paragraph density. Three new #5168 bullets land in one run-on **bold** lead-in chain. Splitting the SDK-codegen items into their own sub-paragraph would help the skim path.

Approving on the strength of the extraction fidelity sweep plus the spec-vs-schema reconciliation on the compliance contract.

@bokelley
Copy link
Copy Markdown
Contributor Author

Issue #5180 proposes extracting the remaining delivery-metric inline shapes (by_package[].missing_metrics.itemscore/missing-metric.json; by_catalog_item, by_creative, by_keyword, by_geo allOf wrappers) — same surface as this PR; consider folding before merge or confirm follow-up.


Generated by Claude Code

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 30, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

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

LGTM. Follow-ups noted below. Extract-to-$ref refactor with byte-identical bodies on the wire; the only behavior change — opening comply_test_controller.scenario and compliance_testing.scenarios[] from closed enum to bare string — is the schema catching up to the 3.0-documented "Runners MUST accept unknown scenario strings" contract, which is correctly a minor on the 3.1 pre-mode line.

Things I checked

  • Wire-shape preservation across all eight extractions. Properties, required arrays, defaults, allOf/if-then conditionals, additionalProperties, and the pre-existing discriminator keyword are preserved verbatim on account-with-authorization.json, canonical-projection-slot-override.json, committed-metric.json, delivery-metric-aggregate.json, the six forecast-dimension-*.json, signal-selection-group-rule.json, and vendor-metric-optimization-supported-metric.json. Inline copies in package.json, get-media-buy-delivery-response.json, forecast-point-dimensions.json, signal-targeting-rules.json, canonical-projection-ref.json, vendor-metric-optimization.json, and list-accounts-response.json are replaced 1:1 with $ref pointers.
  • Discriminator preservation. committed-metric.json and delivery-metric-aggregate.json keep discriminator: { propertyName: "scope" } + oneOf with scope: const branches. The five rate-style if/then requireds on delivery-metric-aggregate.json:99-148 (viewable_rate, completion_rate, cost_per_acquisition, roas) are duplicated verbatim from the inline original. forecast-point-dimensions.json reduces to a oneOf of six $ref entries; each extracted file still carries kind: const: <value> so the discriminator is preserved at the property level.
  • additionalProperties semantics. signal-selection-group-rule.json and canonical-projection-slot-override.json preserve their additionalProperties: true; the new x-adcp-open-payload: true annotations land only on objects already declaring additionalProperties: true (protocol-envelope.json:payload, vendor-metric-value.json:breakdown, check-governance-request.json:payload, the comply-test-controller-response.json decoded-JSON arm).
  • Schema-vs-docs coherence. docs/protocol/get_adcp_capabilities.mdx describes the open-string contract with SHOULD-include-canonical / MAY-include-custom. docs/reference/schema-extensions.mdx adds the x-adcp-open-payload section with explicit true/false/omitted semantics. docs/reference/whats-new-in-3-1.mdx calls out both ergonomics and open-scenario widening. docs/spec-guidelines.md adds the nullable-scalars section and explicitly rejects OpenAPI-style nullable: true in draft-07 source.
  • Changeset. .changeset/schema-ergonomics-sdk-codegen.md is minor. Right shape — extractions and the x-adcp-open-payload marker are non-breaking; the enum-to-string widening accepts every prior value plus more. Beta line absorbs this as 3.1.0-beta.N.
  • New tests. Test 4B asserts x-adcp-open-payload values are boolean; Test 11B asserts scenario is string (not enum) on the request, list-scenarios response items, and capabilities, and round-trips a custom seller_custom_fixture_reset scenario through all three schemas. 5473/5473 schema tests pass.
  • protocol-envelope.json:payload gains x-adcp-open-payload: true — correctly marks the decoded payload arm as intentionally open without changing its additionalProperties: true shape.

Follow-ups (non-blocking — file as issues)

  • x-adcp-open-payload: false is documented but unused. docs/reference/schema-extensions.mdx defines the false semantics, but no schema in this PR sets it. Either land one canonical use site (a structured object that's extension-tolerant but not free-form) or mark false as reserved-for-future-use so generators don't write dead handling for a value they'll never see.
  • Test 4B only enforces type, not allowed values. x-adcp-open-payload is documented as true / false / omitted. A boolean-only assertion is correct today but won't catch a future tightening that forbids false. Cheap to upgrade to an allow-list when the spec narrows.
  • Open-scenario widening deserves a release-note callout, not just a Misc bullet. It's the only field-level type widening in 3.1; buyers/sellers running literal-union codegen need the heads-up. The whats-new entry at docs/reference/whats-new-in-3-1.mdx:248 covers the migration, but consider promoting it.

Minor nits (non-blocking)

  1. Brittle branch lookup in the compliance-scenario test. tests/schema-validation.test.cjs:687 locates the list-scenarios branch via complyResponseSchema.oneOf?.find(branch => branch.title === 'ListScenariosSuccess'). Optional chaining makes a future title rename or restructure into allOf silently downgrade the assertion to a no-op — listScenariosResponse?.properties?.scenarios?.items?.enum is undefined either way. Add an explicit if (!listScenariosResponse) return 'ListScenariosSuccess branch missing — selector out of date'; so a rename trips the test instead of passing it.
  2. Extracted-file descriptions are thinner than parent prose. New forecast-dimension-*.json files have one-line root descriptions; the parent forecast-point-dimensions.json carries the dispatch-on-kind context. SDK doc-generators that read root description will get the slim string. Cosmetic — but a one-line pointer to the parent ("Variant of ForecastPoint dimensions; see forecast-point-dimensions.json for dispatch rules.") would help adopters reading the bundled type docs cold.

Approving on the strength of the byte-identical extractions plus the new tests that pin both the open-payload boolean contract and the open-string scenario contract.

@bokelley
Copy link
Copy Markdown
Contributor Author

Confirmed: not folding the remaining delivery-metric inline extraction into this PR. It is tracked as #5180 so this PR stays focused on the #5168 cleanup and compliance enum mismatch.

@bokelley
Copy link
Copy Markdown
Contributor Author

Issue #5181 proposes clarifying x-adcp-open-payload: false semantics and tightening Test 4B — same surface as this PR (docs/reference/schema-extensions.mdx, tests/schema-validation.test.cjs); consider folding before merge or confirm follow-up.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

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

LGTM. Clean ergonomics refactor: extract inline shapes into named $ref schemas without changing wire validation, plus one additive widening (open-string compliance scenarios) and one new documentary annotation (x-adcp-open-payload). The architectural principle holds — keep the wire stable, give SDK generators stable type names to land on.

Things I checked

  • Refactor fidelity. Twelve extracted schemas are byte-equivalent to the inline shapes they replaced — required, additionalProperties, if/then gates, oneOf branches, descriptions, and x-entity annotations all preserved (code-reviewer verified). Spot-checked core/delivery-metric-aggregate.json:6-9 (discriminator preserved at the new root) and core/forecast-point-dimensions.json:13-37 (collapses 250+ lines of inline oneOf into six $ref lines, same six discriminator branches in the same order).
  • Discriminator integrity. core/committed-metric.json and core/delivery-metric-aggregate.json both carry discriminator: {propertyName: "scope"} at the new top-level with const: "standard" / const: "vendor" on each branch and additionalProperties: false. scripts/oneof-discriminators.baseline.json tracks only undiscriminated entries — no baseline update required.
  • Index completeness. All twelve new files registered in static/schemas/source/index.json under the right namespaces.
  • Open-payload annotation pairing. All four sites (core/protocol-envelope.json:57, core/vendor-metric-value.json:33, governance/check-governance-request.json:36, compliance/comply-test-controller-response.json recorded-call payload) paired with additionalProperties: true and the mixed-arm case explicitly carved out in the description prose.
  • Open-scenario semver. minor is the right call — the wire contract was already open per docs/protocol/get_adcp_capabilities.mdx ("Runners MUST accept unknown scenario strings"). Typed-SDK consumers will notice the union widen to string on regeneration; docs/reference/whats-new-in-3-1.mdx calls that out with a migration note. Schema is being brought into agreement with documented runner behavior.
  • Test coverage. tests/schema-validation.test.cjs Test 4B recurses arrays-by-index and object-entries to catch any non-boolean x-adcp-open-payload; Test 11B asserts no .enum at the three open-scenario sites and round-trips a custom scenario string through Ajv with discriminator: true. A future enum re-introduction fails the latter.
  • Schema-vs-docs coherence. docs/protocol/get_adcp_capabilities.mdx:525 prose now matches the open schema. docs/spec-guidelines.md nullable-scalar convention ({"type": ["string", "null"]}) is consistent with how core/forecast-dimension-signal.json:34 already encodes signal_value.

Follow-ups (non-blocking — already tracked)

  • x-adcp-open-payload: false ships defined-but-unused. The doc concedes the line between false and omission is fuzzy when additionalProperties: true. Notable; usage/test-policy already tracked in #5181.
  • Remaining delivery-metric extraction tracked in #5180.
  • Python SDK regeneration tracked in adcontextprotocol/adcp-client-python#902.

Approving on the strength of byte-equivalent extraction plus a tightened schema-authoring lint that locks the open-scenario contract in.

@bokelley
Copy link
Copy Markdown
Contributor Author

Confirmed: not folding the x-adcp-open-payload false policy decision into this PR. It is tracked as #5181; this PR keeps the documented annotation plus boolean authoring lint and leaves the usage/test-policy decision for follow-up.

@bokelley bokelley merged commit dbc5b56 into main May 30, 2026
32 checks passed
@bokelley bokelley deleted the adcp-5168 branch May 30, 2026 02:07
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.

3.1 schema ergonomics for SDK generation

1 participant