Skip to content

fix(codegen): follow source additionalProperties instead of asserting closure - #3678

Merged
bokelley merged 5 commits into
mainfrom
fix/codegen-open-schema-assumptions
Nov 9, 2026
Merged

bokelley merged 5 commits into
mainfrom
fix/codegen-open-schema-assumptions

Conversation

@bokelley

@bokelley bokelley commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This prepares the code generator for AdCP 3.3.0-rc.0's opened schemas. On Nov 9 the AdCP WG decided that 3.x opens the schemas that were closed by accident. adcontextprotocol/adcp#8866 does that for 3.3: about 744 locations open, and about 115 stay closed for stated structural reasons. The SDK has to regenerate cleanly from that bundle before the 3.3.0-rc.0 SDK regeneration (RC cut Mon Nov 16).

Today the generator assumes closure. #8866's non-required "Generate TypeScript SDK" check fails with get_reporting_status source schema must close scope. Several other post-processors don't fail but stay stricter than the source. This PR makes every post-processor that restores strictness read additionalProperties from the synced source cache:

  • Closed source: the post-processor emits exactly what it emits today.
  • Open source: the object stays passthrough, or the unknown-key check is left out.

Three new helpers do this. sourceSchemaAt(file, pointer) reads one object schema from schemas/cache/latest, and sourceUnknownKeyPolicy() returns the matching policy. The third, openInlineObjectPointers(), guards the passes that close a whole generated expression: if a closed root contains an open inline object, generation fails rather than closing it by accident.

Audit: every place the generator assumed closure

All of these are in scripts/generate-zod-from-ts.ts.

Post-processor What it assumed Source #8866 Now
reportingStatusClosedStructures + postProcessGetReportingStatusEvidenceStrictness Threw unless scope, scope.delivery_config_generations[], obligation_counts and pagination were closed; superRefine rejected unknown keys on all four media-buy/get-reporting-status-response.json (bundled) opened Key set recorded only for closed structures; unknown keys rejected only there; required-field checks unchanged
postProcessReportingEvidenceStrictness .strict() on 14 hard-coded evidence schemas and every inline object inside them core/reporting-{coverage,status-issue,canonical-content-digest,control-total,resource,verification,schedule,receipt,revision,obligation,materialization}.json opened Each name mapped to its file and pointer (title checked); strict only while the source root is closed
postProcessReportingConsumerStatusConstraints Threw unless the consumer-status root was closed; hard-coded unknown-key checks on the item, period and the sync_reporting_status request core/reporting-consumer-status.json, media-buy/sync-reporting-status-request.json opened Each check emitted only when its object is closed; a closed key set that drifts from the emitted list fails
postProcessTrustedMatchResponseSchemas .strict() on TMPXChunk, TMPProviderRegistration, PublisherTMPXMacroMapping and the router tmpx_providers entry trusted-match/{tmpx-chunk,provider-registration,publisher-tmpx-config,identity-match-response}.json opened Per-object policy from the source
postProcessCompatibilityPurchaseCoordinatorInput .strict() on the restored schema media-buy/legacy-purchase-continuation-input.json opened Policy from the source
postProcessTrustedMatchPrivacyBoundaryStrictness .strict() on the whole context/identity match request except proof trusted-match/{context,identity}-match-request.json kept closed (privacy boundary) Strict while the root is closed; fails if a closed root keeps an unreviewed inline object open
↳ same pass (bug that predates this PR) Its slice ran from ContextMatchRequestSchema to OfferPriceSchema, so it also closed the neighbouring TargetingKvsSchema, a context-match response shape trusted-match/{,provider-}context-match-response.json#/definitions/targeting_kvs/items opened The pass closes only the request export; TargetingKvsSchema gets its own policy from the response sources (and generation fails if the two disagree)
postProcessPlacementPresentationRuntimeConstraints .strict() on 8 presentation objects core/placement-presentation.json kept closed Per-object policy from the source
postProcessBeta6ReportingConstraints .strict() on CanonicalMetricQualifier core/canonical-metric-qualifier.json kept closed (join key) Policy from the source
reportingFileManifestClosedStructures Already read additionalProperties: false from the source core/reporting-file-manifest.json + refs opened No change needed: it follows the source and now emits passthrough

