From c9d7b2ce8b365b2e5da0493bd66c7351105d9854 Mon Sep 17 00:00:00 2001 From: "Constantine.mirin" Date: Tue, 27 Oct 2026 11:54:56 +0100 Subject: [PATCH 1/2] fix(schemas): reopen the proposal-flow schemas closed against DR-0019 on 3.2.x 3.2 published the proposal-flow tasks closed from their request and response roots down, and closed the containers they use. DR-0019 keeps request roots, response roots and container objects open, and allows closure only on join-key objects or row envelopes with an ext slot. DR-0036 on main records the reopening as a correction that ships in 3.3 and in a 3.2.x patch (#8501). Set additionalProperties to true at the same 24 locations as main: - the request roots of list_products, request_proposals, refine_proposals, buy_products, accept_proposal, decline_proposals and control_media_buy; - the response roots of list_products, request_proposals, refine_proposals and decline_proposals, and the refine_proposals result row; - the three branches of media-buy-commitment-response and of control-media-buy-response; - canonical-proposal, canonical-product, product-discovery-criteria, proposal-refinement, package-control and opportunity-context. Keep the rejections that closure provided by naming the keys: a root not on canonical-product forbids the legacy format_ids, and the root not of request-proposals-response now also forbids feed_version and pricing_version. Each rejects exactly what 3.2.7 rejected. Adapt the tests that assumed closure: the training agent parity test compares closure with source, the MCP projection test ignores the format_ids prohibition, and the products-only brief test also rejects a fabricated pricing_version. Add a correction note to the 3.1 to 3.2 migration guide and a patch changeset. No fields are added, and commercial-terms stays closed. --- .changeset/reopen-proposal-flow-schemas.md | 12 ++++++++++++ docs/reference/migration/3-1-to-3-2.mdx | 16 ++++++++++++++++ .../unit/product-discovery-schema-parity.test.ts | 9 +++++---- .../schemas/source/core/canonical-product.json | 5 ++++- .../schemas/source/core/canonical-proposal.json | 2 +- .../schemas/source/core/opportunity-context.json | 2 +- .../media-buy/accept-proposal-request.json | 2 +- .../source/media-buy/buy-products-request.json | 2 +- .../media-buy/control-media-buy-request.json | 2 +- .../media-buy/control-media-buy-response.json | 6 +++--- .../media-buy/decline-proposals-request.json | 2 +- .../media-buy/decline-proposals-response.json | 2 +- .../source/media-buy/list-products-request.json | 2 +- .../source/media-buy/list-products-response.json | 2 +- .../media-buy/media-buy-commitment-response.json | 6 +++--- .../source/media-buy/package-control.json | 2 +- .../media-buy/product-discovery-criteria.json | 2 +- .../source/media-buy/proposal-refinement.json | 2 +- .../media-buy/refine-proposals-request.json | 2 +- .../media-buy/refine-proposals-response.json | 4 ++-- .../media-buy/request-proposals-request.json | 2 +- .../media-buy/request-proposals-response.json | 6 ++++-- tests/mcp-schema-projection.test.cjs | 5 ++++- tests/products-only-brief-compatibility.test.cjs | 4 ++++ 24 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 .changeset/reopen-proposal-flow-schemas.md diff --git a/.changeset/reopen-proposal-flow-schemas.md b/.changeset/reopen-proposal-flow-schemas.md new file mode 100644 index 00000000000..c44d2b28736 --- /dev/null +++ b/.changeset/reopen-proposal-flow-schemas.md @@ -0,0 +1,12 @@ +--- +"adcontextprotocol": patch +--- + +Reopen the 3.2 proposal-flow schemas that were closed against DR-0019, as a correction recorded in DR-0036 (#8501). DR-0019 keeps request roots, response roots, and container objects open. These schemas now set `additionalProperties: true` at 24 locations: + +- the request roots of `list_products`, `request_proposals`, `refine_proposals`, `buy_products`, `accept_proposal`, `decline_proposals`, and `control_media_buy`; +- the response roots of `list_products`, `request_proposals`, `refine_proposals`, and `decline_proposals`, and the `refine_proposals` result row; +- the three branches of `media-buy-commitment-response.json` (`buy_products` and `accept_proposal`) and of `control-media-buy-response.json`; +- `core/canonical-proposal.json`, `core/canonical-product.json`, `media-buy/product-discovery-criteria.json`, `media-buy/proposal-refinement.json`, `media-buy/package-control.json`, and `core/opportunity-context.json`. + +Every payload that was valid stays valid. Two rejections that closure provided are now stated by name: `core/canonical-product.json` forbids the legacy `format_ids`, and the `request_proposals` response forbids a root `feed_version` or `pricing_version`. This patch adds no fields. `commercial-terms.json` and the purchases inside it stay closed. diff --git a/docs/reference/migration/3-1-to-3-2.mdx b/docs/reference/migration/3-1-to-3-2.mdx index 2992ef97660..4996e6c2681 100644 --- a/docs/reference/migration/3-1-to-3-2.mdx +++ b/docs/reference/migration/3-1-to-3-2.mdx @@ -324,6 +324,22 @@ Absence of the declaration means use the 3.x compatibility facades. Retry a stateful request with the same tool name, idempotency key, and payload; an idempotency key does not create replay identity across old and new task names. +**Correction: the proposal-flow schemas are open.** 3.2.7 and earlier published +these tasks' request and response schemas closed, with +`additionalProperties: false`. They also closed `canonical-proposal`, +`canonical-product`, `product-discovery-criteria`, `proposal-refinement`, +`package-control`, and `opportunity-context`. +[DR-0019](https://github.com/adcontextprotocol/adcp/blob/main/governance/decisions/DR-0019-closed-row-schemas-deliberate-class.md) +keeps roots and containers open, so later 3.2 patches reopen them. This is a +correction recorded in +[DR-0036](https://github.com/adcontextprotocol/adcp/blob/main/governance/decisions/DR-0036-closed-schemas-stay-closed-within-a-major.md) +([#8501](https://github.com/adcontextprotocol/adcp/issues/8501)). Every payload +that was valid stays valid. The legacy `format_ids` on a canonical product, and a +root `feed_version` or `pricing_version` on a `request_proposals` response, are +still rejected. `commercial_terms` and the purchases inside it stay closed. A +validator on 3.2.7 or earlier still rejects unknown fields on these schemas, so +don't send fields the served release doesn't define. + ### Scope idempotency to mutations AdCP 3.2 requires durable replay for state-mutating requests, not guaranteed diff --git a/server/tests/unit/product-discovery-schema-parity.test.ts b/server/tests/unit/product-discovery-schema-parity.test.ts index 11d90e4ea98..55b91bf7baf 100644 --- a/server/tests/unit/product-discovery-schema-parity.test.ts +++ b/server/tests/unit/product-discovery-schema-parity.test.ts @@ -35,7 +35,7 @@ describe('product discovery MCP schema parity', () => { const source = sourceSchema(fileName); expect(runtime.required ?? []).toEqual(source.required ?? []); expect(runtime.dependencies ?? {}).toEqual(source.dependencies ?? {}); - expect(runtime.additionalProperties).toBe(false); + expect(runtime.additionalProperties).toBe(source.additionalProperties); expect(Object.keys(runtime.properties).sort()).toEqual(Object.keys(source.properties).sort()); if (source.properties.idempotency_key) { expect(runtime.properties.idempotency_key).toMatchObject({ @@ -64,7 +64,7 @@ describe('product discovery MCP schema parity', () => { expect(runtimeRefinement).toMatchObject({ type: sourceRefinement.type, required: sourceRefinement.required, - additionalProperties: false, + additionalProperties: sourceRefinement.additionalProperties, }); // Repository-local refs must be bundled because MCP consumers do not have @@ -78,11 +78,12 @@ describe('product discovery MCP schema parity', () => { uniqueItems: true, items: { enum: expect.arrayContaining(['product_id', 'format_options', 'pricing_options']) }, }); - expect(resolveLocalRef(list, list.properties.criteria)).toMatchObject({ additionalProperties: false }); + expect(resolveLocalRef(list, list.properties.criteria)) + .toMatchObject({ additionalProperties: sourceSchema('product-discovery-criteria').additionalProperties }); expect(resolveLocalRef(refineTool, refineTool.properties.refinements.items)) .toMatchObject({ required: ['proposal_id'], - additionalProperties: false, + additionalProperties: sourceRefinement.additionalProperties, // Definition annotations are intentionally stripped from tools/list; // dispatch still applies the source-schema default semantics. properties: { action: { enum: ['revise', 'finalize'] } }, diff --git a/static/schemas/source/core/canonical-product.json b/static/schemas/source/core/canonical-product.json index c74a7298fa4..f5a207634a5 100644 --- a/static/schemas/source/core/canonical-product.json +++ b/static/schemas/source/core/canonical-product.json @@ -198,5 +198,8 @@ } } ], - "additionalProperties": false + "not": { + "required": ["format_ids"] + }, + "additionalProperties": true } diff --git a/static/schemas/source/core/canonical-proposal.json b/static/schemas/source/core/canonical-proposal.json index 33773a5a908..6bd0012bc68 100644 --- a/static/schemas/source/core/canonical-proposal.json +++ b/static/schemas/source/core/canonical-proposal.json @@ -81,5 +81,5 @@ "then": { "properties": { "commercial_terms": { "required": ["cancellation_terms"] } } } } ], - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/core/opportunity-context.json b/static/schemas/source/core/opportunity-context.json index 8dc3a3c9334..50d54afb3cc 100644 --- a/static/schemas/source/core/opportunity-context.json +++ b/static/schemas/source/core/opportunity-context.json @@ -82,5 +82,5 @@ "then": { "required": ["close_detail"] } } ], - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/accept-proposal-request.json b/static/schemas/source/media-buy/accept-proposal-request.json index adcfec3ebb5..01657bd3a69 100644 --- a/static/schemas/source/media-buy/accept-proposal-request.json +++ b/static/schemas/source/media-buy/accept-proposal-request.json @@ -119,7 +119,7 @@ "ext": { "$ref": "/schemas/core/ext.json" } }, "required": ["idempotency_key", "account", "proposal_id", "proposal_terms_digest"], - "additionalProperties": false, + "additionalProperties": true, "not": { "anyOf": [ { "required": ["packages"] }, diff --git a/static/schemas/source/media-buy/buy-products-request.json b/static/schemas/source/media-buy/buy-products-request.json index 035df8566dc..b5e0dbcdac2 100644 --- a/static/schemas/source/media-buy/buy-products-request.json +++ b/static/schemas/source/media-buy/buy-products-request.json @@ -167,7 +167,7 @@ } } ], - "additionalProperties": false, + "additionalProperties": true, "not": { "anyOf": [ { "required": ["packages"] }, diff --git a/static/schemas/source/media-buy/control-media-buy-request.json b/static/schemas/source/media-buy/control-media-buy-request.json index f33815858da..777918e39da 100644 --- a/static/schemas/source/media-buy/control-media-buy-request.json +++ b/static/schemas/source/media-buy/control-media-buy-request.json @@ -172,7 +172,7 @@ } } ], - "additionalProperties": false, + "additionalProperties": true, "not": { "anyOf": [ { "required": ["creatives"] }, diff --git a/static/schemas/source/media-buy/control-media-buy-response.json b/static/schemas/source/media-buy/control-media-buy-response.json index 7d2ead620e4..088db3cc5eb 100644 --- a/static/schemas/source/media-buy/control-media-buy-response.json +++ b/static/schemas/source/media-buy/control-media-buy-response.json @@ -47,7 +47,7 @@ "replayed": { "type": "boolean", "const": true } }, "required": ["status", "media_buy_id", "revision"], - "additionalProperties": false, + "additionalProperties": true, "not": { "required": ["errors"] } }, { @@ -61,7 +61,7 @@ "replayed": { "type": "boolean", "const": true } }, "required": ["status", "errors"], - "additionalProperties": false, + "additionalProperties": true, "not": { "required": ["media_buy_id"] } }, { @@ -77,7 +77,7 @@ "replayed": { "type": "boolean", "const": true } }, "required": ["status", "task_id"], - "additionalProperties": false, + "additionalProperties": true, "not": { "required": ["media_buy_id"] } } ] diff --git a/static/schemas/source/media-buy/decline-proposals-request.json b/static/schemas/source/media-buy/decline-proposals-request.json index 021f3597a31..97aba89f540 100644 --- a/static/schemas/source/media-buy/decline-proposals-request.json +++ b/static/schemas/source/media-buy/decline-proposals-request.json @@ -48,5 +48,5 @@ } }, "required": ["idempotency_key", "declines"], - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/decline-proposals-response.json b/static/schemas/source/media-buy/decline-proposals-response.json index 2101b0de75f..f6d3fc792eb 100644 --- a/static/schemas/source/media-buy/decline-proposals-response.json +++ b/static/schemas/source/media-buy/decline-proposals-response.json @@ -64,5 +64,5 @@ "ext": { "$ref": "/schemas/core/ext.json" }, "replayed": { "type": "boolean", "const": true } }, - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/list-products-request.json b/static/schemas/source/media-buy/list-products-request.json index 1285513c63d..7247bbd4192 100644 --- a/static/schemas/source/media-buy/list-products-request.json +++ b/static/schemas/source/media-buy/list-products-request.json @@ -82,5 +82,5 @@ } } ], - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/list-products-response.json b/static/schemas/source/media-buy/list-products-response.json index 192f118c285..8ce02cb54e6 100644 --- a/static/schemas/source/media-buy/list-products-response.json +++ b/static/schemas/source/media-buy/list-products-response.json @@ -67,5 +67,5 @@ { "required": ["unchanged"] } ] }, - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/media-buy-commitment-response.json b/static/schemas/source/media-buy/media-buy-commitment-response.json index 163b4a8ea30..7d592bffc61 100644 --- a/static/schemas/source/media-buy/media-buy-commitment-response.json +++ b/static/schemas/source/media-buy/media-buy-commitment-response.json @@ -119,7 +119,7 @@ } }, "required": ["status", "media_buy_id", "revision", "accepted_proposal", "purchase_bindings", "available_actions"], - "additionalProperties": false, + "additionalProperties": true, "not": { "required": ["errors"] } }, { @@ -137,7 +137,7 @@ "replayed": { "type": "boolean", "const": true } }, "required": ["status", "errors"], - "additionalProperties": false, + "additionalProperties": true, "not": { "required": ["media_buy_id"] } }, { @@ -153,7 +153,7 @@ "replayed": { "type": "boolean", "const": true } }, "required": ["status", "task_id"], - "additionalProperties": false, + "additionalProperties": true, "not": { "required": ["media_buy_id"] } } ] diff --git a/static/schemas/source/media-buy/package-control.json b/static/schemas/source/media-buy/package-control.json index b0d4f53eac3..714ec9dfc62 100644 --- a/static/schemas/source/media-buy/package-control.json +++ b/static/schemas/source/media-buy/package-control.json @@ -162,7 +162,7 @@ } } }, - "additionalProperties": false, + "additionalProperties": true, "not": { "anyOf": [ { "required": ["creatives"] }, diff --git a/static/schemas/source/media-buy/product-discovery-criteria.json b/static/schemas/source/media-buy/product-discovery-criteria.json index fa89879e41e..20a575ab63a 100644 --- a/static/schemas/source/media-buy/product-discovery-criteria.json +++ b/static/schemas/source/media-buy/product-discovery-criteria.json @@ -52,5 +52,5 @@ "ext": { "type": "object", "additionalProperties": true } }, "minProperties": 1, - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/proposal-refinement.json b/static/schemas/source/media-buy/proposal-refinement.json index a45cbb82664..9f5bfaa30ea 100644 --- a/static/schemas/source/media-buy/proposal-refinement.json +++ b/static/schemas/source/media-buy/proposal-refinement.json @@ -142,5 +142,5 @@ } ] }, - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/refine-proposals-request.json b/static/schemas/source/media-buy/refine-proposals-request.json index b30409de7e9..c7fc3252e51 100644 --- a/static/schemas/source/media-buy/refine-proposals-request.json +++ b/static/schemas/source/media-buy/refine-proposals-request.json @@ -60,5 +60,5 @@ } }, "required": ["idempotency_key", "refinements"], - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/refine-proposals-response.json b/static/schemas/source/media-buy/refine-proposals-response.json index d43243141ec..7560fbb01a1 100644 --- a/static/schemas/source/media-buy/refine-proposals-response.json +++ b/static/schemas/source/media-buy/refine-proposals-response.json @@ -153,7 +153,7 @@ } } ], - "additionalProperties": false + "additionalProperties": true }, "allOf": [ { @@ -192,5 +192,5 @@ { "required": ["pagination"] } ] }, - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/request-proposals-request.json b/static/schemas/source/media-buy/request-proposals-request.json index 37e54a3ccfc..a2e42cfc51d 100644 --- a/static/schemas/source/media-buy/request-proposals-request.json +++ b/static/schemas/source/media-buy/request-proposals-request.json @@ -80,5 +80,5 @@ } } ], - "additionalProperties": false + "additionalProperties": true } diff --git a/static/schemas/source/media-buy/request-proposals-response.json b/static/schemas/source/media-buy/request-proposals-response.json index 27d2d9414bc..708fa9dac88 100644 --- a/static/schemas/source/media-buy/request-proposals-response.json +++ b/static/schemas/source/media-buy/request-proposals-response.json @@ -244,8 +244,10 @@ { "required": ["refinement_applied"] }, { "required": ["pagination"] }, { "required": ["unchanged"] }, - { "required": ["wholesale_feed_version"] } + { "required": ["wholesale_feed_version"] }, + { "required": ["feed_version"] }, + { "required": ["pricing_version"] } ] }, - "additionalProperties": false + "additionalProperties": true } diff --git a/tests/mcp-schema-projection.test.cjs b/tests/mcp-schema-projection.test.cjs index ea16c1301ea..b1a61db8724 100644 --- a/tests/mcp-schema-projection.test.cjs +++ b/tests/mcp-schema-projection.test.cjs @@ -983,7 +983,10 @@ test('generated MCP projection covers every tool within AdCP safety bounds', () if (['list_products', 'request_proposals', 'refine_proposals'].includes(toolName)) { assert.match(output, /Canonical Product/, `${toolName} output must use the canonical-only Product view`); - assert.doesNotMatch(output, /format_ids|format-id\.json|v1_format_ref|update_packages|"update_media_buy"/, + // The canonical Product forbids the legacy key by name; forbidding it + // does not expose it. + const exposed = output.replaceAll('"not":{"required":["format_ids"]}', ''); + assert.doesNotMatch(exposed, /format_ids|format-id\.json|v1_format_ref|update_packages|"update_media_buy"/, `${toolName} output must not expose the legacy Product graph`); } } diff --git a/tests/products-only-brief-compatibility.test.cjs b/tests/products-only-brief-compatibility.test.cjs index ccf8d21b673..d0d57422c76 100644 --- a/tests/products-only-brief-compatibility.test.cjs +++ b/tests/products-only-brief-compatibility.test.cjs @@ -206,6 +206,10 @@ test('2.5, 3.0, and 3.1 preserve products-only brief discovery through an explic const fabricatedFence = structuredClone(vector.compact_projection); fabricatedFence.feed_version = `synthetic-${vector.source_version}`; assert.equal(validateCompact(fabricatedFence), false, `${vector.source_version} must not fabricate a feed fence`); + + const fabricatedPricingFence = structuredClone(vector.compact_projection); + fabricatedPricingFence.pricing_version = `synthetic-${vector.source_version}`; + assert.equal(validateCompact(fabricatedPricingFence), false, `${vector.source_version} must not fabricate a pricing fence`); } }); From 9d3403297ba99049e7e2a3a5a01aa848288df761 Mon Sep 17 00:00:00 2001 From: "Constantine.mirin" Date: Tue, 27 Oct 2026 21:06:58 +0100 Subject: [PATCH 2/2] fix(schemas): name the keys closure rejected on reopened 3.2.x schemas Reopening the proposal-flow schemas dropped three rejections that tests rely on: - canonical-product rejected every field that only the legacy Product defines, because its description keeps the legacy Product graph absent. Forbid each of those 21 keys by name, including format_ids, is_custom and installments. - The error and submitted branches of media-buy-commitment-response and control-media-buy-response rejected warnings, which only the completed branch carries. Forbid warnings there by name, as create-media-buy and update-media-buy responses already do. Derive the projection test's canonical Product prohibition from the source schema, and check the dual-shape fixture by which schema rejects it rather than by closure's error text. Update the changeset and the migration note to list the named rejections. --- .changeset/reopen-proposal-flow-schemas.md | 2 +- docs/reference/migration/3-1-to-3-2.mdx | 7 ++++-- .../source/core/canonical-product.json | 24 ++++++++++++++++++- .../media-buy/control-media-buy-response.json | 14 +++++++++-- .../media-buy-commitment-response.json | 14 +++++++++-- tests/buyer-fixture-products-schema.test.cjs | 5 +++- tests/mcp-schema-projection.test.cjs | 10 +++++--- 7 files changed, 64 insertions(+), 12 deletions(-) diff --git a/.changeset/reopen-proposal-flow-schemas.md b/.changeset/reopen-proposal-flow-schemas.md index c44d2b28736..b075c7c506f 100644 --- a/.changeset/reopen-proposal-flow-schemas.md +++ b/.changeset/reopen-proposal-flow-schemas.md @@ -9,4 +9,4 @@ Reopen the 3.2 proposal-flow schemas that were closed against DR-0019, as a corr - the three branches of `media-buy-commitment-response.json` (`buy_products` and `accept_proposal`) and of `control-media-buy-response.json`; - `core/canonical-proposal.json`, `core/canonical-product.json`, `media-buy/product-discovery-criteria.json`, `media-buy/proposal-refinement.json`, `media-buy/package-control.json`, and `core/opportunity-context.json`. -Every payload that was valid stays valid. Two rejections that closure provided are now stated by name: `core/canonical-product.json` forbids the legacy `format_ids`, and the `request_proposals` response forbids a root `feed_version` or `pricing_version`. This patch adds no fields. `commercial-terms.json` and the purchases inside it stay closed. +Every payload that was valid stays valid. The rejections that closure provided are now stated by name: `core/canonical-product.json` forbids each field that only the legacy `core/product.json` defines, such as `format_ids`, `is_custom`, and `installments`; the `request_proposals` response forbids a root `feed_version` or `pricing_version`; and the error and submitted branches of `media-buy-commitment-response.json` and `control-media-buy-response.json` forbid `warnings`. This patch adds no fields. `commercial-terms.json` and the purchases inside it stay closed. diff --git a/docs/reference/migration/3-1-to-3-2.mdx b/docs/reference/migration/3-1-to-3-2.mdx index 4996e6c2681..1c136918da1 100644 --- a/docs/reference/migration/3-1-to-3-2.mdx +++ b/docs/reference/migration/3-1-to-3-2.mdx @@ -334,8 +334,11 @@ keeps roots and containers open, so later 3.2 patches reopen them. This is a correction recorded in [DR-0036](https://github.com/adcontextprotocol/adcp/blob/main/governance/decisions/DR-0036-closed-schemas-stay-closed-within-a-major.md) ([#8501](https://github.com/adcontextprotocol/adcp/issues/8501)). Every payload -that was valid stays valid. The legacy `format_ids` on a canonical product, and a -root `feed_version` or `pricing_version` on a `request_proposals` response, are +that was valid stays valid. A field that only the legacy Product defines, +such as `format_ids`, `is_custom`, or `installments`, is still rejected on a +canonical product. A root `feed_version` or `pricing_version` on a +`request_proposals` response, and `warnings` on an error or submitted +`buy_products`, `accept_proposal`, or `control_media_buy` response, are also still rejected. `commercial_terms` and the purchases inside it stay closed. A validator on 3.2.7 or earlier still rejects unknown fields on these schemas, so don't send fields the served release doesn't define. diff --git a/static/schemas/source/core/canonical-product.json b/static/schemas/source/core/canonical-product.json index f5a207634a5..8d10a452a3b 100644 --- a/static/schemas/source/core/canonical-product.json +++ b/static/schemas/source/core/canonical-product.json @@ -199,7 +199,29 @@ } ], "not": { - "required": ["format_ids"] + "anyOf": [ + { "required": ["audience_activation"] }, + { "required": ["cancellation_policy"] }, + { "required": ["conversion_tracking"] }, + { "required": ["creative_policy"] }, + { "required": ["data_provider_signals"] }, + { "required": ["delivery_measurement"] }, + { "required": ["enforced_policies"] }, + { "required": ["format_ids"] }, + { "required": ["included_signals"] }, + { "required": ["installments"] }, + { "required": ["is_custom"] }, + { "required": ["material_submission"] }, + { "required": ["measurement_readiness"] }, + { "required": ["metric_optimization"] }, + { "required": ["outcome_measurement"] }, + { "required": ["product_card"] }, + { "required": ["product_card_detailed"] }, + { "required": ["property_targeting_allowed"] }, + { "required": ["signal_targeting_options"] }, + { "required": ["trusted_match"] }, + { "required": ["vendor_metric_optimization"] } + ] }, "additionalProperties": true } diff --git a/static/schemas/source/media-buy/control-media-buy-response.json b/static/schemas/source/media-buy/control-media-buy-response.json index 088db3cc5eb..cb8b0871479 100644 --- a/static/schemas/source/media-buy/control-media-buy-response.json +++ b/static/schemas/source/media-buy/control-media-buy-response.json @@ -62,7 +62,12 @@ }, "required": ["status", "errors"], "additionalProperties": true, - "not": { "required": ["media_buy_id"] } + "not": { + "anyOf": [ + { "required": ["media_buy_id"] }, + { "required": ["warnings"] } + ] + } }, { "title": "Control Submitted", @@ -78,7 +83,12 @@ }, "required": ["status", "task_id"], "additionalProperties": true, - "not": { "required": ["media_buy_id"] } + "not": { + "anyOf": [ + { "required": ["media_buy_id"] }, + { "required": ["warnings"] } + ] + } } ] } diff --git a/static/schemas/source/media-buy/media-buy-commitment-response.json b/static/schemas/source/media-buy/media-buy-commitment-response.json index 7d592bffc61..069406a7e86 100644 --- a/static/schemas/source/media-buy/media-buy-commitment-response.json +++ b/static/schemas/source/media-buy/media-buy-commitment-response.json @@ -138,7 +138,12 @@ }, "required": ["status", "errors"], "additionalProperties": true, - "not": { "required": ["media_buy_id"] } + "not": { + "anyOf": [ + { "required": ["media_buy_id"] }, + { "required": ["warnings"] } + ] + } }, { "title": "Commitment Submitted", @@ -154,7 +159,12 @@ }, "required": ["status", "task_id"], "additionalProperties": true, - "not": { "required": ["media_buy_id"] } + "not": { + "anyOf": [ + { "required": ["media_buy_id"] }, + { "required": ["warnings"] } + ] + } } ] } diff --git a/tests/buyer-fixture-products-schema.test.cjs b/tests/buyer-fixture-products-schema.test.cjs index 1a0ccfda28b..22497e095c7 100644 --- a/tests/buyer-fixture-products-schema.test.cjs +++ b/tests/buyer-fixture-products-schema.test.cjs @@ -111,8 +111,11 @@ test('a product valid under only one shape is rejected', () => { }, }; assert.deepEqual(productErrors(ajv, dualShape), []); + // installments exists only on the legacy Product, so the canonical Product + // rejects it by name. const errors = productErrors(ajv, { ...dualShape, installments: [] }); - assert.ok(errors.some((e) => e.startsWith('/schemas/core/canonical-product.json') && e.includes('installments')), errors.join('\n')); + assert.ok(errors.some((e) => e.startsWith('/schemas/core/canonical-product.json')), errors.join('\n')); + assert.ok(!errors.some((e) => e.startsWith('/schemas/core/product.json')), errors.join('\n')); const missing = productErrors(ajv, { product_id: 'p', name: 'P' }); assert.ok(missing.some((e) => e.startsWith('/schemas/core/product.json')), missing.join('\n')); }); diff --git a/tests/mcp-schema-projection.test.cjs b/tests/mcp-schema-projection.test.cjs index b1a61db8724..0580a5e0728 100644 --- a/tests/mcp-schema-projection.test.cjs +++ b/tests/mcp-schema-projection.test.cjs @@ -49,6 +49,10 @@ const STORYBOARD_DIR = path.join(REPO_ROOT, 'static', 'compliance', 'source'); const LATEST_DIR = path.join(REPO_ROOT, 'dist', 'schemas', 'latest'); const PROJECTION_DIR = path.join(LATEST_DIR, 'mcp', MCP_PROTOCOL_VERSION); const PRODUCTION_PROFILE_DIR = path.join(PROJECTION_DIR, 'profiles', 'production'); +// The canonical Product's `not`, as the projection serializes it. +const CANONICAL_PRODUCT_PROHIBITION = JSON.stringify({ + not: JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'core', 'canonical-product.json'), 'utf8')).not, +}).slice(1, -1); // The generated profiles describe the active surface of the package's release // line. Before the line's GA (x.y.0-rc.N) that is the x.y.0 surface the tool // manifest declares via added_in; once the line has a stable release it is the @@ -983,9 +987,9 @@ test('generated MCP projection covers every tool within AdCP safety bounds', () if (['list_products', 'request_proposals', 'refine_proposals'].includes(toolName)) { assert.match(output, /Canonical Product/, `${toolName} output must use the canonical-only Product view`); - // The canonical Product forbids the legacy key by name; forbidding it - // does not expose it. - const exposed = output.replaceAll('"not":{"required":["format_ids"]}', ''); + // The canonical Product forbids the legacy Product's keys by name; + // forbidding them does not expose them. + const exposed = output.replaceAll(CANONICAL_PRODUCT_PROHIBITION, ''); assert.doesNotMatch(exposed, /format_ids|format-id\.json|v1_format_ref|update_packages|"update_media_buy"/, `${toolName} output must not expose the legacy Product graph`); }