These do not assume closure, so they are unchanged:

  • scripts/generate-types.ts (TypeScript types): enforceStrictSchema deletes additionalProperties: true and compiles with additionalProperties: false, so TS types never gained index signatures for open objects. That is a typing policy, not runtime validation. The size-mode and mutual-exclusion rewrites emit additionalProperties: false only for the TS projection.
  • Passes that open schemas: CanonicalProposal and the canonical-format passes turn .strict() into .passthrough() on purpose (the SDK's extension policy). These open schemas rather than close them, so they are out of scope here. Note that this also loosens the commercial terms that #8866 keeps closed.
  • Key and forbidden-field constraints: the TMP provider→router "forbidden field" refinements (from not.anyOf required), the postal-area key checks (propertyNames and explicit keys) and propertyNames records are key constraints, not closure.
  • Other scripts: generate-zod-schemas.ts, generate-wellknown-schemas.ts and schema-utils.ts only use open fallbacks. schema-diff.ts reports additionalProperties flips.

Verification

  • 3.3.0-beta.5 (published bundle, digest 6c716e67…): no change. npm run generate-types with the final generator reports every generated file up to date, and git status is clean. Still-closed structures produce byte-identical output.
  • #8866 bundle (CI artifact for head 9c1c52de, digest 785b85a9…): I replayed the adcp "Generate TypeScript SDK" job step for step. The bundle was served locally with ADCP_BASE_URL, then sync-schemas, generate-types and validate-schemas ran, then build:lib, then probe-generated-sdk-creative-assets.cjs from the PR head, then ci:codegen-strict. All pass, and the probe reports 57 Zod shape checks and 157 canonical checks. Before this PR, generation failed at must close scope.
  • Runtime parity: test/generate-zod-reporting-status.test.js checks generated GetReportingStatusResponse validation against Ajv on the authoritative source over 30 cases. Generated and authoritative agree on both bundles. Under #8866, all 17 unknown-key cases (scope, generations, obligation counts, pagination and every evidence object) move from rejected to accepted in the source, and the generated Zod moves with them. That test also pins the closed-beta.5 outcome array, so the rc.0 regeneration PR will need to update it.
  • Spot-check on the #8866 build: TMPXChunk, TargetingKvs items, an evidence digest and CompatibilityPurchaseCoordinatorInput accept an unknown key. ContextMatchRequest, CanonicalMetricQualifier and the placement Rectangle still reject one.
  • Generated-type changes for the opened structures:
    • TS types: no index signatures added, so the public type surface is unchanged. The other diffs are description text plus one equivalent change: demographics has a Required/Supported arm, and #8866 annotates that $ref as deprecated, so jsts inlines it as true. The type is the same.
    • Zod: .strict() becomes .passthrough() on the 14 evidence schemas and their inline period/limitations/native_commit_evidence objects, on ReportingFileEntry/ReportingFileManifest, the TMP chunk, registration, macro mapping and tmpx_providers entry, on TargetingKvs items, and on CompatibilityPurchaseCoordinatorInput.
    • Zod built straight from the source by json-schema-to-zod: TargetingOverlay, TargetingOverlayInput and MediaBuyDeliveryWebhookResult already follow the source, so they open with no generator change (30 → 0 and 45 → 5 .strict() calls).
    • Zod refinements: the "Unrecognized key" checks are gone from ReportingConsumerStatus (item and period) and SyncReportingStatusRequest. The GetReportingStatusResponse refinement keeps its required-field checks but no longer rejects unknown keys on scope, the generation items, obligation_counts or pagination.
    • RecordedReportingReceipt/UnchangedReportingReceipt collapse .and() to .merge() now that the receipt is no longer strict.
    • TMP requests, placement presentation and the canonical qualifier stay .strict().
  • Tests: the new test/generate-zod-source-closure.test.js has 11 tests and uses injected readers and inline fixtures, so it never depends on the synced cache. It covers the closed source, the open source, mixed sources and each new failure mode for every changed post-processor, compiling the emitted Zod where practical. Existing generator tests now inject sources instead of reading the cache. Across the 7 generator test files: 64/64 pass on the published cache.

This PR does not regenerate types: the checked-in output stays on 3.3.0-beta.5. The 3.3.0-rc.0 regeneration will pick up the opened schemas.

Out of scope: the opposite gap

The SDK's global passthrough policy means the Zod output does not enforce most of the locations #8866 keeps closed. Examples are the credential-free reporting destinations, the signed commercial terms (which CanonicalProposal turns from strict to passthrough on purpose), the forecast-dimension-placement join key, and the closed oneOf branches. This PR doesn't change that; it only stops the generator from being stricter than the source.

Refs adcontextprotocol/adcp#8866

🤖 Generated with Claude Code

bokelley and others added 5 commits November 9, 2026 20:22
… closure

AdCP 3.x opens the schemas that were closed by accident
(adcontextprotocol/adcp#8866). The Zod generator required
`additionalProperties: false` on four get_reporting_status structures and
hard-coded `.strict()` or unknown-key rejection in several other
post-processors, so it either failed or stayed stricter than the source.

Read closure from the synced source cache everywhere a post-processor
restores strictness. Still-closed sources keep their strict output, and
the generated output for the current 3.3.0-beta.5 bundle is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… neighbours

The privacy-boundary pass rewrote everything from ContextMatchRequestSchema
to OfferPriceSchema, which also closed TargetingKvsSchema, a context-match
response shape. It matched the source only while targeting_kvs items were
closed; adcontextprotocol/adcp#8866 opens them. Close just the request
export, and give TargetingKvsSchema its own source-driven policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inject inline source fixtures through the readSource parameters so the
tests never depend on schemas/cache/latest. For each changed post-processor
cover the closed source (previous strict output), the open source (no
unknown-key check, no throw, runtime accept/reject of emitted Zod), and the
new failure modes: title mismatch, closed root with open inline object,
changed closed key sets, and targeting_kvs disagreement. Also assert the
privacy-boundary pass closes only the request export.

Update the three tests that hard-coded the old assertion or read the cache.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The file spawns a tsx harness per post-processor and runs about 25s alone,
close to the fast lane's 60s file limit on a cold shard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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 — Zod generator now follows each source schema's additionalProperties (.strict() only where closed, .passthrough() otherwise) instead of asserting closure, preparing for AdCP 3.3.0-rc.0; generated output for the current beta.5 bundle is unchanged.

What was checked

  • Witness-not-translator: passthrough is more faithful than asserting closure — no fabrication or re-shaping at the seam.
  • Load-bearing fix: the new single-export boundary (source.indexOf('\n\nexport const ', start + 1)) prevents the context-match pass from accidentally closing TargetingKvsSchema; a missing next export throws (fail-closed).
  • Fail-closed drift guards (missing file/pointer, title mismatch, open inline under closed root, key-set drift) all throw at generation; RFC 6901 pointer escape order verified.
  • Changeset present and correctly typed as patch — no wire-shape change ships.
  • 977-line test suite compiles and runtime-exercises emitted Zod across closed/open/mixed/drift scenarios including the everyStructureClosed mixed-mode gate.

No blocking or medium findings. No author-team gate, gated_paths false, high_risk false. Falls through to row 9.

@bokelley
bokelley merged commit 29c9af4 into main Nov 9, 2026
46 checks passed
@bokelley
bokelley deleted the fix/codegen-open-schema-assumptions branch November 9, 2026 21:05
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