diff --git a/README.md b/README.md index bcb5e2be..49b2ea06 100644 --- a/README.md +++ b/README.md @@ -276,14 +276,14 @@ async with ADCPMultiAgentClient( ## AdCP version support -The 6.x line is built against **AdCP 3.1.1 stable** and natively validates +The 6.x line is built against **AdCP 3.1.8 stable** and natively validates both AdCP 3.0 and 3.1 wire shapes. Check the versions at runtime: ```python import adcp adcp.get_adcp_sdk_version() # SDK package version, e.g. "6.4.1" -adcp.get_adcp_spec_version() # AdCP spec this build targets, e.g. "3.1.1" +adcp.get_adcp_spec_version() # AdCP spec this build targets, e.g. "3.1.8" ``` If you talk to an agent on a newer spec than this SDK validates, the response diff --git a/SCHEMA_DELTAS.md b/SCHEMA_DELTAS.md index 6c2706c3..3fbd9bf1 100644 --- a/SCHEMA_DELTAS.md +++ b/SCHEMA_DELTAS.md @@ -1,37 +1,15 @@ # Generated-types delta -## Files added - -- `trusted_match/available_package.py` — AvailablePackage -- `trusted_match/context_match_request.py` — ArtifactRef, ContextMatchRequest, ContextSignals, Geo, Keyword, Metro, Sentiment, Type -- `trusted_match/context_match_response.py` — ContextMatchResponse, Signals, TargetingKv -- `trusted_match/error.py` — Code, TmpError -- `trusted_match/identity_match_request.py` — Attestation, Consent, Identity, IdentityMatchRequest, SealedCredential, VerificationLevel -- `trusted_match/identity_match_response.py` — IdentityMatchResponse, TmpxMacro, TmpxProviders -- `trusted_match/offer.py` — Offer -- `trusted_match/offer_price.py` — Model, OfferPrice -- `trusted_match/provider_registration.py` — Country, Status, TmpProviderRegistration, TmpProviderRegistration1, TmpProviderRegistration2, TmpxMacro - -## Files removed - -- `tmp/available_package.py` — AvailablePackage -- `tmp/context_match_request.py` — ArtifactRef, ContextMatchRequest, ContextSignals, Geo, Keyword, Metro, Sentiment, Type -- `tmp/context_match_response.py` — ContextMatchResponse, Signals, TargetingKv -- `tmp/error.py` — Code, TmpError -- `tmp/identity_match_request.py` — Attestation, Consent, Identity, IdentityMatchRequest, SealedCredential, VerificationLevel -- `tmp/identity_match_response.py` — IdentityMatchResponse -- `tmp/offer.py` — Offer -- `tmp/offer_price.py` — Model, OfferPrice -- `tmp/provider_registration.py` — Country, Status, TmpProviderRegistration, TmpProviderRegistration1, TmpProviderRegistration2 - ## Field changes - `bundled/protocol/get_adcp_capabilities_response.py` - - `MediaBuy`: `+governance_aware` -- `core/registry_feed_response.py` - - **classes added**: Freshness - - `RegistryFeedResponse`: `+freshness` -- `manifest_schema.py` - - `Protocol`: `+trusted_match` `-tmp` + - **classes added**: EventType2, Features2, Idempotency3, MraidVersion2, SupportedTarget3, Type12, VastVersion2 + - **classes removed**: EventType1, Features1, Idempotency1, MraidVersion1, SupportedTarget1, Type9, VastVersion1 +- `creative/list_creatives_response.py` + - **classes added**: Creatives, Creatives1 + - **classes removed**: Creative - `protocol/get_adcp_capabilities_response.py` - - `MediaBuy`: `+governance_aware` + - **classes added**: Idempotency1, SupportedTarget1, Type9 + - **classes removed**: Idempotency3, SupportedTarget3, Type12 +- `signals/activate_signal_request.py` + - `ActivateSignalRequest`: `+governance_context` diff --git a/schemas/cache/3.1/adagents.json b/schemas/cache/3.1/adagents.json index 316934eb..f1d8b678 100644 --- a/schemas/cache/3.1/adagents.json +++ b/schemas/cache/3.1/adagents.json @@ -786,12 +786,12 @@ ], "examples": [ { - "$schema": "/schemas/3.1.1/adagents.json", + "$schema": "/schemas/3.1.8/adagents.json", "authoritative_location": "https://cdn.example.com/adagents/v2/adagents.json", "last_updated": "2025-01-15T10:00:00Z" }, { - "$schema": "/schemas/3.1.1/adagents.json", + "$schema": "/schemas/3.1.8/adagents.json", "properties": [ { "property_id": "example_site", @@ -872,7 +872,7 @@ "last_updated": "2025-01-10T12:00:00Z" }, { - "$schema": "/schemas/3.1.1/adagents.json", + "$schema": "/schemas/3.1.8/adagents.json", "contact": { "name": "Meta Advertising Operations", "email": "adops@meta.com", @@ -981,7 +981,7 @@ "last_updated": "2025-01-10T15:30:00Z" }, { - "$schema": "/schemas/3.1.1/adagents.json", + "$schema": "/schemas/3.1.8/adagents.json", "contact": { "name": "Tumblr Advertising" }, @@ -1020,7 +1020,7 @@ "last_updated": "2025-01-10T16:00:00Z" }, { - "$schema": "/schemas/3.1.1/adagents.json", + "$schema": "/schemas/3.1.8/adagents.json", "contact": { "name": "Example Third-Party Sales Agent", "email": "sales@agent.example", @@ -1085,7 +1085,7 @@ "last_updated": "2025-01-10T17:00:00Z" }, { - "$schema": "/schemas/3.1.1/adagents.json", + "$schema": "/schemas/3.1.8/adagents.json", "contact": { "name": "Premium News Publisher", "email": "adops@news.example.com", @@ -1160,7 +1160,7 @@ "last_updated": "2025-01-10T18:00:00Z" }, { - "$schema": "/schemas/3.1.1/adagents.json", + "$schema": "/schemas/3.1.8/adagents.json", "contact": { "name": "Polk Automotive Data", "email": "partnerships@polk.com", diff --git a/schemas/cache/3.1/brand.json b/schemas/cache/3.1/brand.json index 547f29cc..3606330d 100644 --- a/schemas/cache/3.1/brand.json +++ b/schemas/cache/3.1/brand.json @@ -2720,16 +2720,16 @@ ], "examples": [ { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "authoritative_location": "https://adcontextprotocol.org/brand/abc123/brand.json" }, { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "house": "nikeinc.com", "note": "Redirect to house domain for full brand portfolio" }, { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "version": "1.0", "agents": [ { @@ -2740,7 +2740,7 @@ ] }, { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "version": "1.0", "house": { "domain": "pg.com", @@ -3041,7 +3041,7 @@ "last_updated": "2026-01-15T10:00:00Z" }, { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "version": "1.0", "house": { "domain": "nikeinc.com", @@ -3222,7 +3222,7 @@ "last_updated": "2026-01-15T10:00:00Z" }, { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "version": "1.0", "house": { "domain": "mediavine.com", @@ -3273,7 +3273,7 @@ "last_updated": "2026-01-15T10:00:00Z" }, { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "version": "1.0", "house": { "domain": "nikeinc.com", @@ -3310,7 +3310,7 @@ "last_updated": "2026-01-15T10:00:00Z" }, { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "version": "1.0", "house": { "domain": "wpp.com", @@ -3335,7 +3335,7 @@ "last_updated": "2026-01-15T10:00:00Z" }, { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "version": "1.0", "id": "converse", "names": [ @@ -3355,7 +3355,7 @@ "last_updated": "2026-01-15T10:00:00Z" }, { - "$schema": "/schemas/3.1.1/brand.json", + "$schema": "/schemas/3.1.8/brand.json", "version": "1.0", "id": "patagonia", "names": [ diff --git a/schemas/cache/3.1/bundled/content-standards/calibrate-content-request.json b/schemas/cache/3.1/bundled/content-standards/calibrate-content-request.json index 682a4137..cd3d9a84 100644 --- a/schemas/cache/3.1/bundled/content-standards/calibrate-content-request.json +++ b/schemas/cache/3.1/bundled/content-standards/calibrate-content-request.json @@ -2279,7 +2279,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.361Z", + "generatedAt": "2026-07-28T13:04:06.248Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/calibrate-content-response.json b/schemas/cache/3.1/bundled/content-standards/calibrate-content-response.json index 524f05d8..13065242 100644 --- a/schemas/cache/3.1/bundled/content-standards/calibrate-content-response.json +++ b/schemas/cache/3.1/bundled/content-standards/calibrate-content-response.json @@ -667,7 +667,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.364Z", + "generatedAt": "2026-07-28T13:04:06.250Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/create-content-standards-request.json b/schemas/cache/3.1/bundled/content-standards/create-content-standards-request.json index be1a7550..b23ece0b 100644 --- a/schemas/cache/3.1/bundled/content-standards/create-content-standards-request.json +++ b/schemas/cache/3.1/bundled/content-standards/create-content-standards-request.json @@ -4697,7 +4697,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.377Z", + "generatedAt": "2026-07-28T13:04:06.264Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/create-content-standards-response.json b/schemas/cache/3.1/bundled/content-standards/create-content-standards-response.json index 61d42882..29825380 100644 --- a/schemas/cache/3.1/bundled/content-standards/create-content-standards-response.json +++ b/schemas/cache/3.1/bundled/content-standards/create-content-standards-response.json @@ -603,7 +603,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.379Z", + "generatedAt": "2026-07-28T13:04:06.271Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/get-content-standards-request.json b/schemas/cache/3.1/bundled/content-standards/get-content-standards-request.json index 708aa8cf..7a86a95d 100644 --- a/schemas/cache/3.1/bundled/content-standards/get-content-standards-request.json +++ b/schemas/cache/3.1/bundled/content-standards/get-content-standards-request.json @@ -51,7 +51,7 @@ "standards_id" ], "_bundled": { - "generatedAt": "2026-06-30T19:14:39.379Z", + "generatedAt": "2026-07-28T13:04:06.272Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/get-content-standards-response.json b/schemas/cache/3.1/bundled/content-standards/get-content-standards-response.json index 71990c70..c3f349c9 100644 --- a/schemas/cache/3.1/bundled/content-standards/get-content-standards-response.json +++ b/schemas/cache/3.1/bundled/content-standards/get-content-standards-response.json @@ -5454,7 +5454,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.397Z", + "generatedAt": "2026-07-28T13:04:06.284Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/get-media-buy-artifacts-request.json b/schemas/cache/3.1/bundled/content-standards/get-media-buy-artifacts-request.json index 61b41171..5967323b 100644 --- a/schemas/cache/3.1/bundled/content-standards/get-media-buy-artifacts-request.json +++ b/schemas/cache/3.1/bundled/content-standards/get-media-buy-artifacts-request.json @@ -743,7 +743,7 @@ "media_buy_id" ], "_bundled": { - "generatedAt": "2026-06-30T19:14:39.401Z", + "generatedAt": "2026-07-28T13:04:06.290Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/get-media-buy-artifacts-response.json b/schemas/cache/3.1/bundled/content-standards/get-media-buy-artifacts-response.json index 02b7ee56..81ddfdee 100644 --- a/schemas/cache/3.1/bundled/content-standards/get-media-buy-artifacts-response.json +++ b/schemas/cache/3.1/bundled/content-standards/get-media-buy-artifacts-response.json @@ -2921,7 +2921,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.405Z", + "generatedAt": "2026-07-28T13:04:06.295Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/list-content-standards-request.json b/schemas/cache/3.1/bundled/content-standards/list-content-standards-request.json index bd64b6a8..7ca73273 100644 --- a/schemas/cache/3.1/bundled/content-standards/list-content-standards-request.json +++ b/schemas/cache/3.1/bundled/content-standards/list-content-standards-request.json @@ -134,7 +134,7 @@ }, "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.406Z", + "generatedAt": "2026-07-28T13:04:06.297Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/list-content-standards-response.json b/schemas/cache/3.1/bundled/content-standards/list-content-standards-response.json index 0a44c942..3daadec8 100644 --- a/schemas/cache/3.1/bundled/content-standards/list-content-standards-response.json +++ b/schemas/cache/3.1/bundled/content-standards/list-content-standards-response.json @@ -5483,7 +5483,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.422Z", + "generatedAt": "2026-07-28T13:04:06.304Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/update-content-standards-request.json b/schemas/cache/3.1/bundled/content-standards/update-content-standards-request.json index 03c6a10c..d8611392 100644 --- a/schemas/cache/3.1/bundled/content-standards/update-content-standards-request.json +++ b/schemas/cache/3.1/bundled/content-standards/update-content-standards-request.json @@ -4686,7 +4686,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.428Z", + "generatedAt": "2026-07-28T13:04:06.311Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/update-content-standards-response.json b/schemas/cache/3.1/bundled/content-standards/update-content-standards-response.json index ed0b5687..83b691a3 100644 --- a/schemas/cache/3.1/bundled/content-standards/update-content-standards-response.json +++ b/schemas/cache/3.1/bundled/content-standards/update-content-standards-response.json @@ -618,7 +618,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.430Z", + "generatedAt": "2026-07-28T13:04:06.313Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/validate-content-delivery-request.json b/schemas/cache/3.1/bundled/content-standards/validate-content-delivery-request.json index 73573434..0aef9cfe 100644 --- a/schemas/cache/3.1/bundled/content-standards/validate-content-delivery-request.json +++ b/schemas/cache/3.1/bundled/content-standards/validate-content-delivery-request.json @@ -2333,7 +2333,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.432Z", + "generatedAt": "2026-07-28T13:04:06.316Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/content-standards/validate-content-delivery-response.json b/schemas/cache/3.1/bundled/content-standards/validate-content-delivery-response.json index 47f643a5..e742b70a 100644 --- a/schemas/cache/3.1/bundled/content-standards/validate-content-delivery-response.json +++ b/schemas/cache/3.1/bundled/content-standards/validate-content-delivery-response.json @@ -695,7 +695,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.434Z", + "generatedAt": "2026-07-28T13:04:06.317Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/core/tasks-get-request.json b/schemas/cache/3.1/bundled/core/tasks-get-request.json index e043e34b..295d669e 100644 --- a/schemas/cache/3.1/bundled/core/tasks-get-request.json +++ b/schemas/cache/3.1/bundled/core/tasks-get-request.json @@ -737,7 +737,7 @@ } ], "_bundled": { - "generatedAt": "2026-06-30T19:14:39.436Z", + "generatedAt": "2026-07-28T13:04:06.319Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/core/tasks-get-response.json b/schemas/cache/3.1/bundled/core/tasks-get-response.json index c8cfb442..993d690e 100644 --- a/schemas/cache/3.1/bundled/core/tasks-get-response.json +++ b/schemas/cache/3.1/bundled/core/tasks-get-response.json @@ -25116,6 +25116,44 @@ "required": [ "type" ], + "allOf": [ + { + "$comment": "rate quantifies a percent_remaining fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "percent_remaining" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "rate" + ] + } + }, + { + "$comment": "amount quantifies a fixed_fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "fixed_fee" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "amount" + ] + } + } + ], "additionalProperties": true } }, @@ -60468,6 +60506,44 @@ "required": [ "type" ], + "allOf": [ + { + "$comment": "rate quantifies a percent_remaining fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "percent_remaining" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "rate" + ] + } + }, + { + "$comment": "amount quantifies a fixed_fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "fixed_fee" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "amount" + ] + } + } + ], "additionalProperties": true } }, @@ -88399,8 +88475,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -88594,8 +88673,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -89967,8 +90049,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -90162,8 +90247,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -158007,7 +158095,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.703Z", + "generatedAt": "2026-07-28T13:04:06.584Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/core/tasks-list-request.json b/schemas/cache/3.1/bundled/core/tasks-list-request.json index b8c1c6e2..e7956eb5 100644 --- a/schemas/cache/3.1/bundled/core/tasks-list-request.json +++ b/schemas/cache/3.1/bundled/core/tasks-list-request.json @@ -993,7 +993,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.747Z", + "generatedAt": "2026-07-28T13:04:06.655Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/core/tasks-list-response.json b/schemas/cache/3.1/bundled/core/tasks-list-response.json index 8cecfec0..f0b6aa08 100644 --- a/schemas/cache/3.1/bundled/core/tasks-list-response.json +++ b/schemas/cache/3.1/bundled/core/tasks-list-response.json @@ -665,7 +665,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.749Z", + "generatedAt": "2026-07-28T13:04:06.657Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/get-creative-delivery-request.json b/schemas/cache/3.1/bundled/creative/get-creative-delivery-request.json index 7a9b41b9..b4368432 100644 --- a/schemas/cache/3.1/bundled/creative/get-creative-delivery-request.json +++ b/schemas/cache/3.1/bundled/creative/get-creative-delivery-request.json @@ -754,7 +754,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.749Z", + "generatedAt": "2026-07-28T13:04:06.658Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/get-creative-delivery-response.json b/schemas/cache/3.1/bundled/creative/get-creative-delivery-response.json index 31edc2d8..ac8ba6b8 100644 --- a/schemas/cache/3.1/bundled/creative/get-creative-delivery-response.json +++ b/schemas/cache/3.1/bundled/creative/get-creative-delivery-response.json @@ -562,8 +562,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -757,8 +760,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -2122,8 +2128,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -2317,8 +2326,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -23558,7 +23570,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.774Z", + "generatedAt": "2026-07-28T13:04:06.691Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/get-creative-features-request.json b/schemas/cache/3.1/bundled/creative/get-creative-features-request.json index cb199559..b95bdad5 100644 --- a/schemas/cache/3.1/bundled/creative/get-creative-features-request.json +++ b/schemas/cache/3.1/bundled/creative/get-creative-features-request.json @@ -20170,7 +20170,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.797Z", + "generatedAt": "2026-07-28T13:04:06.726Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/get-creative-features-response.json b/schemas/cache/3.1/bundled/creative/get-creative-features-response.json index eeb548bf..86c2a259 100644 --- a/schemas/cache/3.1/bundled/creative/get-creative-features-response.json +++ b/schemas/cache/3.1/bundled/creative/get-creative-features-response.json @@ -836,7 +836,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.801Z", + "generatedAt": "2026-07-28T13:04:06.732Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/list-creative-formats-request.json b/schemas/cache/3.1/bundled/creative/list-creative-formats-request.json index 538cdfd6..24421bdf 100644 --- a/schemas/cache/3.1/bundled/creative/list-creative-formats-request.json +++ b/schemas/cache/3.1/bundled/creative/list-creative-formats-request.json @@ -988,7 +988,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.803Z", + "generatedAt": "2026-07-28T13:04:06.734Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/list-creative-formats-response.json b/schemas/cache/3.1/bundled/creative/list-creative-formats-response.json index 89f05bf2..b6445cef 100644 --- a/schemas/cache/3.1/bundled/creative/list-creative-formats-response.json +++ b/schemas/cache/3.1/bundled/creative/list-creative-formats-response.json @@ -13645,7 +13645,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.811Z", + "generatedAt": "2026-07-28T13:04:06.747Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/list-creatives-request.json b/schemas/cache/3.1/bundled/creative/list-creatives-request.json index f1528f69..93a09fed 100644 --- a/schemas/cache/3.1/bundled/creative/list-creatives-request.json +++ b/schemas/cache/3.1/bundled/creative/list-creatives-request.json @@ -1666,7 +1666,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.816Z", + "generatedAt": "2026-07-28T13:04:06.755Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/list-creatives-response.json b/schemas/cache/3.1/bundled/creative/list-creatives-response.json index 9bfcc2b3..0f32692b 100644 --- a/schemas/cache/3.1/bundled/creative/list-creatives-response.json +++ b/schemas/cache/3.1/bundled/creative/list-creatives-response.json @@ -1738,7 +1738,7 @@ }, "format_id": { "title": "Format Reference (Structured Object)", - "description": "Format identifier specifying which format this creative conforms to", + "description": "Legacy named-format path. Structured format identifier specifying which legacy format this creative conforms to. Mutually exclusive with `format_kind`.", "x-entity": "creative_format", "type": "object", "properties": { @@ -1782,6 +1782,86 @@ ] } }, + "format_kind": { + "title": "Canonical Format Kind", + "description": "3.1+ canonical-format path. The canonical format kind this creative targets. Mutually exclusive with `format_id`.", + "type": "string", + "enum": [ + "image", + "html5", + "display_tag", + "image_carousel", + "video_hosted", + "video_vast", + "audio_hosted", + "audio_daast", + "sponsored_placement", + "native_in_feed", + "responsive_creative", + "agent_placement", + "custom" + ] + }, + "format_option_ref": { + "title": "Format Option Reference", + "description": "Optional 3.1+ reference to the concrete canonical format option this creative targets. Required when `format_kind` alone is ambiguous in the enclosing product context.", + "type": "object", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "title": "Publisher Catalog Format Option Reference", + "description": "Selects a publisher-catalog-backed product format option by publisher domain and format option ID.", + "type": "object", + "properties": { + "scope": { + "type": "string", + "const": "publisher", + "description": "Reference resolves against the named publisher's adagents.json top-level `formats[]` catalog." + }, + "publisher_domain": { + "type": "string", + "description": "Publisher domain where the adagents.json declaring this format option is hosted.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "format_option_id": { + "type": "string", + "description": "Stable format option ID from the publisher's adagents.json top-level `formats[]`, matching a publisher-catalog-backed entry in the target product's `format_options[]`." + } + }, + "required": [ + "scope", + "publisher_domain", + "format_option_id" + ], + "additionalProperties": true + }, + { + "title": "Product-Local Format Option Reference", + "description": "Selects a product-local format option by ID within the enclosing package/product context. This branch deliberately forbids `publisher_domain` (`publisher_domain: false` in the schema) because product-local references are namespaced by the enclosing product only; include `scope: \"publisher\"` when the selector must cross into a publisher catalog.", + "type": "object", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Reference resolves only against the target product's inline `format_options[]`." + }, + "format_option_id": { + "type": "string", + "description": "Stable format option ID from the target product's inline `format_options[]`." + }, + "publisher_domain": false + }, + "required": [ + "scope", + "format_option_id" + ], + "additionalProperties": true + } + ], + "additionalProperties": true + }, "status": { "title": "Creative Status", "description": "Current approval status of the creative", @@ -20223,11 +20303,34 @@ "required": [ "creative_id", "name", - "format_id", "status", "created_date", "updated_date" ], + "oneOf": [ + { + "title": "Legacy listed creative (named-format reference)", + "required": [ + "format_id" + ], + "not": { + "required": [ + "format_kind" + ] + } + }, + { + "title": "3.1+ listed creative (canonical format kind)", + "required": [ + "format_kind" + ], + "not": { + "required": [ + "format_id" + ] + } + } + ], "additionalProperties": true } }, @@ -21305,7 +21408,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.833Z", + "generatedAt": "2026-07-28T13:04:06.777Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/list-transformers-request.json b/schemas/cache/3.1/bundled/creative/list-transformers-request.json index 92e96713..b9b5f352 100644 --- a/schemas/cache/3.1/bundled/creative/list-transformers-request.json +++ b/schemas/cache/3.1/bundled/creative/list-transformers-request.json @@ -887,7 +887,7 @@ }, "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.838Z", + "generatedAt": "2026-07-28T13:04:06.785Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/list-transformers-response.json b/schemas/cache/3.1/bundled/creative/list-transformers-response.json index 62803524..cd1457fa 100644 --- a/schemas/cache/3.1/bundled/creative/list-transformers-response.json +++ b/schemas/cache/3.1/bundled/creative/list-transformers-response.json @@ -1413,7 +1413,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.839Z", + "generatedAt": "2026-07-28T13:04:06.787Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/preview-creative-request.json b/schemas/cache/3.1/bundled/creative/preview-creative-request.json index 8feb8e01..43550c60 100644 --- a/schemas/cache/3.1/bundled/creative/preview-creative-request.json +++ b/schemas/cache/3.1/bundled/creative/preview-creative-request.json @@ -38730,7 +38730,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.874Z", + "generatedAt": "2026-07-28T13:04:06.828Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/preview-creative-response.json b/schemas/cache/3.1/bundled/creative/preview-creative-response.json index 5d8da5f0..c7a4995d 100644 --- a/schemas/cache/3.1/bundled/creative/preview-creative-response.json +++ b/schemas/cache/3.1/bundled/creative/preview-creative-response.json @@ -21080,7 +21080,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.900Z", + "generatedAt": "2026-07-28T13:04:06.862Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/sync-creatives-request.json b/schemas/cache/3.1/bundled/creative/sync-creatives-request.json index 2d48bb2f..4a46761e 100644 --- a/schemas/cache/3.1/bundled/creative/sync-creatives-request.json +++ b/schemas/cache/3.1/bundled/creative/sync-creatives-request.json @@ -19043,7 +19043,7 @@ "dry_run": { "type": "boolean", "default": false, - "description": "When true, preview changes without applying them. Returns what would be created/updated/deleted." + "description": "When true, rehearse this sync_creatives operation without applying it. Validates the actual trafficking request in the seller's current context, including library upsert semantics, creative IDs, assignments, account-scoped gates, and seller policies, then returns what would be created/updated/deleted. This is distinct from validate_input, which only validates manifest structure against canonical/product format targets." }, "validation_mode": { "title": "Validation Mode", @@ -19885,7 +19885,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.924Z", + "generatedAt": "2026-07-28T13:04:06.889Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/sync-creatives-response.json b/schemas/cache/3.1/bundled/creative/sync-creatives-response.json index 1c8bf848..d58b98c8 100644 --- a/schemas/cache/3.1/bundled/creative/sync-creatives-response.json +++ b/schemas/cache/3.1/bundled/creative/sync-creatives-response.json @@ -2448,7 +2448,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.932Z", + "generatedAt": "2026-07-28T13:04:06.898Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/validate-input-request.json b/schemas/cache/3.1/bundled/creative/validate-input-request.json index a48eed64..aeb1b5d7 100644 --- a/schemas/cache/3.1/bundled/creative/validate-input-request.json +++ b/schemas/cache/3.1/bundled/creative/validate-input-request.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Validate Input Request", - "description": "Request payload for the validate_input task. Lets buyers dry-run a creative manifest against canonical formats and/or specific products before committing to a render. Cheaper than preview_creative (no synthesis cost). Used by build_creative internally to validate inputs before producing output. For genuinely nondeterministic generative platforms (Veo/Sora/Runway-class) where predictive validation is impossible, the platform's own post-synthesis QA loop applies \u2014 validate_input is the predictable-case primitive.\n\nThe `targets[]` array is a discriminated list of validation targets, mirroring the response shape on `validate-input-result.json#target`. Each entry has a `kind` (canonical | product | third_party_format) plus a kind-specific identifier. Discriminated-by-kind on both sides eliminates a wire-shape mismatch: a previous draft used `format_ids: string[]` for canonical names alongside `product_ids: string[]`, which collided with `Product.format_ids: FormatId[]` ({agent_url, id}) \u2014 codegen would emit the same field name with two different types. Discriminated `targets[]` makes the intent explicit and codegen-clean.", + "description": "Request payload for the validate_input task. Lets buyers preflight a creative manifest against canonical formats and/or specific products before committing to a render or other expensive creative-production step. It validates manifest structure and target format constraints only: slot counts, asset types, parameter ranges, and format-shape constraints. It does not rehearse a seller's library mutation, creative_id upsert semantics, package assignments, account authorization, active-delivery protections, or other trafficking-time gates. When the question is final seller acceptance for a creative that is ready to traffic, call sync_creatives directly; use dry_run: true for a non-mutating rehearsal of that operation. Used by build_creative internally to validate inputs before producing output. For genuinely nondeterministic generative platforms (Veo/Sora/Runway-class) where predictive validation is impossible, the platform's own post-synthesis QA loop applies \u2014 validate_input is the predictable-case primitive.\n\nThe `targets[]` array is a discriminated list of validation targets, mirroring the response shape on `validate-input-result.json#target`. Each entry has a `kind` (canonical | product | third_party_format) plus a kind-specific identifier. Discriminated-by-kind on both sides eliminates a wire-shape mismatch: a previous draft used `format_ids: string[]` for canonical names alongside `product_ids: string[]`, which collided with `Product.format_ids: FormatId[]` ({agent_url, id}) \u2014 codegen would emit the same field name with two different types. Discriminated `targets[]` makes the intent explicit and codegen-clean.", "type": "object", "required": [ "manifest" @@ -20068,7 +20068,7 @@ "additionalProperties": true, "examples": [ { - "description": "Dry-run a video manifest against canonical video_hosted and a specific Meta product in one round-trip", + "description": "Preflight a video manifest against canonical video_hosted and a specific Meta product in one round-trip", "data": { "manifest": { "format_kind": "video_hosted", @@ -20724,7 +20724,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:39.961Z", + "generatedAt": "2026-07-28T13:04:06.920Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/creative/validate-input-response.json b/schemas/cache/3.1/bundled/creative/validate-input-response.json index 8826c65d..e221b9d7 100644 --- a/schemas/cache/3.1/bundled/creative/validate-input-response.json +++ b/schemas/cache/3.1/bundled/creative/validate-input-response.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Validate Input Response", - "description": "Response payload for the validate_input task. Returns per-target validation results \u2014 one entry per format_id or product_id requested. Each result carries a `result_kind` discriminator (`validated_pass` / `validated_fail` / `unvalidatable_nondeterministic`) so callers can branch on three meaningfully different outcomes. The `predicted` field on violations carries the platform's pre-flight estimate (e.g., predicted audio duration from text-length analysis), NOT the actual output \u2014 there is no protocol state for orphaned out-of-spec artifacts. For nondeterministic generative platforms (Veo / Sora / Runway-class with `synthesis_nondeterministic: true`), the result_kind is `unvalidatable_nondeterministic` \u2014 predictive validation is impossible, the platform's post-synthesis QA loop applies on `build_creative`, and out-of-spec output never reaches this surface (instead `build_creative` returns task_failed with synthesis_failed reason).\n\nThe `ValidateInputResult` type is split into its own schema (`/schemas/creative/validate-input-result.json`) rather than inlined here because the same per-target shape is intended for reuse by adjacent async-validation surfaces (planned: per-batch result envelopes on `build_creative` async paths, and asynchronous canonical-against-product validation in `sync_creatives`). Producers that only need the synchronous batch shape today MAY treat the split as YAGNI, but the schema reuse anchors the violation/retry shape so downstream surfaces don't drift.", + "description": "Response payload for the validate_input task. Returns per-target manifest validation results \u2014 one entry per canonical, product, or third-party target requested. Each result carries a `result_kind` discriminator (`validated_pass` / `validated_fail` / `unvalidatable_nondeterministic`) so callers can branch on three meaningfully different outcomes. A pass means the manifest is structurally valid for that target; it does not mean a later sync_creatives call will be accepted, because trafficking-time gates such as account authorization, creative_id upsert state, package assignments, active-delivery protections, and seller review policy are outside validate_input's scope. The `predicted` field on violations carries the platform's pre-flight estimate (e.g., predicted audio duration from text-length analysis), NOT the actual output \u2014 there is no protocol state for orphaned out-of-spec artifacts. For nondeterministic generative platforms (Veo / Sora / Runway-class with `synthesis_nondeterministic: true`), the result_kind is `unvalidatable_nondeterministic` \u2014 predictive validation is impossible, the platform's post-synthesis QA loop applies on `build_creative`, and out-of-spec output never reaches this surface (instead `build_creative` returns task_failed with synthesis_failed reason).\n\nThe `ValidateInputResult` type is split into its own schema (`/schemas/creative/validate-input-result.json`) rather than inlined here because the same per-target shape is intended for reuse by adjacent async-validation surfaces (planned: per-batch result envelopes on `build_creative` async paths, and asynchronous canonical-against-product validation in `sync_creatives`). Producers that only need the synchronous batch shape today MAY treat the split as YAGNI, but the schema reuse anchors the violation/retry shape so downstream surfaces don't drift.", "type": "object", "required": [ "results" @@ -11,7 +11,7 @@ "type": "array", "items": { "title": "Validate Input Result", - "description": "Per-target result of a validate_input call. The `result_kind` discriminator (replacing the earlier boolean `ok`) lets buyers distinguish three meaningfully different outcomes:\n\n- `validated_pass` \u2014 manifest validates cleanly against the target. Buyers can submit with confidence.\n- `validated_fail` \u2014 manifest is structurally evaluable AND fails specific constraints. `violations[]` enumerates which. Buyers fix and retry.\n- `unvalidatable_nondeterministic` \u2014 predictive validation is impossible because the target's production pipeline is genuinely nondeterministic (Veo / Sora / Runway-class formats with `synthesis_nondeterministic: true`). The platform's own post-synthesis QA loop applies; outcome is unknowable until `build_creative` runs. Buyers MUST plan for the QA-loop semantics: submission may return `task_failed` with a `synthesis_failed` reason if the QA loop exhausts. There is no protocol state for orphaned out-of-spec artifacts.\n\nThe boolean `ok` field carried in earlier drafts is removed \u2014 it conflated `validated_fail` (a real validation result the buyer can act on) with `unvalidatable_nondeterministic` (a structural property of the target the buyer needs to handle differently). `validated_fail` returns `violations[]`; `unvalidatable_nondeterministic` does not (there's nothing to enumerate).\n\n**Scope of validation (normative).** `validate_input` validates **manifest structure** against the canonical / product / third-party format target \u2014 slot counts, asset types, parameter ranges, format-shape constraints. It does NOT predict the surface's per-impression rendering choice. This distinction matters for `composition_model: algorithmic` formats (`responsive_creative`, `agent_placement`) where the surface picks combinations or phrasing at delivery time: the buyer's asset pool can still be structurally validated (does it meet the count/size/length requirements?) \u2192 `validated_pass` or `validated_fail` with violations. What's unpredictable is the rendered output composition, and that's NOT what `validate_input` claims to predict. `unvalidatable_nondeterministic` is reserved for `synthesis_nondeterministic: true` cases where the production pipeline itself can't be evaluated upfront \u2014 distinct from algorithmic composition where the inputs ARE evaluable but the output rendering isn't. Buyers calling `validate_input` on an algorithmic-composition target SHOULD expect a structural verdict (pass/fail), not a rendering preview.", + "description": "Per-target result of a validate_input call. The `result_kind` discriminator (replacing the earlier boolean `ok`) lets buyers distinguish three meaningfully different outcomes:\n\n- `validated_pass` \u2014 manifest is structurally valid against the target. This is not seller trafficking acceptance; use `sync_creatives` or `sync_creatives` with `dry_run: true` for upload/update, account, assignment, policy, and lifecycle gates.\n- `validated_fail` \u2014 manifest is structurally evaluable AND fails specific constraints. `violations[]` enumerates which. Buyers fix and retry.\n- `unvalidatable_nondeterministic` \u2014 predictive validation is impossible because the target's production pipeline is genuinely nondeterministic (Veo / Sora / Runway-class formats with `synthesis_nondeterministic: true`). The platform's own post-synthesis QA loop applies; outcome is unknowable until `build_creative` runs. Buyers MUST plan for the QA-loop semantics: submission may return `task_failed` with a `synthesis_failed` reason if the QA loop exhausts. There is no protocol state for orphaned out-of-spec artifacts.\n\nThe boolean `ok` field carried in earlier drafts is removed \u2014 it conflated `validated_fail` (a real validation result the buyer can act on) with `unvalidatable_nondeterministic` (a structural property of the target the buyer needs to handle differently). `validated_fail` returns `violations[]`; `unvalidatable_nondeterministic` does not (there's nothing to enumerate).\n\n**Scope of validation (normative).** `validate_input` validates **manifest structure** against the canonical / product / third-party format target \u2014 slot counts, asset types, parameter ranges, format-shape constraints. It does NOT predict the surface's per-impression rendering choice. This distinction matters for `composition_model: algorithmic` formats (`responsive_creative`, `agent_placement`) where the surface picks combinations or phrasing at delivery time: the buyer's asset pool can still be structurally validated (does it meet the count/size/length requirements?) \u2192 `validated_pass` or `validated_fail` with violations. What's unpredictable is the rendered output composition, and that's NOT what `validate_input` claims to predict. `unvalidatable_nondeterministic` is reserved for `synthesis_nondeterministic: true` cases where the production pipeline itself can't be evaluated upfront \u2014 distinct from algorithmic composition where the inputs ARE evaluable but the output rendering isn't. Buyers calling `validate_input` on an algorithmic-composition target SHOULD expect a structural verdict (pass/fail), not a rendering preview.", "type": "object", "required": [ "target", @@ -592,7 +592,7 @@ } ], "_bundled": { - "generatedAt": "2026-06-30T19:14:39.972Z", + "generatedAt": "2026-07-28T13:04:06.927Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/build-creative-request.json b/schemas/cache/3.1/bundled/media-buy/build-creative-request.json index 5f2576ff..07623a82 100644 --- a/schemas/cache/3.1/bundled/media-buy/build-creative-request.json +++ b/schemas/cache/3.1/bundled/media-buy/build-creative-request.json @@ -26224,7 +26224,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.022Z", + "generatedAt": "2026-07-28T13:04:06.955Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/build-creative-response.json b/schemas/cache/3.1/bundled/media-buy/build-creative-response.json index 54d33f2f..54ed0bda 100644 --- a/schemas/cache/3.1/bundled/media-buy/build-creative-response.json +++ b/schemas/cache/3.1/bundled/media-buy/build-creative-response.json @@ -60679,7 +60679,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.120Z", + "generatedAt": "2026-07-28T13:04:07.058Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/create-media-buy-request.json b/schemas/cache/3.1/bundled/media-buy/create-media-buy-request.json index 6810271e..66b540ae 100644 --- a/schemas/cache/3.1/bundled/media-buy/create-media-buy-request.json +++ b/schemas/cache/3.1/bundled/media-buy/create-media-buy-request.json @@ -27058,7 +27058,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.160Z", + "generatedAt": "2026-07-28T13:04:07.108Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/create-media-buy-response.json b/schemas/cache/3.1/bundled/media-buy/create-media-buy-response.json index cb9652a2..54fd6135 100644 --- a/schemas/cache/3.1/bundled/media-buy/create-media-buy-response.json +++ b/schemas/cache/3.1/bundled/media-buy/create-media-buy-response.json @@ -10489,7 +10489,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.179Z", + "generatedAt": "2026-07-28T13:04:07.128Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/get-media-buy-delivery-request.json b/schemas/cache/3.1/bundled/media-buy/get-media-buy-delivery-request.json index 7a5c234e..37a58f6d 100644 --- a/schemas/cache/3.1/bundled/media-buy/get-media-buy-delivery-request.json +++ b/schemas/cache/3.1/bundled/media-buy/get-media-buy-delivery-request.json @@ -1332,7 +1332,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.182Z", + "generatedAt": "2026-07-28T13:04:07.133Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/get-media-buy-delivery-response.json b/schemas/cache/3.1/bundled/media-buy/get-media-buy-delivery-response.json index eb53bddf..60b92931 100644 --- a/schemas/cache/3.1/bundled/media-buy/get-media-buy-delivery-response.json +++ b/schemas/cache/3.1/bundled/media-buy/get-media-buy-delivery-response.json @@ -621,8 +621,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Aggregate completion rate across all media buys (weighted by impressions, not a simple average of per-buy rates)", + "type": [ + "number", + "null" + ], + "description": "Aggregate completion rate across all media buys (weighted by impressions, not a simple average of per-buy rates). Null indicates the metric is not applicable to the aggregated buys (e.g. all non-video inventory).", "minimum": 0, "maximum": 1 }, @@ -1507,8 +1510,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -1702,8 +1708,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -3075,8 +3084,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -3270,8 +3282,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -5325,8 +5340,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -5520,8 +5538,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -6915,8 +6936,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -7110,8 +7134,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -8493,8 +8520,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -8688,8 +8718,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -10080,8 +10113,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -10275,8 +10311,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -11997,8 +12036,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -12192,8 +12234,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -13579,8 +13624,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -13774,8 +13822,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -15167,8 +15218,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -15362,8 +15416,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -16759,8 +16816,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -16954,8 +17014,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -18418,8 +18481,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -18613,8 +18679,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -19973,8 +20042,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -20168,8 +20240,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -22082,7 +22157,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.200Z", + "generatedAt": "2026-07-28T13:04:07.154Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/get-media-buys-request.json b/schemas/cache/3.1/bundled/media-buy/get-media-buys-request.json index 645bb307..1b99b12f 100644 --- a/schemas/cache/3.1/bundled/media-buy/get-media-buys-request.json +++ b/schemas/cache/3.1/bundled/media-buy/get-media-buys-request.json @@ -782,7 +782,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.205Z", + "generatedAt": "2026-07-28T13:04:07.164Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/get-media-buys-response.json b/schemas/cache/3.1/bundled/media-buy/get-media-buys-response.json index 191613f2..274efd60 100644 --- a/schemas/cache/3.1/bundled/media-buy/get-media-buys-response.json +++ b/schemas/cache/3.1/bundled/media-buy/get-media-buys-response.json @@ -6142,7 +6142,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.210Z", + "generatedAt": "2026-07-28T13:04:07.170Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/get-products-request.json b/schemas/cache/3.1/bundled/media-buy/get-products-request.json index 31f56a44..dc3038e2 100644 --- a/schemas/cache/3.1/bundled/media-buy/get-products-request.json +++ b/schemas/cache/3.1/bundled/media-buy/get-products-request.json @@ -5317,7 +5317,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.217Z", + "generatedAt": "2026-07-28T13:04:07.177Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/get-products-response.json b/schemas/cache/3.1/bundled/media-buy/get-products-response.json index e42892a6..d3ce4939 100644 --- a/schemas/cache/3.1/bundled/media-buy/get-products-response.json +++ b/schemas/cache/3.1/bundled/media-buy/get-products-response.json @@ -24607,6 +24607,44 @@ "required": [ "type" ], + "allOf": [ + { + "$comment": "rate quantifies a percent_remaining fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "percent_remaining" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "rate" + ] + } + }, + { + "$comment": "amount quantifies a fixed_fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "fixed_fee" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "amount" + ] + } + } + ], "additionalProperties": true } }, @@ -36964,7 +37002,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.246Z", + "generatedAt": "2026-07-28T13:04:07.218Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/list-creative-formats-request.json b/schemas/cache/3.1/bundled/media-buy/list-creative-formats-request.json index 9577916b..0f07cc48 100644 --- a/schemas/cache/3.1/bundled/media-buy/list-creative-formats-request.json +++ b/schemas/cache/3.1/bundled/media-buy/list-creative-formats-request.json @@ -343,7 +343,7 @@ }, "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.253Z", + "generatedAt": "2026-07-28T13:04:07.229Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/list-creative-formats-response.json b/schemas/cache/3.1/bundled/media-buy/list-creative-formats-response.json index a695604c..8b139150 100644 --- a/schemas/cache/3.1/bundled/media-buy/list-creative-formats-response.json +++ b/schemas/cache/3.1/bundled/media-buy/list-creative-formats-response.json @@ -13658,7 +13658,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.262Z", + "generatedAt": "2026-07-28T13:04:07.244Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/log-event-request.json b/schemas/cache/3.1/bundled/media-buy/log-event-request.json index 678ede41..abd9be2b 100644 --- a/schemas/cache/3.1/bundled/media-buy/log-event-request.json +++ b/schemas/cache/3.1/bundled/media-buy/log-event-request.json @@ -627,7 +627,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.265Z", + "generatedAt": "2026-07-28T13:04:07.252Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/log-event-response.json b/schemas/cache/3.1/bundled/media-buy/log-event-response.json index 0d1582af..b5435f62 100644 --- a/schemas/cache/3.1/bundled/media-buy/log-event-response.json +++ b/schemas/cache/3.1/bundled/media-buy/log-event-response.json @@ -680,7 +680,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.266Z", + "generatedAt": "2026-07-28T13:04:07.253Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/package-request.json b/schemas/cache/3.1/bundled/media-buy/package-request.json index 07ada014..2f046e76 100644 --- a/schemas/cache/3.1/bundled/media-buy/package-request.json +++ b/schemas/cache/3.1/bundled/media-buy/package-request.json @@ -25250,7 +25250,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.292Z", + "generatedAt": "2026-07-28T13:04:07.277Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/provide-performance-feedback-request.json b/schemas/cache/3.1/bundled/media-buy/provide-performance-feedback-request.json index 3bb82890..071ee6ba 100644 --- a/schemas/cache/3.1/bundled/media-buy/provide-performance-feedback-request.json +++ b/schemas/cache/3.1/bundled/media-buy/provide-performance-feedback-request.json @@ -132,7 +132,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.297Z", + "generatedAt": "2026-07-28T13:04:07.285Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/provide-performance-feedback-response.json b/schemas/cache/3.1/bundled/media-buy/provide-performance-feedback-response.json index a41444cf..90d1f5d7 100644 --- a/schemas/cache/3.1/bundled/media-buy/provide-performance-feedback-response.json +++ b/schemas/cache/3.1/bundled/media-buy/provide-performance-feedback-response.json @@ -629,7 +629,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.298Z", + "generatedAt": "2026-07-28T13:04:07.286Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/sync-audiences-request.json b/schemas/cache/3.1/bundled/media-buy/sync-audiences-request.json index 5c9811ea..704702db 100644 --- a/schemas/cache/3.1/bundled/media-buy/sync-audiences-request.json +++ b/schemas/cache/3.1/bundled/media-buy/sync-audiences-request.json @@ -965,7 +965,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.299Z", + "generatedAt": "2026-07-28T13:04:07.287Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/sync-audiences-response.json b/schemas/cache/3.1/bundled/media-buy/sync-audiences-response.json index 4bd2e84c..2448a4de 100644 --- a/schemas/cache/3.1/bundled/media-buy/sync-audiences-response.json +++ b/schemas/cache/3.1/bundled/media-buy/sync-audiences-response.json @@ -1104,7 +1104,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.300Z", + "generatedAt": "2026-07-28T13:04:07.289Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/sync-catalogs-request.json b/schemas/cache/3.1/bundled/media-buy/sync-catalogs-request.json index 479bc4bd..d0b5f566 100644 --- a/schemas/cache/3.1/bundled/media-buy/sync-catalogs-request.json +++ b/schemas/cache/3.1/bundled/media-buy/sync-catalogs-request.json @@ -1372,7 +1372,7 @@ } ], "_bundled": { - "generatedAt": "2026-06-30T19:14:40.302Z", + "generatedAt": "2026-07-28T13:04:07.291Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/sync-catalogs-response.json b/schemas/cache/3.1/bundled/media-buy/sync-catalogs-response.json index 6eb9182c..85d0f84a 100644 --- a/schemas/cache/3.1/bundled/media-buy/sync-catalogs-response.json +++ b/schemas/cache/3.1/bundled/media-buy/sync-catalogs-response.json @@ -1089,7 +1089,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.303Z", + "generatedAt": "2026-07-28T13:04:07.292Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/sync-event-sources-request.json b/schemas/cache/3.1/bundled/media-buy/sync-event-sources-request.json index 22e31f4f..8c5bd678 100644 --- a/schemas/cache/3.1/bundled/media-buy/sync-event-sources-request.json +++ b/schemas/cache/3.1/bundled/media-buy/sync-event-sources-request.json @@ -905,7 +905,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.304Z", + "generatedAt": "2026-07-28T13:04:07.294Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/sync-event-sources-response.json b/schemas/cache/3.1/bundled/media-buy/sync-event-sources-response.json index e0e303a6..800c0f15 100644 --- a/schemas/cache/3.1/bundled/media-buy/sync-event-sources-response.json +++ b/schemas/cache/3.1/bundled/media-buy/sync-event-sources-response.json @@ -1098,7 +1098,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.306Z", + "generatedAt": "2026-07-28T13:04:07.295Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/update-media-buy-request.json b/schemas/cache/3.1/bundled/media-buy/update-media-buy-request.json index 704838f6..cb05b394 100644 --- a/schemas/cache/3.1/bundled/media-buy/update-media-buy-request.json +++ b/schemas/cache/3.1/bundled/media-buy/update-media-buy-request.json @@ -48818,7 +48818,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.348Z", + "generatedAt": "2026-07-28T13:04:07.343Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/media-buy/update-media-buy-response.json b/schemas/cache/3.1/bundled/media-buy/update-media-buy-response.json index 2f10abc9..6253f683 100644 --- a/schemas/cache/3.1/bundled/media-buy/update-media-buy-response.json +++ b/schemas/cache/3.1/bundled/media-buy/update-media-buy-response.json @@ -8068,7 +8068,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.363Z", + "generatedAt": "2026-07-28T13:04:07.365Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/create-property-list-request.json b/schemas/cache/3.1/bundled/property/create-property-list-request.json index 6732e98a..e9224b99 100644 --- a/schemas/cache/3.1/bundled/property/create-property-list-request.json +++ b/schemas/cache/3.1/bundled/property/create-property-list-request.json @@ -1602,7 +1602,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.366Z", + "generatedAt": "2026-07-28T13:04:07.370Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/create-property-list-response.json b/schemas/cache/3.1/bundled/property/create-property-list-response.json index 61abf909..80d1e543 100644 --- a/schemas/cache/3.1/bundled/property/create-property-list-response.json +++ b/schemas/cache/3.1/bundled/property/create-property-list-response.json @@ -2322,7 +2322,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.368Z", + "generatedAt": "2026-07-28T13:04:07.373Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/delete-property-list-request.json b/schemas/cache/3.1/bundled/property/delete-property-list-request.json index 3e81e81d..c592b11c 100644 --- a/schemas/cache/3.1/bundled/property/delete-property-list-request.json +++ b/schemas/cache/3.1/bundled/property/delete-property-list-request.json @@ -707,7 +707,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.369Z", + "generatedAt": "2026-07-28T13:04:07.374Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/delete-property-list-response.json b/schemas/cache/3.1/bundled/property/delete-property-list-response.json index 6a4c15c3..3a3c7c53 100644 --- a/schemas/cache/3.1/bundled/property/delete-property-list-response.json +++ b/schemas/cache/3.1/bundled/property/delete-property-list-response.json @@ -456,7 +456,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.370Z", + "generatedAt": "2026-07-28T13:04:07.375Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/get-property-list-request.json b/schemas/cache/3.1/bundled/property/get-property-list-request.json index eeedc66f..94ac79af 100644 --- a/schemas/cache/3.1/bundled/property/get-property-list-request.json +++ b/schemas/cache/3.1/bundled/property/get-property-list-request.json @@ -721,7 +721,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.371Z", + "generatedAt": "2026-07-28T13:04:07.376Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/get-property-list-response.json b/schemas/cache/3.1/bundled/property/get-property-list-response.json index ce53e271..20df32b0 100644 --- a/schemas/cache/3.1/bundled/property/get-property-list-response.json +++ b/schemas/cache/3.1/bundled/property/get-property-list-response.json @@ -2395,7 +2395,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.373Z", + "generatedAt": "2026-07-28T13:04:07.379Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/list-property-lists-request.json b/schemas/cache/3.1/bundled/property/list-property-lists-request.json index 66d221df..12956392 100644 --- a/schemas/cache/3.1/bundled/property/list-property-lists-request.json +++ b/schemas/cache/3.1/bundled/property/list-property-lists-request.json @@ -713,7 +713,7 @@ }, "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.375Z", + "generatedAt": "2026-07-28T13:04:07.380Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/list-property-lists-response.json b/schemas/cache/3.1/bundled/property/list-property-lists-response.json index 85be2009..a8595d45 100644 --- a/schemas/cache/3.1/bundled/property/list-property-lists-response.json +++ b/schemas/cache/3.1/bundled/property/list-property-lists-response.json @@ -2340,7 +2340,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.377Z", + "generatedAt": "2026-07-28T13:04:07.382Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/update-property-list-request.json b/schemas/cache/3.1/bundled/property/update-property-list-request.json index 6ab91de0..8e9e3c06 100644 --- a/schemas/cache/3.1/bundled/property/update-property-list-request.json +++ b/schemas/cache/3.1/bundled/property/update-property-list-request.json @@ -1611,7 +1611,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.379Z", + "generatedAt": "2026-07-28T13:04:07.385Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/update-property-list-response.json b/schemas/cache/3.1/bundled/property/update-property-list-response.json index 91c33200..cb5d9aa3 100644 --- a/schemas/cache/3.1/bundled/property/update-property-list-response.json +++ b/schemas/cache/3.1/bundled/property/update-property-list-response.json @@ -2317,7 +2317,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.381Z", + "generatedAt": "2026-07-28T13:04:07.388Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/validate-property-delivery-request.json b/schemas/cache/3.1/bundled/property/validate-property-delivery-request.json index c9b0b6db..5475326b 100644 --- a/schemas/cache/3.1/bundled/property/validate-property-delivery-request.json +++ b/schemas/cache/3.1/bundled/property/validate-property-delivery-request.json @@ -817,7 +817,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.382Z", + "generatedAt": "2026-07-28T13:04:07.389Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/property/validate-property-delivery-response.json b/schemas/cache/3.1/bundled/property/validate-property-delivery-response.json index 18cd71fd..0f5ee61a 100644 --- a/schemas/cache/3.1/bundled/property/validate-property-delivery-response.json +++ b/schemas/cache/3.1/bundled/property/validate-property-delivery-response.json @@ -830,7 +830,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.384Z", + "generatedAt": "2026-07-28T13:04:07.390Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/protocol/get-adcp-capabilities-request.json b/schemas/cache/3.1/bundled/protocol/get-adcp-capabilities-request.json index 2705f8d2..b6dce1f9 100644 --- a/schemas/cache/3.1/bundled/protocol/get-adcp-capabilities-request.json +++ b/schemas/cache/3.1/bundled/protocol/get-adcp-capabilities-request.json @@ -60,7 +60,7 @@ }, "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.384Z", + "generatedAt": "2026-07-28T13:04:07.391Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/protocol/get-adcp-capabilities-response.json b/schemas/cache/3.1/bundled/protocol/get-adcp-capabilities-response.json index ce604d8f..9c156933 100644 --- a/schemas/cache/3.1/bundled/protocol/get-adcp-capabilities-response.json +++ b/schemas/cache/3.1/bundled/protocol/get-adcp-capabilities-response.json @@ -490,7 +490,7 @@ }, "in_flight_max_seconds": { "type": "integer", - "description": "Maximum lifetime in seconds of an in-flight idempotency row before the seller releases it per L1/security.mdx rule 9 (treat the in-flight attempt as failed if the handler does not complete within this bound). Buyer SDKs use this value to compute a retry budget when they see `IDEMPOTENCY_IN_FLIGHT` \u2014 cap individual retry waits at this value rather than the much-wider `replay_ttl_seconds` ceiling. Optional in 3.1 (additive declaration); SDKs that don't see the field fall back to rule 9's order-of-magnitude SHOULD heuristic. Required when `supported: true` in 4.0. MUST be no greater than `replay_ttl_seconds` (a bound larger than the replay window is vacuous \u2014 any retry past the TTL hits IDEMPOTENCY_EXPIRED regardless of in-flight state); validators MUST enforce this cross-field constraint at the test layer since JSON Schema cannot express field-relative bounds. A buyer that observes `error.details.retry_after` exceeding this value MAY treat that as a seller bug \u2014 the in-flight row cannot legitimately outlive the bound the seller declared.", + "description": "Maximum lifetime in seconds of an in-flight idempotency row before the seller releases it per L1/security.mdx rule 9 (treat the in-flight attempt as failed if the handler does not complete within this bound). Buyer SDKs use this value to compute a retry budget when they see `IDEMPOTENCY_IN_FLIGHT` \u2014 cap individual retry waits at this value rather than the much-wider `replay_ttl_seconds` ceiling. Optional in 3.1 (additive declaration); SDKs that don't see the field fall back to rule 9's order-of-magnitude SHOULD heuristic. Required when `supported: true` in 4.0. MUST be no greater than `replay_ttl_seconds` (a bound larger than the replay window is vacuous \u2014 any retry past the TTL hits IDEMPOTENCY_EXPIRED regardless of in-flight state); validators MUST enforce this cross-field constraint at the test layer since JSON Schema cannot express field-relative bounds. A buyer that observes top-level `error.retry_after` exceeding this value MAY treat that as a seller bug \u2014 the in-flight row cannot legitimately outlive the bound the seller declared.", "minimum": 1, "maximum": 604800 }, @@ -11750,7 +11750,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.392Z", + "generatedAt": "2026-07-28T13:04:07.401Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/protocol/get-task-status-request.json b/schemas/cache/3.1/bundled/protocol/get-task-status-request.json index 68e789f2..54f643df 100644 --- a/schemas/cache/3.1/bundled/protocol/get-task-status-request.json +++ b/schemas/cache/3.1/bundled/protocol/get-task-status-request.json @@ -737,7 +737,7 @@ } ], "_bundled": { - "generatedAt": "2026-06-30T19:14:40.395Z", + "generatedAt": "2026-07-28T13:04:07.406Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/protocol/get-task-status-response.json b/schemas/cache/3.1/bundled/protocol/get-task-status-response.json index 1dcc592c..1705ec67 100644 --- a/schemas/cache/3.1/bundled/protocol/get-task-status-response.json +++ b/schemas/cache/3.1/bundled/protocol/get-task-status-response.json @@ -25116,6 +25116,44 @@ "required": [ "type" ], + "allOf": [ + { + "$comment": "rate quantifies a percent_remaining fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "percent_remaining" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "rate" + ] + } + }, + { + "$comment": "amount quantifies a fixed_fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "fixed_fee" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "amount" + ] + } + } + ], "additionalProperties": true } }, @@ -60468,6 +60506,44 @@ "required": [ "type" ], + "allOf": [ + { + "$comment": "rate quantifies a percent_remaining fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "percent_remaining" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "rate" + ] + } + }, + { + "$comment": "amount quantifies a fixed_fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "fixed_fee" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "amount" + ] + } + } + ], "additionalProperties": true } }, @@ -88399,8 +88475,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -88594,8 +88673,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -89967,8 +90049,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -90162,8 +90247,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", @@ -158007,7 +158095,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.809Z", + "generatedAt": "2026-07-28T13:04:07.565Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/protocol/list-tasks-request.json b/schemas/cache/3.1/bundled/protocol/list-tasks-request.json index bfc976f0..01acc435 100644 --- a/schemas/cache/3.1/bundled/protocol/list-tasks-request.json +++ b/schemas/cache/3.1/bundled/protocol/list-tasks-request.json @@ -993,7 +993,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.842Z", + "generatedAt": "2026-07-28T13:04:07.632Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/protocol/list-tasks-response.json b/schemas/cache/3.1/bundled/protocol/list-tasks-response.json index 4971339b..772ce048 100644 --- a/schemas/cache/3.1/bundled/protocol/list-tasks-response.json +++ b/schemas/cache/3.1/bundled/protocol/list-tasks-response.json @@ -665,7 +665,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.843Z", + "generatedAt": "2026-07-28T13:04:07.633Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/signals/activate-signal-request.json b/schemas/cache/3.1/bundled/signals/activate-signal-request.json index e7ffc67b..9d8eec78 100644 --- a/schemas/cache/3.1/bundled/signals/activate-signal-request.json +++ b/schemas/cache/3.1/bundled/signals/activate-signal-request.json @@ -109,6 +109,13 @@ "description": "The pricing option selected from the signal's pricing_options in the get_signals response. Required when the signal has pricing options. Records the buyer's pricing commitment at activation time; pass this same value in report_usage for billing verification.", "x-entity": "vendor_pricing_option" }, + "governance_context": { + "type": "string", + "description": "Opaque governance context returned by check_governance for this signal activation. Required when the account has a registered governance agent; signal agents MUST reject governed activations that omit a valid context.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, "account": { "title": "Account Reference", "description": "Account for this activation. Associates with a commercial relationship established via sync_accounts.", @@ -781,7 +788,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.844Z", + "generatedAt": "2026-07-28T13:04:07.634Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/signals/activate-signal-response.json b/schemas/cache/3.1/bundled/signals/activate-signal-response.json index d3d60fa3..7842b5fc 100644 --- a/schemas/cache/3.1/bundled/signals/activate-signal-response.json +++ b/schemas/cache/3.1/bundled/signals/activate-signal-response.json @@ -816,7 +816,7 @@ ], "properties": {}, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.845Z", + "generatedAt": "2026-07-28T13:04:07.636Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/signals/get-signals-request.json b/schemas/cache/3.1/bundled/signals/get-signals-request.json index f44cff5b..354e2ef3 100644 --- a/schemas/cache/3.1/bundled/signals/get-signals-request.json +++ b/schemas/cache/3.1/bundled/signals/get-signals-request.json @@ -1286,7 +1286,7 @@ }, "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.846Z", + "generatedAt": "2026-07-28T13:04:07.637Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/signals/get-signals-response.json b/schemas/cache/3.1/bundled/signals/get-signals-response.json index d98224a5..5ae76f07 100644 --- a/schemas/cache/3.1/bundled/signals/get-signals-response.json +++ b/schemas/cache/3.1/bundled/signals/get-signals-response.json @@ -5608,7 +5608,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.851Z", + "generatedAt": "2026-07-28T13:04:07.643Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/sponsored-intelligence/si-get-offering-request.json b/schemas/cache/3.1/bundled/sponsored-intelligence/si-get-offering-request.json index 1aa19e1c..051e20ce 100644 --- a/schemas/cache/3.1/bundled/sponsored-intelligence/si-get-offering-request.json +++ b/schemas/cache/3.1/bundled/sponsored-intelligence/si-get-offering-request.json @@ -70,7 +70,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.852Z", + "generatedAt": "2026-07-28T13:04:07.645Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/sponsored-intelligence/si-get-offering-response.json b/schemas/cache/3.1/bundled/sponsored-intelligence/si-get-offering-response.json index 3f0b7fb8..97bed886 100644 --- a/schemas/cache/3.1/bundled/sponsored-intelligence/si-get-offering-response.json +++ b/schemas/cache/3.1/bundled/sponsored-intelligence/si-get-offering-response.json @@ -1477,7 +1477,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.853Z", + "generatedAt": "2026-07-28T13:04:07.647Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/sponsored-intelligence/si-initiate-session-request.json b/schemas/cache/3.1/bundled/sponsored-intelligence/si-initiate-session-request.json index d8e91629..79862987 100644 --- a/schemas/cache/3.1/bundled/sponsored-intelligence/si-initiate-session-request.json +++ b/schemas/cache/3.1/bundled/sponsored-intelligence/si-initiate-session-request.json @@ -1400,7 +1400,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.855Z", + "generatedAt": "2026-07-28T13:04:07.649Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/sponsored-intelligence/si-initiate-session-response.json b/schemas/cache/3.1/bundled/sponsored-intelligence/si-initiate-session-response.json index a5595580..1729ff96 100644 --- a/schemas/cache/3.1/bundled/sponsored-intelligence/si-initiate-session-response.json +++ b/schemas/cache/3.1/bundled/sponsored-intelligence/si-initiate-session-response.json @@ -1859,7 +1859,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.857Z", + "generatedAt": "2026-07-28T13:04:07.651Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/sponsored-intelligence/si-send-message-request.json b/schemas/cache/3.1/bundled/sponsored-intelligence/si-send-message-request.json index 13f3e930..06a5d17f 100644 --- a/schemas/cache/3.1/bundled/sponsored-intelligence/si-send-message-request.json +++ b/schemas/cache/3.1/bundled/sponsored-intelligence/si-send-message-request.json @@ -1159,7 +1159,7 @@ } }, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.858Z", + "generatedAt": "2026-07-28T13:04:07.653Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/sponsored-intelligence/si-send-message-response.json b/schemas/cache/3.1/bundled/sponsored-intelligence/si-send-message-response.json index 67ac14c8..7923c6a2 100644 --- a/schemas/cache/3.1/bundled/sponsored-intelligence/si-send-message-response.json +++ b/schemas/cache/3.1/bundled/sponsored-intelligence/si-send-message-response.json @@ -1830,7 +1830,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.859Z", + "generatedAt": "2026-07-28T13:04:07.655Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/sponsored-intelligence/si-terminate-session-request.json b/schemas/cache/3.1/bundled/sponsored-intelligence/si-terminate-session-request.json index 2e2163f1..33fedd0d 100644 --- a/schemas/cache/3.1/bundled/sponsored-intelligence/si-terminate-session-request.json +++ b/schemas/cache/3.1/bundled/sponsored-intelligence/si-terminate-session-request.json @@ -100,7 +100,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.860Z", + "generatedAt": "2026-07-28T13:04:07.656Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/bundled/sponsored-intelligence/si-terminate-session-response.json b/schemas/cache/3.1/bundled/sponsored-intelligence/si-terminate-session-response.json index c41a5dda..31a77b23 100644 --- a/schemas/cache/3.1/bundled/sponsored-intelligence/si-terminate-session-response.json +++ b/schemas/cache/3.1/bundled/sponsored-intelligence/si-terminate-session-response.json @@ -642,7 +642,7 @@ ], "additionalProperties": true, "_bundled": { - "generatedAt": "2026-06-30T19:14:40.860Z", + "generatedAt": "2026-07-28T13:04:07.656Z", "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." } } \ No newline at end of file diff --git a/schemas/cache/3.1/core/cancellation-policy.json b/schemas/cache/3.1/core/cancellation-policy.json index a2177370..0340921d 100644 --- a/schemas/cache/3.1/core/cancellation-policy.json +++ b/schemas/cache/3.1/core/cancellation-policy.json @@ -37,6 +37,44 @@ "required": [ "type" ], + "allOf": [ + { + "$comment": "rate quantifies a percent_remaining fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "percent_remaining" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "rate" + ] + } + }, + { + "$comment": "amount quantifies a fixed_fee; without it the cancellation cost is undefined.", + "if": { + "properties": { + "type": { + "const": "fixed_fee" + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "amount" + ] + } + } + ], "additionalProperties": true } }, diff --git a/schemas/cache/3.1/core/delivery-metrics.json b/schemas/cache/3.1/core/delivery-metrics.json index 98ab5c68..8b5e6682 100644 --- a/schemas/cache/3.1/core/delivery-metrics.json +++ b/schemas/cache/3.1/core/delivery-metrics.json @@ -36,8 +36,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Completion rate (completed_views/impressions)", + "type": [ + "number", + "null" + ], + "description": "Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).", "minimum": 0, "maximum": 1 }, @@ -207,8 +210,11 @@ "minimum": 0 }, "quartile_data": { - "type": "object", - "description": "Audio/video quartile completion data", + "type": [ + "object", + "null" + ], + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", "properties": { "q1_views": { "type": "number", diff --git a/schemas/cache/3.1/creative/asset-types/index.json b/schemas/cache/3.1/creative/asset-types/index.json index 46d5e9b2..cea47ac8 100644 --- a/schemas/cache/3.1/creative/asset-types/index.json +++ b/schemas/cache/3.1/creative/asset-types/index.json @@ -3,7 +3,7 @@ "title": "AdCP Asset Type Registry", "description": "Registry of asset types used in AdCP creative manifests. Each asset type defines the structure of actual content payloads (what you send), not requirements or constraints (which belong in format specifications).", "version": "1.0.0", - "lastUpdated": "2026-06-30", + "lastUpdated": "2026-07-28", "asset_types": { "image": { "description": "Static image asset (JPG, PNG, GIF, WebP, SVG)", @@ -139,9 +139,9 @@ "creative_manifests": "Creative manifests provide actual asset content, keyed by asset_id from the format. Each asset value carries an `asset_type` discriminator (one of the registry keys) so validators can select the matching asset schema and report errors against only that branch. The format specification also defines what asset_type each asset_id should have \u2014 payload and format should agree.", "example_flow": "Format says 'hero_image' must be type 'image' with width 1200, height 627. Manifest provides hero_image: {url: '...', width: 1200, height: 627}. The format spec tells us it's an image type." }, - "published_version": "3.1.1", - "adcp_version": "3.1.1", - "baseUrl": "/schemas/3.1.1", + "published_version": "3.1.8", + "adcp_version": "3.1.8", + "baseUrl": "/schemas/3.1.8", "protocol_layers": [ { "id": "negotiation", @@ -170,6 +170,6 @@ "prerelease": false, "deprecated": false, "versioning": { - "note": "AdCP uses build-time versioning. This directory contains schemas for AdCP 3.1.1. Full semantic versions are available at /schemas/{version}/ (e.g., /schemas/2.5.0/). Major version aliases point to the latest stable release in that major line; use /schemas/index.json or /schemas/latest.json for the canonical file-based pointer." + "note": "AdCP uses build-time versioning. This directory contains schemas for AdCP 3.1.8. Full semantic versions are available at /schemas/{version}/ (e.g., /schemas/2.5.0/). Major version aliases point to the latest stable release in that major line; use /schemas/index.json or /schemas/latest.json for the canonical file-based pointer." } } \ No newline at end of file diff --git a/schemas/cache/3.1/creative/list-creatives-response.json b/schemas/cache/3.1/creative/list-creatives-response.json index 2487eaa3..15662daa 100644 --- a/schemas/cache/3.1/creative/list-creatives-response.json +++ b/schemas/cache/3.1/creative/list-creatives-response.json @@ -76,7 +76,15 @@ }, "format_id": { "$ref": "../core/format-id.json", - "description": "Format identifier specifying which format this creative conforms to" + "description": "Legacy named-format path. Structured format identifier specifying which legacy format this creative conforms to. Mutually exclusive with `format_kind`." + }, + "format_kind": { + "$ref": "../core/canonical-format-kind.json", + "description": "3.1+ canonical-format path. The canonical format kind this creative targets. Mutually exclusive with `format_id`." + }, + "format_option_ref": { + "$ref": "../core/format-option-ref.json", + "description": "Optional 3.1+ reference to the concrete canonical format option this creative targets. Required when `format_kind` alone is ambiguous in the enclosing product context." }, "status": { "$ref": "../enums/creative-status.json", @@ -265,11 +273,34 @@ "required": [ "creative_id", "name", - "format_id", "status", "created_date", "updated_date" ], + "oneOf": [ + { + "title": "Legacy listed creative (named-format reference)", + "required": [ + "format_id" + ], + "not": { + "required": [ + "format_kind" + ] + } + }, + { + "title": "3.1+ listed creative (canonical format kind)", + "required": [ + "format_kind" + ], + "not": { + "required": [ + "format_id" + ] + } + } + ], "additionalProperties": true } }, diff --git a/schemas/cache/3.1/creative/sync-creatives-request.json b/schemas/cache/3.1/creative/sync-creatives-request.json index 979eac12..7c4d22bd 100644 --- a/schemas/cache/3.1/creative/sync-creatives-request.json +++ b/schemas/cache/3.1/creative/sync-creatives-request.json @@ -87,7 +87,7 @@ "dry_run": { "type": "boolean", "default": false, - "description": "When true, preview changes without applying them. Returns what would be created/updated/deleted." + "description": "When true, rehearse this sync_creatives operation without applying it. Validates the actual trafficking request in the seller's current context, including library upsert semantics, creative IDs, assignments, account-scoped gates, and seller policies, then returns what would be created/updated/deleted. This is distinct from validate_input, which only validates manifest structure against canonical/product format targets." }, "validation_mode": { "$ref": "../enums/validation-mode.json", diff --git a/schemas/cache/3.1/creative/validate-input-request.json b/schemas/cache/3.1/creative/validate-input-request.json index 4904fb0d..9cf8a96b 100644 --- a/schemas/cache/3.1/creative/validate-input-request.json +++ b/schemas/cache/3.1/creative/validate-input-request.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Validate Input Request", - "description": "Request payload for the validate_input task. Lets buyers dry-run a creative manifest against canonical formats and/or specific products before committing to a render. Cheaper than preview_creative (no synthesis cost). Used by build_creative internally to validate inputs before producing output. For genuinely nondeterministic generative platforms (Veo/Sora/Runway-class) where predictive validation is impossible, the platform's own post-synthesis QA loop applies \u2014 validate_input is the predictable-case primitive.\n\nThe `targets[]` array is a discriminated list of validation targets, mirroring the response shape on `validate-input-result.json#target`. Each entry has a `kind` (canonical | product | third_party_format) plus a kind-specific identifier. Discriminated-by-kind on both sides eliminates a wire-shape mismatch: a previous draft used `format_ids: string[]` for canonical names alongside `product_ids: string[]`, which collided with `Product.format_ids: FormatId[]` ({agent_url, id}) \u2014 codegen would emit the same field name with two different types. Discriminated `targets[]` makes the intent explicit and codegen-clean.", + "description": "Request payload for the validate_input task. Lets buyers preflight a creative manifest against canonical formats and/or specific products before committing to a render or other expensive creative-production step. It validates manifest structure and target format constraints only: slot counts, asset types, parameter ranges, and format-shape constraints. It does not rehearse a seller's library mutation, creative_id upsert semantics, package assignments, account authorization, active-delivery protections, or other trafficking-time gates. When the question is final seller acceptance for a creative that is ready to traffic, call sync_creatives directly; use dry_run: true for a non-mutating rehearsal of that operation. Used by build_creative internally to validate inputs before producing output. For genuinely nondeterministic generative platforms (Veo/Sora/Runway-class) where predictive validation is impossible, the platform's own post-synthesis QA loop applies \u2014 validate_input is the predictable-case primitive.\n\nThe `targets[]` array is a discriminated list of validation targets, mirroring the response shape on `validate-input-result.json#target`. Each entry has a `kind` (canonical | product | third_party_format) plus a kind-specific identifier. Discriminated-by-kind on both sides eliminates a wire-shape mismatch: a previous draft used `format_ids: string[]` for canonical names alongside `product_ids: string[]`, which collided with `Product.format_ids: FormatId[]` ({agent_url, id}) \u2014 codegen would emit the same field name with two different types. Discriminated `targets[]` makes the intent explicit and codegen-clean.", "type": "object", "required": [ "manifest" @@ -93,7 +93,7 @@ "additionalProperties": true, "examples": [ { - "description": "Dry-run a video manifest against canonical video_hosted and a specific Meta product in one round-trip", + "description": "Preflight a video manifest against canonical video_hosted and a specific Meta product in one round-trip", "data": { "manifest": { "format_kind": "video_hosted", diff --git a/schemas/cache/3.1/creative/validate-input-response.json b/schemas/cache/3.1/creative/validate-input-response.json index ed14f929..e89ecfc2 100644 --- a/schemas/cache/3.1/creative/validate-input-response.json +++ b/schemas/cache/3.1/creative/validate-input-response.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Validate Input Response", - "description": "Response payload for the validate_input task. Returns per-target validation results \u2014 one entry per format_id or product_id requested. Each result carries a `result_kind` discriminator (`validated_pass` / `validated_fail` / `unvalidatable_nondeterministic`) so callers can branch on three meaningfully different outcomes. The `predicted` field on violations carries the platform's pre-flight estimate (e.g., predicted audio duration from text-length analysis), NOT the actual output \u2014 there is no protocol state for orphaned out-of-spec artifacts. For nondeterministic generative platforms (Veo / Sora / Runway-class with `synthesis_nondeterministic: true`), the result_kind is `unvalidatable_nondeterministic` \u2014 predictive validation is impossible, the platform's post-synthesis QA loop applies on `build_creative`, and out-of-spec output never reaches this surface (instead `build_creative` returns task_failed with synthesis_failed reason).\n\nThe `ValidateInputResult` type is split into its own schema (`/schemas/creative/validate-input-result.json`) rather than inlined here because the same per-target shape is intended for reuse by adjacent async-validation surfaces (planned: per-batch result envelopes on `build_creative` async paths, and asynchronous canonical-against-product validation in `sync_creatives`). Producers that only need the synchronous batch shape today MAY treat the split as YAGNI, but the schema reuse anchors the violation/retry shape so downstream surfaces don't drift.", + "description": "Response payload for the validate_input task. Returns per-target manifest validation results \u2014 one entry per canonical, product, or third-party target requested. Each result carries a `result_kind` discriminator (`validated_pass` / `validated_fail` / `unvalidatable_nondeterministic`) so callers can branch on three meaningfully different outcomes. A pass means the manifest is structurally valid for that target; it does not mean a later sync_creatives call will be accepted, because trafficking-time gates such as account authorization, creative_id upsert state, package assignments, active-delivery protections, and seller review policy are outside validate_input's scope. The `predicted` field on violations carries the platform's pre-flight estimate (e.g., predicted audio duration from text-length analysis), NOT the actual output \u2014 there is no protocol state for orphaned out-of-spec artifacts. For nondeterministic generative platforms (Veo / Sora / Runway-class with `synthesis_nondeterministic: true`), the result_kind is `unvalidatable_nondeterministic` \u2014 predictive validation is impossible, the platform's post-synthesis QA loop applies on `build_creative`, and out-of-spec output never reaches this surface (instead `build_creative` returns task_failed with synthesis_failed reason).\n\nThe `ValidateInputResult` type is split into its own schema (`/schemas/creative/validate-input-result.json`) rather than inlined here because the same per-target shape is intended for reuse by adjacent async-validation surfaces (planned: per-batch result envelopes on `build_creative` async paths, and asynchronous canonical-against-product validation in `sync_creatives`). Producers that only need the synchronous batch shape today MAY treat the split as YAGNI, but the schema reuse anchors the violation/retry shape so downstream surfaces don't drift.", "type": "object", "required": [ "results" diff --git a/schemas/cache/3.1/creative/validate-input-result.json b/schemas/cache/3.1/creative/validate-input-result.json index 31365906..4ae19eec 100644 --- a/schemas/cache/3.1/creative/validate-input-result.json +++ b/schemas/cache/3.1/creative/validate-input-result.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Validate Input Result", - "description": "Per-target result of a validate_input call. The `result_kind` discriminator (replacing the earlier boolean `ok`) lets buyers distinguish three meaningfully different outcomes:\n\n- `validated_pass` \u2014 manifest validates cleanly against the target. Buyers can submit with confidence.\n- `validated_fail` \u2014 manifest is structurally evaluable AND fails specific constraints. `violations[]` enumerates which. Buyers fix and retry.\n- `unvalidatable_nondeterministic` \u2014 predictive validation is impossible because the target's production pipeline is genuinely nondeterministic (Veo / Sora / Runway-class formats with `synthesis_nondeterministic: true`). The platform's own post-synthesis QA loop applies; outcome is unknowable until `build_creative` runs. Buyers MUST plan for the QA-loop semantics: submission may return `task_failed` with a `synthesis_failed` reason if the QA loop exhausts. There is no protocol state for orphaned out-of-spec artifacts.\n\nThe boolean `ok` field carried in earlier drafts is removed \u2014 it conflated `validated_fail` (a real validation result the buyer can act on) with `unvalidatable_nondeterministic` (a structural property of the target the buyer needs to handle differently). `validated_fail` returns `violations[]`; `unvalidatable_nondeterministic` does not (there's nothing to enumerate).\n\n**Scope of validation (normative).** `validate_input` validates **manifest structure** against the canonical / product / third-party format target \u2014 slot counts, asset types, parameter ranges, format-shape constraints. It does NOT predict the surface's per-impression rendering choice. This distinction matters for `composition_model: algorithmic` formats (`responsive_creative`, `agent_placement`) where the surface picks combinations or phrasing at delivery time: the buyer's asset pool can still be structurally validated (does it meet the count/size/length requirements?) \u2192 `validated_pass` or `validated_fail` with violations. What's unpredictable is the rendered output composition, and that's NOT what `validate_input` claims to predict. `unvalidatable_nondeterministic` is reserved for `synthesis_nondeterministic: true` cases where the production pipeline itself can't be evaluated upfront \u2014 distinct from algorithmic composition where the inputs ARE evaluable but the output rendering isn't. Buyers calling `validate_input` on an algorithmic-composition target SHOULD expect a structural verdict (pass/fail), not a rendering preview.", + "description": "Per-target result of a validate_input call. The `result_kind` discriminator (replacing the earlier boolean `ok`) lets buyers distinguish three meaningfully different outcomes:\n\n- `validated_pass` \u2014 manifest is structurally valid against the target. This is not seller trafficking acceptance; use `sync_creatives` or `sync_creatives` with `dry_run: true` for upload/update, account, assignment, policy, and lifecycle gates.\n- `validated_fail` \u2014 manifest is structurally evaluable AND fails specific constraints. `violations[]` enumerates which. Buyers fix and retry.\n- `unvalidatable_nondeterministic` \u2014 predictive validation is impossible because the target's production pipeline is genuinely nondeterministic (Veo / Sora / Runway-class formats with `synthesis_nondeterministic: true`). The platform's own post-synthesis QA loop applies; outcome is unknowable until `build_creative` runs. Buyers MUST plan for the QA-loop semantics: submission may return `task_failed` with a `synthesis_failed` reason if the QA loop exhausts. There is no protocol state for orphaned out-of-spec artifacts.\n\nThe boolean `ok` field carried in earlier drafts is removed \u2014 it conflated `validated_fail` (a real validation result the buyer can act on) with `unvalidatable_nondeterministic` (a structural property of the target the buyer needs to handle differently). `validated_fail` returns `violations[]`; `unvalidatable_nondeterministic` does not (there's nothing to enumerate).\n\n**Scope of validation (normative).** `validate_input` validates **manifest structure** against the canonical / product / third-party format target \u2014 slot counts, asset types, parameter ranges, format-shape constraints. It does NOT predict the surface's per-impression rendering choice. This distinction matters for `composition_model: algorithmic` formats (`responsive_creative`, `agent_placement`) where the surface picks combinations or phrasing at delivery time: the buyer's asset pool can still be structurally validated (does it meet the count/size/length requirements?) \u2192 `validated_pass` or `validated_fail` with violations. What's unpredictable is the rendered output composition, and that's NOT what `validate_input` claims to predict. `unvalidatable_nondeterministic` is reserved for `synthesis_nondeterministic: true` cases where the production pipeline itself can't be evaluated upfront \u2014 distinct from algorithmic composition where the inputs ARE evaluable but the output rendering isn't. Buyers calling `validate_input` on an algorithmic-composition target SHOULD expect a structural verdict (pass/fail), not a rendering preview.", "type": "object", "required": [ "target", diff --git a/schemas/cache/3.1/enums/error-code.json b/schemas/cache/3.1/enums/error-code.json index f75d38b3..0dc3b2c3 100644 --- a/schemas/cache/3.1/enums/error-code.json +++ b/schemas/cache/3.1/enums/error-code.json @@ -103,7 +103,7 @@ "AUTH_MISSING": "No credentials were presented. Sellers MUST return this code when no `Authorization` header was included in the request. Recovery: correctable (provide credentials via the auth header and retry).", "AUTH_INVALID": "Credentials were presented but rejected \u2014 revoked, malformed signature, or a key no longer in the seller's keystore. Sellers MUST return this code when an `Authorization` header was present but verification failed. Recovery: terminal. Exception: agents with a valid OAuth 2.1 refresh grant MAY treat this as correctable when the rejection reason is token expiry \u2014 silently refresh and retry once; if the refresh fails or the seller explicitly signals revocation, escalate to human.", "AUTHORIZATION_REQUIRED": "The caller is authenticated, but the referenced object requires an additional downstream platform connection, identity, creator, or post authorization before the seller can complete the requested action. Typical use: `sync_creatives` with a `published_post` reference where the seller can resolve the post but the owning identity has not authorized paid serving, or authorization has expired/revoked and can be restored. Distinct from `AUTH_MISSING` / `AUTH_INVALID` (caller credentials) and from `PERMISSION_DENIED` (seller policy denies the caller). Sellers SHOULD include recovery details conforming to `error-details/authorization-required.json`, especially `error.details.missing_connections[]` when the caller needs to complete one of several platform connections. Legacy recovery hints such as `authorization_url`, `authorization_instructions`, or `reference_authorization` remain valid when safe to disclose. Recovery: correctable (complete or restore the required authorization, then retry).", - "RATE_LIMITED": "Request rate exceeded. Retry after the retry_after interval. Recovery: transient.", + "RATE_LIMITED": "Request rate exceeded. Sellers SHOULD populate top-level `error.retry_after` with the number of seconds to wait. Recovery: transient (wait `error.retry_after` seconds when present, then retry).", "SERVICE_UNAVAILABLE": "Seller service is temporarily unavailable. Retry with exponential backoff. Recovery: transient.", "CONFIGURATION_ERROR": "The seller's deployment is misconfigured in a way that prevents handling the request \u2014 the buyer cannot fix it, retrying will not help, and reporting to the seller's operator is the only remediation. Examples: account declared with `mode: 'mock'` but no `mock_upstream_url` populated; platform declared with `mode: 'live'` or `mode: 'sandbox'` but no `upstream_url` declared; required environment variable unset on the seller process. Distinct from `INVALID_REQUEST` (buyer-fixable; the request itself is malformed), `SERVICE_UNAVAILABLE` (transient; retry-with-backoff may succeed), `UNSUPPORTED_FEATURE` (capability mismatch \u2014 the seller does not implement the requested specialism), `ACCOUNT_SETUP_REQUIRED` (buyer-side onboarding incomplete; this code is seller-side deployment incomplete), and `GOVERNANCE_UNAVAILABLE` (governance-agent-scoped; transient). Wire placement. The deployment cannot produce a success artifact, so sellers MUST flip transport-level failure markers (HTTP 5xx, MCP `isError: true`, A2A `failed`) and populate both layers per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. The code itself is the discriminator; no `error.details` shape is defined for this code (mirroring the minimal-disclosure precedent of `AGENT_SUSPENDED` / `AGENT_BLOCKED`). Sellers SHOULD populate `error.message` with operator-actionable detail (which metadata key is missing, which env var is unset) and MUST NOT include credentials, connection strings, or stack traces \u2014 the message is wire-visible to the buyer. Recovery: terminal \u2014 the buyer MUST surface to the seller's operator and MUST NOT auto-retry (retries cannot resolve a misconfigured deployment until the operator intervenes).", "POLICY_VIOLATION": "Request violates the seller's content or advertising policies. Recovery: correctable (review policy requirements in the error details).", @@ -133,7 +133,7 @@ "CONFLICT": "Concurrent modification detected. The resource was modified by another request between read and write. Recovery: transient (re-read the resource and retry with current state).", "IDEMPOTENCY_CONFLICT": "An earlier request with the same idempotency_key was processed with a different canonical payload within the seller's replay window. Distinct from CONFLICT (concurrent write) \u2014 this indicates the client reused a key across semantically different requests. Recovery: correctable (use a fresh UUID v4 for the new request, or resend the exact original payload to get the cached response).", "IDEMPOTENCY_EXPIRED": "The idempotency_key was seen previously but its cached response has been evicted because it is past the seller's declared replay_ttl_seconds. Distinct from IDEMPOTENCY_CONFLICT (different payload within window) \u2014 this indicates the retry arrived too late for at-most-once guarantees. Recovery: correctable (perform a natural-key reconciliation \u2014 e.g., call get_media_buys for the relevant account/status scope and match returned media_buys[].context.internal_campaign_id \u2014 to determine whether the original request succeeded, then either accept that result or generate a fresh idempotency_key for a new attempt). If the buyer has any evidence the prior call succeeded (partial response received before crash, entry in the buyer's own DB, a webhook fired), the buyer MUST do the natural-key reconciliation BEFORE minting a new key \u2014 minting a new key in that situation is exactly how double-creation happens.", - "IDEMPOTENCY_IN_FLIGHT": "A prior request with the same `idempotency_key` is still being processed and has not yet produced a cached response. The second request arrived before the first completed. Sellers MAY return this code instead of blocking the second caller until the first finishes \u2014 useful when the first call invokes a slow downstream system (SSP, ad server, payment provider). Distinct from IDEMPOTENCY_CONFLICT (different canonical payload \u2014 a client bug) and from CONFLICT (concurrent modification of a different resource) \u2014 IDEMPOTENCY_IN_FLIGHT is the seller telling the buyer 'your retry was correct but your previous attempt is still running, come back shortly.' Sellers SHOULD populate `error.details.retry_after` (seconds, integer) with a wait hint based on the first request's elapsed time and expected completion. Buyers MUST treat this as transient and MUST NOT mint a fresh `idempotency_key` \u2014 minting a new key turns a safe retry into a double-execution race. Recovery: transient (wait `retry_after` seconds and retry with the same `idempotency_key`; the second attempt will either replay the now-cached response or, if still in flight, return IDEMPOTENCY_IN_FLIGHT again).", + "IDEMPOTENCY_IN_FLIGHT": "A prior request with the same `idempotency_key` is still being processed and has not yet produced a cached response. The second request arrived before the first completed. Sellers MAY return this code instead of blocking the second caller until the first finishes \u2014 useful when the first call invokes a slow downstream system (SSP, ad server, payment provider). Distinct from IDEMPOTENCY_CONFLICT (different canonical payload \u2014 a client bug) and from CONFLICT (concurrent modification of a different resource) \u2014 IDEMPOTENCY_IN_FLIGHT is the seller telling the buyer 'your retry was correct but your previous attempt is still running, come back shortly.' Sellers SHOULD populate top-level `error.retry_after` (seconds) with a wait hint based on the first request's elapsed time and expected completion. Buyers MUST treat this as transient and MUST NOT mint a fresh `idempotency_key` \u2014 minting a new key turns a safe retry into a double-execution race. Recovery: transient (wait `error.retry_after` seconds and retry with the same `idempotency_key`; the second attempt will either replay the now-cached response or, if still in flight, return IDEMPOTENCY_IN_FLIGHT again).", "INVALID_STATE": "Operation is not permitted for the resource's current status (e.g., updating a completed or canceled media buy, or modifying a canceled package). Recovery: correctable (check current status via get_media_buys and adjust request).", "MEDIA_BUY_NOT_FOUND": "Referenced media buy does not exist or is not accessible to the requesting agent. Recovery: correctable (verify media_buy_id; when recovering across legacy sellers or missing echoed IDs, reconcile via get_media_buys and the opaque request/response context correlation handle, such as context.internal_campaign_id, rather than deprecated top-level buyer_ref).", "NOT_CANCELLABLE": "The media buy or package cannot be canceled in its current state. The seller may have contractual or operational constraints that prevent cancellation. Recovery: correctable (check the seller's cancellation policy or contact the seller).", @@ -215,7 +215,7 @@ }, "RATE_LIMITED": { "recovery": "transient", - "suggestion": "retry after the retry_after interval" + "suggestion": "wait top-level error.retry_after seconds when present, then retry" }, "SERVICE_UNAVAILABLE": { "recovery": "transient", @@ -327,7 +327,7 @@ }, "IDEMPOTENCY_IN_FLIGHT": { "recovery": "transient", - "suggestion": "wait error.details.retry_after seconds and retry with the SAME idempotency_key \u2014 MUST NOT mint a fresh key (turns a safe retry into a double-execution race)" + "suggestion": "wait top-level error.retry_after seconds and retry with the SAME idempotency_key \u2014 MUST NOT mint a fresh key (turns a safe retry into a double-execution race)" }, "CREATIVE_DEADLINE_EXCEEDED": { "recovery": "correctable", diff --git a/schemas/cache/3.1/extensions/index.json b/schemas/cache/3.1/extensions/index.json index a4b20617..30f5535b 100644 --- a/schemas/cache/3.1/extensions/index.json +++ b/schemas/cache/3.1/extensions/index.json @@ -3,6 +3,6 @@ "title": "AdCP Extension Registry", "description": "Auto-generated registry of formal AdCP extensions. Extensions provide typed schemas for vendor-specific or domain-specific data within the ext field. Agents declare which extensions they support in their agent card.", "_generated": true, - "_generatedAt": "2026-06-30T19:14:39.178Z", + "_generatedAt": "2026-07-28T13:04:06.049Z", "extensions": {} } \ No newline at end of file diff --git a/schemas/cache/3.1/index.json b/schemas/cache/3.1/index.json index ebc66a61..98e21b0d 100644 --- a/schemas/cache/3.1/index.json +++ b/schemas/cache/3.1/index.json @@ -3,12 +3,12 @@ "title": "AdCP Schema Registry", "version": "1.0.0", "description": "Registry of all AdCP JSON schemas for validation and discovery", - "adcp_version": "3.1.1", + "adcp_version": "3.1.8", "versioning": { - "note": "AdCP uses build-time versioning. This directory contains schemas for AdCP 3.1.1. Full semantic versions are available at /schemas/{version}/ (e.g., /schemas/2.5.0/). Major version aliases point to the latest stable release in that major line; use /schemas/index.json or /schemas/latest.json for the canonical file-based pointer." + "note": "AdCP uses build-time versioning. This directory contains schemas for AdCP 3.1.8. Full semantic versions are available at /schemas/{version}/ (e.g., /schemas/2.5.0/). Major version aliases point to the latest stable release in that major line; use /schemas/index.json or /schemas/latest.json for the canonical file-based pointer." }, - "lastUpdated": "2026-06-30", - "baseUrl": "/schemas/3.1.1", + "lastUpdated": "2026-07-28", + "baseUrl": "/schemas/3.1.8", "stability": "stable", "prerelease": false, "deprecated": false, @@ -2015,5 +2015,5 @@ "code": "// Use everit-org/json-schema or similar library" } ], - "published_version": "3.1.1" + "published_version": "3.1.8" } \ No newline at end of file diff --git a/schemas/cache/3.1/manifest.json b/schemas/cache/3.1/manifest.json index 405fbb24..c1e4b2e8 100644 --- a/schemas/cache/3.1/manifest.json +++ b/schemas/cache/3.1/manifest.json @@ -1,7 +1,7 @@ { - "$schema": "/schemas/3.1.1/manifest.schema.json", - "adcp_version": "3.1.1", - "generated_at": "2026-06-30T19:14:39.349Z", + "$schema": "/schemas/3.1.8/manifest.schema.json", + "adcp_version": "3.1.8", + "generated_at": "2026-07-28T13:04:06.237Z", "tools": { "acquire_rights": { "protocol": "brand", @@ -607,8 +607,7 @@ "brand_rights", "governance_aware_seller", "governance_delivery_monitor", - "governance_spend_authority", - "signal_marketplace" + "governance_spend_authority" ] }, "update_collection_list": { @@ -740,8 +739,8 @@ }, "RATE_LIMITED": { "recovery": "transient", - "description": "Request rate exceeded. Retry after the retry_after interval.", - "suggestion": "retry after the retry_after interval" + "description": "Request rate exceeded. Sellers SHOULD populate top-level `error.retry_after` with the number of seconds to wait.", + "suggestion": "wait top-level error.retry_after seconds when present, then retry" }, "SERVICE_UNAVAILABLE": { "recovery": "transient", @@ -880,8 +879,8 @@ }, "IDEMPOTENCY_IN_FLIGHT": { "recovery": "transient", - "description": "A prior request with the same `idempotency_key` is still being processed and has not yet produced a cached response. The second request arrived before the first completed. Sellers MAY return this code instead of blocking the second caller until the first finishes \u2014 useful when the first call invokes a slow downstream system (SSP, ad server, payment provider). Distinct from IDEMPOTENCY_CONFLICT (different canonical payload \u2014 a client bug) and from CONFLICT (concurrent modification of a different resource) \u2014 IDEMPOTENCY_IN_FLIGHT is the seller telling the buyer 'your retry was correct but your previous attempt is still running, come back shortly.' Sellers SHOULD populate `error.details.retry_after` (seconds, integer) with a wait hint based on the first request's elapsed time and expected completion. Buyers MUST treat this as transient and MUST NOT mint a fresh `idempotency_key` \u2014 minting a new key turns a safe retry into a double-execution race.", - "suggestion": "wait error.details.retry_after seconds and retry with the SAME idempotency_key \u2014 MUST NOT mint a fresh key (turns a safe retry into a double-execution race)" + "description": "A prior request with the same `idempotency_key` is still being processed and has not yet produced a cached response. The second request arrived before the first completed. Sellers MAY return this code instead of blocking the second caller until the first finishes \u2014 useful when the first call invokes a slow downstream system (SSP, ad server, payment provider). Distinct from IDEMPOTENCY_CONFLICT (different canonical payload \u2014 a client bug) and from CONFLICT (concurrent modification of a different resource) \u2014 IDEMPOTENCY_IN_FLIGHT is the seller telling the buyer 'your retry was correct but your previous attempt is still running, come back shortly.' Sellers SHOULD populate top-level `error.retry_after` (seconds) with a wait hint based on the first request's elapsed time and expected completion. Buyers MUST treat this as transient and MUST NOT mint a fresh `idempotency_key` \u2014 minting a new key turns a safe retry into a double-execution race.", + "suggestion": "wait top-level error.retry_after seconds and retry with the SAME idempotency_key \u2014 MUST NOT mint a fresh key (turns a safe retry into a double-execution race)" }, "CREATIVE_DEADLINE_EXCEEDED": { "recovery": "correctable", @@ -1498,8 +1497,7 @@ "get_adcp_capabilities", "get_signals", "sync_accounts", - "sync_governance", - "sync_plans" + "sync_governance" ] }, "signal_owned": { diff --git a/schemas/cache/3.1/media-buy/get-media-buy-delivery-response.json b/schemas/cache/3.1/media-buy/get-media-buy-delivery-response.json index b180493e..9de117ea 100644 --- a/schemas/cache/3.1/media-buy/get-media-buy-delivery-response.json +++ b/schemas/cache/3.1/media-buy/get-media-buy-delivery-response.json @@ -128,8 +128,11 @@ "minimum": 0 }, "completion_rate": { - "type": "number", - "description": "Aggregate completion rate across all media buys (weighted by impressions, not a simple average of per-buy rates)", + "type": [ + "number", + "null" + ], + "description": "Aggregate completion rate across all media buys (weighted by impressions, not a simple average of per-buy rates). Null indicates the metric is not applicable to the aggregated buys (e.g. all non-video inventory).", "minimum": 0, "maximum": 1 }, diff --git a/schemas/cache/3.1/protocol/get-adcp-capabilities-response.json b/schemas/cache/3.1/protocol/get-adcp-capabilities-response.json index 9d30adca..1f227b6b 100644 --- a/schemas/cache/3.1/protocol/get-adcp-capabilities-response.json +++ b/schemas/cache/3.1/protocol/get-adcp-capabilities-response.json @@ -79,7 +79,7 @@ }, "in_flight_max_seconds": { "type": "integer", - "description": "Maximum lifetime in seconds of an in-flight idempotency row before the seller releases it per L1/security.mdx rule 9 (treat the in-flight attempt as failed if the handler does not complete within this bound). Buyer SDKs use this value to compute a retry budget when they see `IDEMPOTENCY_IN_FLIGHT` \u2014 cap individual retry waits at this value rather than the much-wider `replay_ttl_seconds` ceiling. Optional in 3.1 (additive declaration); SDKs that don't see the field fall back to rule 9's order-of-magnitude SHOULD heuristic. Required when `supported: true` in 4.0. MUST be no greater than `replay_ttl_seconds` (a bound larger than the replay window is vacuous \u2014 any retry past the TTL hits IDEMPOTENCY_EXPIRED regardless of in-flight state); validators MUST enforce this cross-field constraint at the test layer since JSON Schema cannot express field-relative bounds. A buyer that observes `error.details.retry_after` exceeding this value MAY treat that as a seller bug \u2014 the in-flight row cannot legitimately outlive the bound the seller declared.", + "description": "Maximum lifetime in seconds of an in-flight idempotency row before the seller releases it per L1/security.mdx rule 9 (treat the in-flight attempt as failed if the handler does not complete within this bound). Buyer SDKs use this value to compute a retry budget when they see `IDEMPOTENCY_IN_FLIGHT` \u2014 cap individual retry waits at this value rather than the much-wider `replay_ttl_seconds` ceiling. Optional in 3.1 (additive declaration); SDKs that don't see the field fall back to rule 9's order-of-magnitude SHOULD heuristic. Required when `supported: true` in 4.0. MUST be no greater than `replay_ttl_seconds` (a bound larger than the replay window is vacuous \u2014 any retry past the TTL hits IDEMPOTENCY_EXPIRED regardless of in-flight state); validators MUST enforce this cross-field constraint at the test layer since JSON Schema cannot express field-relative bounds. A buyer that observes top-level `error.retry_after` exceeding this value MAY treat that as a seller bug \u2014 the in-flight row cannot legitimately outlive the bound the seller declared.", "minimum": 1, "maximum": 604800 }, diff --git a/schemas/cache/3.1/registries/v1-canonical-mapping.json b/schemas/cache/3.1/registries/v1-canonical-mapping.json index a1d3744c..10b06e2e 100644 --- a/schemas/cache/3.1/registries/v1-canonical-mapping.json +++ b/schemas/cache/3.1/registries/v1-canonical-mapping.json @@ -1,9 +1,9 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "v1 \u2192 v2 Canonical Format Mapping Registry", - "description": "Authoritative AAO-published mapping from v1 named formats to v2 canonical declarations. Used by SDKs to project the v1 wire shape into v2 canonical declarations during the migration window.\n\n**Direction of truth (normative).** This registry is authoritative for **v1 \u2192 v2 projection only**. v2 \u2192 v1 projection MUST rely on `v1_format_ref` on the v2 `ProductFormatDeclaration` \u2014 sellers assert the v1 pairing explicitly. SDKs MUST NOT synthesize a v1 `format_id` from the registry by inverting structural matches: the registry's `id` slugs lack the `agent_url` half of a `FormatId`, and registry entries with `format_id_glob: '*'` or pure-structural matches have no single literal to invert to. SDKs that synthesize unilaterally produce inter-SDK divergence on structurally-equal values (different SDKs pick different `agent_url` and `id` patterns for the same v2 declaration). When a v2 declaration carries no `v1_format_ref` and a v1-only buyer queries the product, the SDK reports the canonical as v1-unreachable via `FORMAT_DECLARATION_V1_AMBIGUOUS` and lets the buyer surface it to the seller; the seller's path is to add `v1_format_ref` to disambiguate.\n\nBest-effort inversion is not available from this registry: as of 3.1 all published entries are pure-structural (family-level), and structural matches are not invertible to a specific v1 `format_id`. Future versions MAY add literal `format_id_glob` entries for platform-specific v1 conventions; in that case SDKs MAY do best-effort inversion for non-wildcarded literals only, AND only when the seller hasn't authored a contradicting `v1_format_ref` \u2014 even then, the projection is non-normative and downstream consumers MUST NOT depend on it.\n\n**Resolution order** (per RFC #3305 amendment #3767, normative):\n1. **Authoritative v2\u2192v1 link**: if any v2 `ProductFormatDeclaration` on the same product carries `v1_format_ref` pointing at this v1 format_id, use that v2 declaration. Highest priority \u2014 seller asserts the link directly. SDKs SHOULD run the *narrows* check (canonical-formats.mdx 'Narrows \u2014 formal definition') between the v2 declaration's `params` and the referenced v1 format's `requirements`; on conflict, surface `FORMAT_DECLARATION_DIVERGENT` on the `get_products` response `errors[]`. Without the narrowing check, `v1_format_ref` is a hint rather than a contract.\n2. **Seller-asserted on the v1 file**: if the v1 format declaration carries an explicit `canonical` field, use it. (Note: `canonical_parameters` on the v1 file is deprecated for 3.1; SDKs reading 3.1 catalogs MUST still honor it when present, but `v1_format_ref` is the path forward.)\n3. **Registry glob**: look up `format_id` in this registry's `format_id_glob` entries.\n4. **Structural match**: attempt structural match against this registry's `structural` entries. A successful structural match yields a *family-level* identification only (e.g., 'this is a `video_vast`') \u2014 it does NOT yield a specific v1 `format_id`. SDKs use structural match for v1 \u2192 v2 projection (the inbound direction) but MUST NOT invert it to a v1 `format_id` on the outbound path.\n5. **Ambiguous family**: if step 4 succeeded but the matched entries are family-only (pure structural, no invertible literal), the canonical is **v1-unreachable for THIS specific product** unless the seller authors `v1_format_ref`. SDKs MUST surface `FORMAT_DECLARATION_V1_AMBIGUOUS` via `errors[]` augmentation rather than synthesize a plausible-but-arbitrary v1 `format_id`. Distinct from canonical-level v1-unreachability (a canonical with `v1_translatable: false` \u2014 `agent_placement`, `sponsored_placement`, `responsive_creative`, `image_carousel` \u2014 never has any v1 form regardless of registry coverage).\n6. **Fail closed**: SDK MUST NOT emit `format_options` for products carrying this format. SDKs MUST augment the response's `errors[]` array with an entry carrying `source: \"sdk\"`, `sdk_id: \"@\"`, `code: \"FORMAT_PROJECTION_FAILED\"`, `field: \"products[N].format_ids[K]\"`, and `error.details: { format_id, product_id, resolution_failure: \"no_explicit_canonical\" | \"no_registry_match\" | \"no_structural_match\" }`. Single mandated surface (`errors[]` augmentation) \u2014 lint-output channels are NOT acceptable; the multi-hop agent network needs warnings to propagate across SDK boundaries via the wire response. Logger-only warnings die in DEBUG. The advisory is non-fatal: the response stays 200/success, the product is still valid on the v1 path, only the v2 `format_options` projection is absent. Consumer-side counterpart to the producer SHOULD (sellers should add a v2 declaration with `v1_format_ref`, an explicit `canonical` field, or file a registry PR).\n\n**Match modes:**\n- `format_id_glob` \u2014 exact / glob match against the v1 `format_id.id`. **As of 3.1 the registry carries zero literal entries**: AAO-catalog-published formats (display_300x250_image, video_vast_30s, audio_standard_30s, etc.) project via resolution-order step 2 (catalog entry's `canonical:` annotation), and platform-specific formats (e.g., Meta Reels, TikTok Spark Ads) project via structural fallback or via the platform's own adagents.json `formats[]` block (#4620). A future literal entry is only justified when (a) the v1 name carries semantic narrowing not recoverable from slot shape AND (b) no platform-published adagents.json exists; such entries land via the AAO governance PR process with a documented rationale. The whole point of canonical-formats is parametrization: ONE `image` canonical with width/height params, not 8 per-size variants. Glob syntax: `*` matches any segment.\n- `structural` \u2014 match against the format's slot shape, asset types, and version constraints. The PRIMARY fallback for v1 wire traffic \u2014 catches custom v1 formats (a publisher's `acme_homepage_300x250` is structurally an IAB MREC) without enumerating every possible v1 name. v1 sellers in the wild naming things their own way are handled here, not by literal globs.\n\n**Alias collision precedence (normative).** When a v1 format's `assets[i]` carries multiple `asset_group_id` aliases that resolve to the same canonical asset_group (e.g., two slots both aliasing to `landing_page_url`), the SDK MUST resolve deterministically: the v1 format's `assets[*]` array order is authoritative \u2014 the first slot in declaration order wins, subsequent collisions are dropped from the projected v2 manifest and surfaced via `FORMAT_PROJECTION_FAILED` with `error.details: { collision_kind: \"asset_group_id_alias\", asset_group_id, winning_slot_id, dropped_slot_ids }`. SDKs MUST NOT silently pick one and discard the other without surfacing \u2014 silent picking creates inter-SDK divergence. Producers SHOULD avoid the collision by deduplicating aliased slots or using distinct `asset_group_id` values when both slots are semantically meaningful.\n\n**Governance**: same vocabulary-governance rules as `asset-group-vocabulary.json` and `format-shape-vocabulary.json` \u2014 additions land via PR with rationale + \u22651 reference adopter; AAO maintainer review; versioned + content-digested. Entries are additive; once published they are not removed (they may be marked `deprecated: true` if superseded).\n\n**Initial scope (3.1)**: 7 pure-structural fallback entries covering VAST 4.x / legacy VAST, DAAST 1.x, HTML5 zip bundles, hosted video, hosted audio, and url-shaped display tags. Per-size and per-duration literals (display_300x250_image, video_vast_30s, etc.) are NOT enumerated here \u2014 those project via catalog `canonical:` annotation (resolution-order step 2), keeping the registry aligned with the canonical-formats parametrization principle. Platform-specific formats (Meta Reels, TikTok Spark Ads, etc.) project via structural fallback or via the platform's own adagents.json `formats[]` block (#4620). The full v1-format audit dataset (~76% of formats from the 12-platform / 86-format audit in #3305) seeds the long-term roadmap and informs future literal-entry decisions.\n\nDigest the file content (sha256) when emitting in capabilities responses or referencing from SDK output. Buyers cache by `version` + `digest`.", - "version": "1.0.0", - "last_updated": "2026-05-01", + "description": "Authoritative AAO-published mapping from v1 named formats to v2 canonical declarations. Used by SDKs to project the v1 wire shape into v2 canonical declarations during the migration window.\n\n**Direction of truth (normative).** This registry is authoritative for **v1 \u2192 v2 projection only**. v2 \u2192 v1 projection MUST rely on `v1_format_ref` on the v2 `ProductFormatDeclaration` \u2014 sellers assert the v1 pairing explicitly. SDKs MUST NOT synthesize a v1 `format_id` from the registry by inverting structural matches: the registry's `id` slugs lack the `agent_url` half of a `FormatId`, and registry entries with `format_id_glob: '*'` or pure-structural matches have no single literal to invert to. SDKs that synthesize unilaterally produce inter-SDK divergence on structurally-equal values (different SDKs pick different `agent_url` and `id` patterns for the same v2 declaration). When a v2 declaration carries no `v1_format_ref` and a v1-only buyer queries the product, the SDK reports the canonical as v1-unreachable via `FORMAT_DECLARATION_V1_AMBIGUOUS` and lets the buyer surface it to the seller; the seller's path is to add `v1_format_ref` to disambiguate.\n\nBest-effort inversion is available only from this registry's non-wildcarded literal format_id_glob entries. Structural matches remain family-level and are not invertible to a specific v1 format_id. SDKs MAY do best-effort inversion for non-wildcarded literals only, AND only when the seller hasn't authored a contradicting `v1_format_ref` \u2014 even then, the projection is non-normative and downstream consumers MUST NOT depend on it.\n\n**Resolution order** (per RFC #3305 amendment #3767, normative):\n1. **Authoritative v2\u2192v1 link**: if any v2 `ProductFormatDeclaration` on the same product carries `v1_format_ref` pointing at this v1 format_id, use that v2 declaration. Highest priority \u2014 seller asserts the link directly. SDKs SHOULD run the *narrows* check (canonical-formats.mdx 'Narrows \u2014 formal definition') between the v2 declaration's `params` and the referenced v1 format's `requirements`; on conflict, surface `FORMAT_DECLARATION_DIVERGENT` on the `get_products` response `errors[]`. Without the narrowing check, `v1_format_ref` is a hint rather than a contract.\n2. **Seller-asserted on the v1 file**: if the v1 format declaration carries an explicit `canonical` field, use it. (Note: `canonical_parameters` on the v1 file is deprecated for 3.1; SDKs reading 3.1 catalogs MUST still honor it when present, but `v1_format_ref` is the path forward.)\n3. **Registry glob**: look up `format_id` in this registry's `format_id_glob` entries.\n4. **Structural match**: attempt structural match against this registry's `structural` entries. A successful structural match yields a *family-level* identification only (e.g., 'this is a `video_vast`') \u2014 it does NOT yield a specific v1 `format_id`. SDKs use structural match for v1 \u2192 v2 projection (the inbound direction) but MUST NOT invert it to a v1 `format_id` on the outbound path.\n5. **Ambiguous family**: if step 4 succeeded but the matched entries are family-only (pure structural, no invertible literal), the canonical is **v1-unreachable for THIS specific product** unless the seller authors `v1_format_ref`. SDKs MUST surface `FORMAT_DECLARATION_V1_AMBIGUOUS` via `errors[]` augmentation rather than synthesize a plausible-but-arbitrary v1 `format_id`. Distinct from canonical-level v1-unreachability (a canonical with `v1_translatable: false` \u2014 `agent_placement`, `sponsored_placement`, `responsive_creative`, `image_carousel` \u2014 never has any v1 form regardless of registry coverage).\n6. **Fail closed**: SDK MUST NOT emit `format_options` for products carrying this format. SDKs MUST augment the response's `errors[]` array with an entry carrying `source: \"sdk\"`, `sdk_id: \"@\"`, `code: \"FORMAT_PROJECTION_FAILED\"`, `field: \"products[N].format_ids[K]\"`, and `error.details: { format_id, product_id, resolution_failure: \"no_explicit_canonical\" | \"no_registry_match\" | \"no_structural_match\" }`. Single mandated surface (`errors[]` augmentation) \u2014 lint-output channels are NOT acceptable; the multi-hop agent network needs warnings to propagate across SDK boundaries via the wire response. Logger-only warnings die in DEBUG. The advisory is non-fatal: the response stays 200/success, the product is still valid on the v1 path, only the v2 `format_options` projection is absent. Consumer-side counterpart to the producer SHOULD (sellers should add a v2 declaration with `v1_format_ref`, an explicit `canonical` field, or file a registry PR).\n\n**Match modes:**\n- `format_id_glob` \u2014 exact / glob match against the v1 `format_id.id`. Published non-wildcarded literals cover independently observed legacy conventions whose names carry semantic narrowing (duration, dimensions, or VAST delivery) that slot shape alone cannot recover. Each literal mapping preserves those constraints in canonical parameters; AAO-catalog-published formats with explicit canonical annotations still project via resolution-order step 2. Glob syntax: `*` matches any segment.\n- `structural` \u2014 match against the format's slot shape, asset types, and version constraints. The PRIMARY fallback for v1 wire traffic \u2014 catches custom v1 formats (a publisher's `acme_homepage_300x250` is structurally an IAB MREC) without enumerating every possible v1 name. v1 sellers in the wild naming things their own way are handled here, not by literal globs.\n\n**Alias collision precedence (normative).** When a v1 format's `assets[i]` carries multiple `asset_group_id` aliases that resolve to the same canonical asset_group (e.g., two slots both aliasing to `landing_page_url`), the SDK MUST resolve deterministically: the v1 format's `assets[*]` array order is authoritative \u2014 the first slot in declaration order wins, subsequent collisions are dropped from the projected v2 manifest and surfaced via `FORMAT_PROJECTION_FAILED` with `error.details: { collision_kind: \"asset_group_id_alias\", asset_group_id, winning_slot_id, dropped_slot_ids }`. SDKs MUST NOT silently pick one and discard the other without surfacing \u2014 silent picking creates inter-SDK divergence. Producers SHOULD avoid the collision by deduplicating aliased slots or using distinct `asset_group_id` values when both slots are semantically meaningful.\n\n**Governance**: same vocabulary-governance rules as `asset-group-vocabulary.json` and `format-shape-vocabulary.json` \u2014 additions land via PR with rationale + \u22651 reference adopter; AAO maintainer review; versioned + content-digested. Entries are additive; once published they are not removed (they may be marked `deprecated: true` if superseded).\n\n**Scope**: 7 structural fallback entries cover VAST 4.x / legacy VAST, DAAST 1.x, HTML5 zip bundles, hosted video, hosted audio, and url-shaped display tags. Literal entries cover independently observed legacy duration-, dimension-, and VAST-token naming conventions. Durationless placement ids video_pre_roll and video_mid_roll are deliberately omitted: projecting either to video_hosted without duration_ms_exact would resolve to an untraffickable format and defer failure until ad-server line-item creation. Failing closed during discovery is safer. Platform-specific formats (Meta Reels, TikTok Spark Ads, etc.) project via structural fallback or via the platform's own adagents.json `formats[]` block (#4620). The full v1-format audit dataset (~76% of formats from the 12-platform / 86-format audit in #3305) seeds the long-term roadmap and informs future literal-entry decisions.\n\nDigest the file content (sha256) when emitting in capabilities responses or referencing from SDK output. Buyers cache by `version` + `digest`.", + "version": "1.2.0", + "last_updated": "2026-07-28", "type": "object", "required": [ "version", @@ -122,6 +122,283 @@ } }, "mappings": [ + { + "v1_pattern": { + "format_id_glob": "display_static" + }, + "v2": { + "canonical": "image" + }, + "notes": "Observed AAO-namespace static display id. The absent size constraint is intentional: the id carries no dimensions, so execution narrows size from the matched creative's intrinsic dimensions." + }, + { + "v1_pattern": { + "format_id_glob": "display_300x250" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 300, + "height": 250 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_728x90" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 728, + "height": 90 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_320x50" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 320, + "height": 50 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_300x600" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 300, + "height": 600 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_970x250" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 970, + "height": 250 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_250x250" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 250, + "height": 250 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_200x200" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 200, + "height": 200 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_300x50" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 300, + "height": 50 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_320x480" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 320, + "height": 480 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_320x400" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 320, + "height": 400 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_320x320" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 320, + "height": 320 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "display_320x250" + }, + "v2": { + "canonical": "image", + "parameters": { + "width": 320, + "height": 250 + } + }, + "notes": "Observed AAO-namespace unsuffixed display size id. Exact pixel dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "video_30s" + }, + "v2": { + "canonical": "video_hosted", + "parameters": { + "duration_ms_exact": 30000 + } + }, + "notes": "Observed legacy duration suffix. Duration is retained so the projected format remains traffickable." + }, + { + "v1_pattern": { + "format_id_glob": "video_15s" + }, + "v2": { + "canonical": "video_hosted", + "parameters": { + "duration_ms_exact": 15000 + } + }, + "notes": "Observed legacy duration suffix. Duration is retained so the projected format remains traffickable." + }, + { + "v1_pattern": { + "format_id_glob": "video_pre_roll_30s" + }, + "v2": { + "canonical": "video_hosted", + "parameters": { + "duration_ms_exact": 30000 + } + }, + "notes": "Observed legacy placement and duration suffix. Placement does not change the canonical; duration is retained." + }, + { + "v1_pattern": { + "format_id_glob": "video_pre_roll_15s" + }, + "v2": { + "canonical": "video_hosted", + "parameters": { + "duration_ms_exact": 15000 + } + }, + "notes": "Observed legacy placement and duration suffix. Placement does not change the canonical; duration is retained." + }, + { + "v1_pattern": { + "format_id_glob": "video_640x480" + }, + "v2": { + "canonical": "video_hosted", + "parameters": { + "width": 640, + "height": 480 + } + }, + "notes": "Observed legacy dimension suffix. Exact dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "video_300x250" + }, + "v2": { + "canonical": "video_hosted", + "parameters": { + "width": 300, + "height": 250 + } + }, + "notes": "Observed legacy dimension suffix. Exact dimensions are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "video_16x9_30s" + }, + "v2": { + "canonical": "video_hosted", + "parameters": { + "aspect_ratio": "16:9", + "duration_ms_exact": 30000 + } + }, + "notes": "Observed legacy aspect-ratio and duration suffixes. Both constraints are retained in the projected parameters." + }, + { + "v1_pattern": { + "format_id_glob": "video_640x360_vast" + }, + "v2": { + "canonical": "video_vast", + "parameters": { + "width": 640, + "height": 360 + } + }, + "notes": "The VAST token selects video_vast wherever it occurs in a legacy id, including this observed suffix form; exact dimensions are retained." + }, + { + "v1_pattern": { + "format_id_glob": "audio_15s" + }, + "v2": { + "canonical": "audio_hosted", + "parameters": { + "duration_ms_exact": 15000 + } + }, + "notes": "Observed legacy duration suffix. Duration is retained so the projected format remains traffickable." + }, { "v1_pattern": { "structural": { diff --git a/schemas/cache/3.1/signals/activate-signal-request.json b/schemas/cache/3.1/signals/activate-signal-request.json index 75299003..2162dfd3 100644 --- a/schemas/cache/3.1/signals/activate-signal-request.json +++ b/schemas/cache/3.1/signals/activate-signal-request.json @@ -37,6 +37,13 @@ "description": "The pricing option selected from the signal's pricing_options in the get_signals response. Required when the signal has pricing options. Records the buyer's pricing commitment at activation time; pass this same value in report_usage for billing verification.", "x-entity": "vendor_pricing_option" }, + "governance_context": { + "type": "string", + "description": "Opaque governance context returned by check_governance for this signal activation. Required when the account has a registered governance agent; signal agents MUST reject governed activations that omit a valid context.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, "account": { "$ref": "../core/account-ref.json", "description": "Account for this activation. Associates with a commercial relationship established via sync_accounts." diff --git a/scripts/generate_ergonomic_coercion.py b/scripts/generate_ergonomic_coercion.py index 968606d8..3d95dbd7 100644 --- a/scripts/generate_ergonomic_coercion.py +++ b/scripts/generate_ergonomic_coercion.py @@ -454,7 +454,6 @@ def _find_media_buy_success_variant(module: Any) -> type[_PydBaseModel] | None: lines.append(" ListCreativeFormatsResponse,") lines.append(")") lines.append("from adcp.types.generated_poc.creative.list_creatives_response import (") - lines.append(" Creative,") lines.append(" ListCreativesResponse,") lines.append(")") @@ -479,7 +478,6 @@ def _find_media_buy_success_variant(module: Any) -> type[_PydBaseModel] | None: "GetProductsResponse", "ListCreativeFormatsResponse", "CreativeAgent", - "Creative", "ListCreativesResponse", } request_imports_sorted = sorted(set(request_imports)) diff --git a/scripts/post_generate_fixes.py b/scripts/post_generate_fixes.py index 0c07d177..c8c9fe94 100644 --- a/scripts/post_generate_fixes.py +++ b/scripts/post_generate_fixes.py @@ -3587,6 +3587,68 @@ def fix_registry_collection_payload_status_override() -> None: print(" core/registry_event.py: suppressed collection status override") +def fix_list_creatives_format_reference_xor() -> None: + """Restore list_creatives response ``format_id`` XOR ``format_kind``. + + The schema models list_creatives creative records as a oneOf: + legacy items require ``format_id`` and forbid ``format_kind``; + canonical items require ``format_kind`` and forbid ``format_id``. + datamodel-code-generator preserves the required fields but drops the + opposing ``not`` constraints, so add runtime validators to the generated + branch models. + """ + + target = OUTPUT_DIR / "creative" / "list_creatives_response.py" + if not target.exists(): + print(" creative/list_creatives_response.py: not found (skipping)") + return + + source = target.read_text() + if "_reject_canonical_format_ref" in source and "_reject_legacy_format_ref" in source: + print(" creative/list_creatives_response.py: format reference XOR already fixed") + return + + source = source.replace( + "from pydantic import AwareDatetime, ConfigDict, Field, RootModel, StringConstraints", + "from pydantic import AwareDatetime, ConfigDict, Field, RootModel, StringConstraints, model_validator", + 1, + ) + + legacy_validator = """ + + @model_validator(mode='after') + def _reject_canonical_format_ref(self) -> Creatives: + if self.format_kind is not None: + raise ValueError('format_id and format_kind are mutually exclusive') + return self +""" + canonical_validator = """ + + @model_validator(mode='after') + def _reject_legacy_format_ref(self) -> Creatives1: + if self.format_id is not None: + raise ValueError('format_id and format_kind are mutually exclusive') + return self +""" + + if "_reject_canonical_format_ref" not in source: + source = source.replace( + "\n\nclass Creatives1(AdCPBaseModel):", + legacy_validator + "\n\nclass Creatives1(AdCPBaseModel):", + 1, + ) + if "_reject_legacy_format_ref" not in source: + source = source.replace( + "\n\nclass ListCreativesResponse(AdcpVersionEnvelope, ProtocolEnvelope):", + canonical_validator + + "\n\nclass ListCreativesResponse(AdcpVersionEnvelope, ProtocolEnvelope):", + 1, + ) + + target.write_text(source) + print(" creative/list_creatives_response.py: added format reference XOR validators") + + def strip_extra_blank_lines_at_eof() -> None: """Normalize generated Python files to one trailing newline.""" changed = 0 @@ -3641,6 +3703,7 @@ def main(): fix_verify_brand_claim_models, fix_signal_coverage_forecast_point_types, fix_registry_collection_payload_status_override, + fix_list_creatives_format_reference_xor, rewrite_generated_enums_to_strenum, strip_extra_blank_lines_at_eof, ] diff --git a/skills/call-adcp-agent/SKILL.md b/skills/call-adcp-agent/SKILL.md index 41218012..765ccc25 100644 --- a/skills/call-adcp-agent/SKILL.md +++ b/skills/call-adcp-agent/SKILL.md @@ -36,7 +36,7 @@ UUID format. The key is your retry-safety guarantee — and the most common way - **Same key on retry → replay.** The server returns the SAME response — same `task_id`, same `media_buy_id`, same shape, byte-for-byte. Use this for transport-level retries (timeout, 5xx, dropped connection). - **Fresh key on retry → NEW operation.** Generating a new UUID because the previous attempt failed is how you double-book. Reuse the key until you've seen a terminal response (success, error, or non-retryable error). - **Same key, different canonical body → `IDEMPOTENCY_CONFLICT`.** Servers MUST reject. Do not silently apply the second body; do not silently replay the first. If your planner re-ran and produced different bytes, the intent changed — mint a new key. -- **Same key while first request still running → `IDEMPOTENCY_IN_FLIGHT`.** Server returns this with `error.details.retry_after` (seconds) when it doesn't want to block. Wait the hint and retry with the **same key** — minting a fresh key here turns a safe retry into a double-execution race. +- **Same key while first request still running → `IDEMPOTENCY_IN_FLIGHT`.** Server returns this with top-level `error.retry_after` (seconds) when it doesn't want to block. Wait the hint and retry with the **same key** — minting a fresh key here turns a safe retry into a double-execution race. - For async flows, the replayed response carries the **same `task_id`**, so polling continues against the same task instead of forking a duplicate. Required on: `create_media_buy`, `update_media_buy`, `sync_creatives`, `sync_audiences`, `sync_accounts`, `sync_catalogs`, `sync_event_sources`, `sync_plans`, `sync_governance`, `activate_signal`, `acquire_rights`, `log_event`, `report_usage`, `provide_performance_feedback`, `report_plan_outcome`, `create_property_list`, `update_property_list`, `delete_property_list`, `create_collection_list`, `update_collection_list`, `delete_collection_list`, `create_content_standards`, `update_content_standards`, `calibrate_content`, `si_initiate_session`, `si_send_message`. diff --git a/src/adcp/ADCP_VERSION b/src/adcp/ADCP_VERSION index 94ff29cc..c848fb9c 100644 --- a/src/adcp/ADCP_VERSION +++ b/src/adcp/ADCP_VERSION @@ -1 +1 @@ -3.1.1 +3.1.8 diff --git a/src/adcp/canonical_formats/registry.py b/src/adcp/canonical_formats/registry.py index e528e145..bcab1590 100644 --- a/src/adcp/canonical_formats/registry.py +++ b/src/adcp/canonical_formats/registry.py @@ -4,11 +4,10 @@ ``registries/v1-canonical-mapping.json``. Two match modes: * **Glob** — exact / wildcard match against a v1 ``format_id.id`` value. - As of 3.1 the registry carries zero literal entries; the AAO-published - IAB-standard formats project via catalog ``canonical:`` annotations - (resolution-order step 2). The matcher is implemented to handle - ``*`` wildcards anywhere in the pattern so future literal entries - work without further code change. + The 3.1.8 registry carries literal entries for observed legacy duration, + dimension, and VAST naming conventions. The matcher handles ``*`` + wildcards anywhere in the pattern so additional literal entries work + without further code change. * **Structural** — match against the v1 format's slot shape, asset types, and VAST/DAAST version constraints. The primary fallback for v1 wire traffic. diff --git a/src/adcp/canonical_formats/v1_to_v2.py b/src/adcp/canonical_formats/v1_to_v2.py index 6a762ca3..7296ae90 100644 --- a/src/adcp/canonical_formats/v1_to_v2.py +++ b/src/adcp/canonical_formats/v1_to_v2.py @@ -21,9 +21,9 @@ glob still fill in anything the seller didn't restate. 2. **Registry glob match** (registry step 3). Look up ``v1_format.format_id.id`` in the bundled registry's - ``format_id_glob`` entries. As of 3.1 the registry ships zero - literal globs — this step is reserved for future per-platform - entries. + ``format_id_glob`` entries. The bundled 3.1.8 registry includes + literal entries for observed legacy duration, dimension, and VAST + naming conventions. 3. **Registry structural match** (registry steps 4 + 5). Match ``v1_format.assets[*].asset_type`` + VAST/DAAST versions + dimensions against the registry's ``structural`` entries. Yields a diff --git a/src/adcp/types/__init__.py b/src/adcp/types/__init__.py index 02013375..47a74de6 100644 --- a/src/adcp/types/__init__.py +++ b/src/adcp/types/__init__.py @@ -768,6 +768,9 @@ # Creative "DeliveryCreative", "ListCreativesCreative", + "ListCreativesLegacyCreative", + "ListCreativesCanonicalCreative", + "ListCreativesCreativeItem", "SyncCreativesCreative", "BuildCreativeCreative", "CapabilitiesCreative", @@ -1321,8 +1324,11 @@ def __dir__() -> list[str]: ListContentStandardsSuccessResponse, ListCreativeFormatsRequest, ListCreativeFormatsResponse, + ListCreativesCanonicalCreative, ListCreativesCreative, + ListCreativesCreativeItem, ListCreativesField, + ListCreativesLegacyCreative, ListCreativesRequest, ListCreativesResponse, ListCreativesSort, diff --git a/src/adcp/types/_eager.py b/src/adcp/types/_eager.py index c1460139..ec916506 100644 --- a/src/adcp/types/_eager.py +++ b/src/adcp/types/_eager.py @@ -597,7 +597,10 @@ ListContentStandardsErrorResponse, ListContentStandardsResponse1, ListContentStandardsSuccessResponse, + ListCreativesCanonicalCreative, ListCreativesCreative, + ListCreativesCreativeItem, + ListCreativesLegacyCreative, ListCreativesSort, ListTasksSort, LogEventErrorResponse, @@ -1228,10 +1231,13 @@ def __init__(self, *args: object, **kwargs: object) -> None: "ListContentStandardsResponse", "ListContentStandardsResponse1", "ListContentStandardsSuccessResponse", + "ListCreativesCanonicalCreative", "ListCreativeFormatsRequest", "ListCreativeFormatsResponse", "ListCreativesCreative", + "ListCreativesCreativeItem", "ListCreativesField", + "ListCreativesLegacyCreative", "ListCreativesRequest", "ListCreativesResponse", "ListCreativesSort", diff --git a/src/adcp/types/_ergonomic.py b/src/adcp/types/_ergonomic.py index 6023882a..c0e8c391 100644 --- a/src/adcp/types/_ergonomic.py +++ b/src/adcp/types/_ergonomic.py @@ -100,7 +100,6 @@ ListCreativeFormatsResponse, ) from adcp.types.generated_poc.creative.list_creatives_response import ( - Creative, ListCreativesResponse, ) from adcp.types.generated_poc.media_buy.get_products_request import BuyingMode @@ -402,7 +401,6 @@ def _apply_coercion() -> None: # Apply coercion to ListCreativesResponse # - context: ContextObject | dict | None # - status: TaskStatus | str | None - # - creatives: Sequence[Creative] (accepts subclass instances) # - errors: list[Error] (accepts subclass instances) # - ext: ExtensionObject | dict | None _patch_field_annotation( @@ -415,14 +413,6 @@ def _apply_coercion() -> None: "status", Annotated[TaskStatus | None, BeforeValidator(coerce_to_enum(TaskStatus))], ) - _patch_field_annotation( - ListCreativesResponse, - "creatives", - Annotated[ - Sequence[Creative], - BeforeValidator(coerce_subclass_list(Creative)), - ], - ) _patch_field_annotation( ListCreativesResponse, "errors", diff --git a/src/adcp/types/_generated.py b/src/adcp/types/_generated.py index eb639305..6b969a5f 100644 --- a/src/adcp/types/_generated.py +++ b/src/adcp/types/_generated.py @@ -10,7 +10,7 @@ DO NOT EDIT MANUALLY. Generated from: https://github.com/adcontextprotocol/adcp/tree/main/schemas -Generation date: 2026-07-01 07:44:44 UTC +Generation date: 2026-07-29 02:08:16 UTC """ # ruff: noqa: E501, I001 @@ -1430,6 +1430,9 @@ from adcp.types.generated_poc.creative.list_creatives_response import ( AssignedPackage, Assignments, + Assignments1, + Creatives, + Creatives1, ListCreativesResponse, Purge, Snapshot, @@ -1913,7 +1916,7 @@ GetAdcpCapabilitiesResponse, Governance, Idempotency, - Idempotency3, + Idempotency1, Identity, KeyOrigins, KeywordTargets, @@ -1934,11 +1937,11 @@ SupportedIdentifierType, SupportedOptimizationMetric, SupportedProtocol, - SupportedTarget3, + SupportedTarget1, Surface, Targeting, Transport, - Type12, + Type9, WebhookSigning, WholesaleFeedVersioning, WholesaleFeedWebhooks, @@ -2282,6 +2285,7 @@ "AssignedPackage", "Assignment", "Assignments", + "Assignments1", "Attestation", "AttestationClaim", "AttributeDefinition", @@ -2608,6 +2612,8 @@ "CreativeStatusChangedWebhook", "CreativeVariable", "CreativeVariant", + "Creatives", + "Creatives1", "Credit", "CreditLimit", "CssAsset", @@ -2882,7 +2888,7 @@ "IconSize", "IdType", "Idempotency", - "Idempotency3", + "Idempotency1", "Identifier", "Identifiers", "Identity", @@ -3505,8 +3511,8 @@ "SupportedProtocol", "SupportedTagType", "SupportedTarget", + "SupportedTarget1", "SupportedTarget17", - "SupportedTarget3", "SupportedVersion", "SupportedViewDuration", "Surface", @@ -3621,7 +3627,7 @@ "TruncationSentinel", "TrustedMatch", "Type", - "Type12", + "Type9", "Uid", "UidType", "Unit", diff --git a/src/adcp/types/aliases.py b/src/adcp/types/aliases.py index 6da08c33..531e43a1 100644 --- a/src/adcp/types/aliases.py +++ b/src/adcp/types/aliases.py @@ -1906,8 +1906,9 @@ class UnknownGroupAsset(_BaseGroupAsset): # Each alias imports its variant directly from the source module named in the # alias prefix. Grouped by base name in __all__ below; sorted by module here. # Notable shape differences worth disambiguating: -# - creative.list_creatives_response.Creative is the full creative record; -# get_creative_delivery_response.Creative (the bare-name winner) is a lean +# - creative.list_creatives_response split its full creative record into +# legacy-format and canonical-format branches in AdCP 3.1.8; both are +# distinct from get_creative_delivery_response.Creative, which is a lean # totals view. # - core.notification_config.Authentication makes ``credentials`` optional; # the other four Authentication variants require it. @@ -1984,7 +1985,10 @@ class UnknownGroupAsset(_BaseGroupAsset): Sort as ListCreativesSort, ) from adcp.types.generated_poc.creative.list_creatives_response import ( - Creative as ListCreativesCreative, + Creatives as ListCreativesLegacyCreative, +) +from adcp.types.generated_poc.creative.list_creatives_response import ( + Creatives1 as ListCreativesCanonicalCreative, ) from adcp.types.generated_poc.creative.sync_creatives_response import ( Creative as SyncCreativesCreative, @@ -2026,6 +2030,9 @@ class UnknownGroupAsset(_BaseGroupAsset): TmpxMacro as ProviderRegistrationTmpxMacro, ) +ListCreativesCreative: TypeAlias = ListCreativesLegacyCreative +ListCreativesCreativeItem: TypeAlias = ListCreativesLegacyCreative | ListCreativesCanonicalCreative + # ============================================================================ # EXPORTS # ============================================================================ @@ -2035,6 +2042,9 @@ class UnknownGroupAsset(_BaseGroupAsset): # Creative "DeliveryCreative", "ListCreativesCreative", + "ListCreativesLegacyCreative", + "ListCreativesCanonicalCreative", + "ListCreativesCreativeItem", "SyncCreativesCreative", "BuildCreativeCreative", "CapabilitiesCreative", diff --git a/src/adcp/types/generated_poc/adagents.py b/src/adcp/types/generated_poc/adagents.py index 31f9c33a..e87027f9 100644 --- a/src/adcp/types/generated_poc/adagents.py +++ b/src/adcp/types/generated_poc/adagents.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: adagents.json -# timestamp: 2026-07-01T04:30:58+00:00 +# timestamp: 2026-07-29T02:07:25+00:00 from __future__ import annotations @@ -1270,12 +1270,12 @@ class AdcpAgentsAuthorization2( description='Inline structure variant - contains full agent authorization data', examples=[ { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'authoritative_location': 'https://cdn.example.com/adagents/v2/adagents.json', 'last_updated': '2025-01-15T10:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'properties': [ { 'property_id': 'example_site', @@ -1332,7 +1332,7 @@ class AdcpAgentsAuthorization2( 'last_updated': '2025-01-10T12:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': { 'name': 'Meta Advertising Operations', 'email': 'adops@meta.com', @@ -1401,7 +1401,7 @@ class AdcpAgentsAuthorization2( 'last_updated': '2025-01-10T15:30:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': {'name': 'Tumblr Advertising'}, 'properties': [ { @@ -1429,7 +1429,7 @@ class AdcpAgentsAuthorization2( 'last_updated': '2025-01-10T16:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': { 'name': 'Example Third-Party Sales Agent', 'email': 'sales@agent.example', @@ -1486,7 +1486,7 @@ class AdcpAgentsAuthorization2( 'last_updated': '2025-01-10T17:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': { 'name': 'Premium News Publisher', 'email': 'adops@news.example.com', @@ -1544,7 +1544,7 @@ class AdcpAgentsAuthorization2( 'last_updated': '2025-01-10T18:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': { 'name': 'Polk Automotive Data', 'email': 'partnerships@polk.com', @@ -1629,12 +1629,12 @@ class AdcpAgentsAuthorization(RootModel[AdcpAgentsAuthorization1 | AdcpAgentsAut description='Declaration of authorized agents for advertising inventory and data signals. Hosted at /.well-known/adagents.json on publisher domains (for properties) or data provider domains (for signals). Can either contain the full structure inline or reference an authoritative URL.', examples=[ { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'authoritative_location': 'https://cdn.example.com/adagents/v2/adagents.json', 'last_updated': '2025-01-15T10:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'properties': [ { 'property_id': 'example_site', @@ -1691,7 +1691,7 @@ class AdcpAgentsAuthorization(RootModel[AdcpAgentsAuthorization1 | AdcpAgentsAut 'last_updated': '2025-01-10T12:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': { 'name': 'Meta Advertising Operations', 'email': 'adops@meta.com', @@ -1760,7 +1760,7 @@ class AdcpAgentsAuthorization(RootModel[AdcpAgentsAuthorization1 | AdcpAgentsAut 'last_updated': '2025-01-10T15:30:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': {'name': 'Tumblr Advertising'}, 'properties': [ { @@ -1788,7 +1788,7 @@ class AdcpAgentsAuthorization(RootModel[AdcpAgentsAuthorization1 | AdcpAgentsAut 'last_updated': '2025-01-10T16:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': { 'name': 'Example Third-Party Sales Agent', 'email': 'sales@agent.example', @@ -1845,7 +1845,7 @@ class AdcpAgentsAuthorization(RootModel[AdcpAgentsAuthorization1 | AdcpAgentsAut 'last_updated': '2025-01-10T17:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': { 'name': 'Premium News Publisher', 'email': 'adops@news.example.com', @@ -1903,7 +1903,7 @@ class AdcpAgentsAuthorization(RootModel[AdcpAgentsAuthorization1 | AdcpAgentsAut 'last_updated': '2025-01-10T18:00:00Z', }, { - '$schema': '/schemas/3.1.1/adagents.json', + '$schema': '/schemas/3.1.8/adagents.json', 'contact': { 'name': 'Polk Automotive Data', 'email': 'partnerships@polk.com', diff --git a/src/adcp/types/generated_poc/brand/__init__.py b/src/adcp/types/generated_poc/brand/__init__.py index 9c9917d8..9832926b 100644 --- a/src/adcp/types/generated_poc/brand/__init__.py +++ b/src/adcp/types/generated_poc/brand/__init__.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: brand.json -# timestamp: 2026-07-01T04:30:58+00:00 +# timestamp: 2026-07-29T02:07:25+00:00 from __future__ import annotations @@ -2021,23 +2021,23 @@ class BrandDiscovery( description='Brand identity and discovery file. Hosted at /.well-known/brand.json on house domains. Contains the full brand portfolio with identity, creative assets, and digital properties. Brands are identified by house + brand_id (like properties are identified by publisher + property_id). Supports variants: house portfolio (full brand data), brand agent (agent provides brand info via MCP), house redirect (pointer to house domain), or authoritative location redirect.', examples=[ { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'authoritative_location': 'https://adcontextprotocol.org/brand/abc123/brand.json', }, { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'house': 'nikeinc.com', 'note': 'Redirect to house domain for full brand portfolio', }, { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'version': '1.0', 'agents': [ {'type': 'brand', 'url': 'https://agent.acme.com/mcp', 'id': 'acme_brand'} ], }, { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'version': '1.0', 'house': { 'domain': 'pg.com', @@ -2273,7 +2273,7 @@ class BrandDiscovery( 'last_updated': '2026-01-15T10:00:00Z', }, { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'version': '1.0', 'house': { 'domain': 'nikeinc.com', @@ -2377,7 +2377,7 @@ class BrandDiscovery( 'last_updated': '2026-01-15T10:00:00Z', }, { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'version': '1.0', 'house': { 'domain': 'mediavine.com', @@ -2420,7 +2420,7 @@ class BrandDiscovery( 'last_updated': '2026-01-15T10:00:00Z', }, { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'version': '1.0', 'house': { 'domain': 'nikeinc.com', @@ -2444,7 +2444,7 @@ class BrandDiscovery( 'last_updated': '2026-01-15T10:00:00Z', }, { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'version': '1.0', 'house': {'domain': 'wpp.com', 'name': 'WPP plc'}, 'brand_refs': [ @@ -2463,7 +2463,7 @@ class BrandDiscovery( 'last_updated': '2026-01-15T10:00:00Z', }, { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'version': '1.0', 'id': 'converse', 'names': [{'en_US': 'Converse'}], @@ -2474,7 +2474,7 @@ class BrandDiscovery( 'last_updated': '2026-01-15T10:00:00Z', }, { - '$schema': '/schemas/3.1.1/brand.json', + '$schema': '/schemas/3.1.8/brand.json', 'version': '1.0', 'id': 'patagonia', 'names': [{'en_US': 'Patagonia'}], diff --git a/src/adcp/types/generated_poc/bundled/protocol/get_adcp_capabilities_response.py b/src/adcp/types/generated_poc/bundled/protocol/get_adcp_capabilities_response.py index 1bbd9bb7..0d9a620f 100644 --- a/src/adcp/types/generated_poc/bundled/protocol/get_adcp_capabilities_response.py +++ b/src/adcp/types/generated_poc/bundled/protocol/get_adcp_capabilities_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: bundled/protocol/get_adcp_capabilities_response.json -# timestamp: 2026-07-01T07:44:23+00:00 +# timestamp: 2026-07-29T02:07:25+00:00 from __future__ import annotations @@ -237,7 +237,7 @@ class Idempotency(AdCPBaseModel): in_flight_max_seconds: Annotated[ int | None, Field( - description="Maximum lifetime in seconds of an in-flight idempotency row before the seller releases it per L1/security.mdx rule 9 (treat the in-flight attempt as failed if the handler does not complete within this bound). Buyer SDKs use this value to compute a retry budget when they see `IDEMPOTENCY_IN_FLIGHT` — cap individual retry waits at this value rather than the much-wider `replay_ttl_seconds` ceiling. Optional in 3.1 (additive declaration); SDKs that don't see the field fall back to rule 9's order-of-magnitude SHOULD heuristic. Required when `supported: true` in 4.0. MUST be no greater than `replay_ttl_seconds` (a bound larger than the replay window is vacuous — any retry past the TTL hits IDEMPOTENCY_EXPIRED regardless of in-flight state); validators MUST enforce this cross-field constraint at the test layer since JSON Schema cannot express field-relative bounds. A buyer that observes `error.details.retry_after` exceeding this value MAY treat that as a seller bug — the in-flight row cannot legitimately outlive the bound the seller declared.", + description="Maximum lifetime in seconds of an in-flight idempotency row before the seller releases it per L1/security.mdx rule 9 (treat the in-flight attempt as failed if the handler does not complete within this bound). Buyer SDKs use this value to compute a retry budget when they see `IDEMPOTENCY_IN_FLIGHT` — cap individual retry waits at this value rather than the much-wider `replay_ttl_seconds` ceiling. Optional in 3.1 (additive declaration); SDKs that don't see the field fall back to rule 9's order-of-magnitude SHOULD heuristic. Required when `supported: true` in 4.0. MUST be no greater than `replay_ttl_seconds` (a bound larger than the replay window is vacuous — any retry past the TTL hits IDEMPOTENCY_EXPIRED regardless of in-flight state); validators MUST enforce this cross-field constraint at the test layer since JSON Schema cannot express field-relative bounds. A buyer that observes top-level `error.retry_after` exceeding this value MAY treat that as a seller bug — the in-flight row cannot legitimately outlive the bound the seller declared.", ge=1, le=604800, ), @@ -250,7 +250,7 @@ class Idempotency(AdCPBaseModel): ] = False -class Idempotency1(AdCPBaseModel): +class Idempotency3(AdCPBaseModel): supported: Annotated[ Literal[False], Field(description='Discriminator. False means the seller does not deduplicate retries.'), @@ -288,7 +288,7 @@ class Adcp(AdCPBaseModel): ), ] = None idempotency: Annotated[ - Idempotency | Idempotency1, + Idempotency | Idempotency3, Field( description='Idempotency semantics for mutating requests. Sellers MUST declare whether they honor idempotency_key replay protection so buyers can reason about safe retry behavior. Modeled as a discriminated union on the supported boolean so that code generators produce two named types (IdempotencySupported, IdempotencyUnsupported) with the replay_ttl_seconds invariant enforced at the type level — draft-07 if/then would be dropped by most generators (openapi-typescript, zod-to-json-schema, datamodel-code-generator pre-0.25, quicktype). Clients MUST NOT assume a default — a seller without this declaration is non-compliant and should be treated as unsafe for retry-sensitive operations.' ), @@ -608,7 +608,7 @@ class VendorMetricOptimization(AdCPBaseModel): ] = None -class SupportedTarget1(StrEnum): +class SupportedTarget3(StrEnum): cost_per = 'cost_per' per_ad_spend = 'per_ad_spend' maximize_value = 'maximize_value' @@ -701,7 +701,7 @@ class DiscoveryMode(StrEnum): wholesale = 'wholesale' -class Features1(AdCPBaseModel): +class Features2(AdCPBaseModel): catalog_signals: Annotated[ bool | None, Field( @@ -727,7 +727,7 @@ class Signals(AdCPBaseModel): ), ] = [DiscoveryMode.brief] features: Annotated[ - Features1 | None, Field(description='Optional signals features supported') + Features2 | None, Field(description='Optional signals features supported') ] = None @@ -825,7 +825,7 @@ class Governance(AdCPBaseModel): ] = None -class Type9(StrEnum): +class Type12(StrEnum): mcp = 'mcp' a2a = 'a2a' @@ -834,7 +834,7 @@ class Transport(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - type: Annotated[Type9, Field(description='Protocol transport type')] + type: Annotated[Type12, Field(description='Protocol transport type')] url: Annotated[AnyUrl, Field(description='Agent endpoint URL for this transport')] @@ -847,7 +847,7 @@ class Endpoint(AdCPBaseModel): ), ] preferred: Annotated[ - Type9 | None, Field(description='Preferred transport when host supports multiple') + Type12 | None, Field(description='Preferred transport when host supports multiple') ] = None @@ -1359,7 +1359,7 @@ class RequiredConnection1(RequiredConnection): pass -class MraidVersion1(StrEnum): +class MraidVersion2(StrEnum): field_2_0 = '2.0' field_3_0 = '3.0' @@ -1442,7 +1442,7 @@ class RequiredConnection5(RequiredConnection): pass -class VastVersion1(StrEnum): +class VastVersion2(StrEnum): field_2_0 = '2.0' field_3_0 = '3.0' field_4_0 = '4.0' @@ -2007,7 +2007,7 @@ class Error(AdCPBaseModel): ] = None -class EventType1(StrEnum): +class EventType2(StrEnum): product_created = 'product.created' product_updated = 'product.updated' product_priced = 'product.priced' @@ -2030,7 +2030,7 @@ class WholesaleFeedWebhooks(AdCPBaseModel): ), ] event_types: Annotated[ - list[EventType1] | None, + list[EventType2] | None, Field( description='Wholesale feed webhook event types this agent can emit. Sales agents emit product.* events. Signals agents emit signal.* events. Agents that are both can emit both event families. Agents listing product.* event types MUST declare and support get_products with media_buy.buying_modes including wholesale. Agents listing signal.* event types MUST declare and support get_signals with signals.discovery_modes including wholesale. wholesale_feed.bulk_change tells consumers to repair by re-reading the affected wholesale feed via get_products and/or get_signals; agents listing it MUST have at least one of those wholesale repair paths and MUST only emit bulk-change payloads for affected_entity_type values backed by a declared repair path.', min_length=1, @@ -2310,7 +2310,7 @@ class ConversionTracking(AdCPBaseModel): ), ] = None supported_targets: Annotated[ - list[SupportedTarget1] | None, + list[SupportedTarget3] | None, Field( description='Event-goal target kinds this seller can compute against. Buyers should only submit event-kind optimization goals whose target.kind is listed here — sellers MUST reject goals with unlisted target kinds. When omitted, only target-less event goals (maximize conversion count within budget) are guaranteed; sellers MAY accept specific target kinds but buyers should not rely on it. Named to parallel `metric_optimization.supported_targets` at the product level — same concept (which target kinds are supported), one at seller-capability granularity and one at product granularity.', min_length=1, @@ -2989,7 +2989,7 @@ class Params2(AdCPBaseModel): bool | None, Field(description='Whether MRAID compatibility is required (mobile in-app).') ] = None mraid_version: Annotated[ - MraidVersion1 | None, + MraidVersion2 | None, Field(description='Required MRAID version when mraid_required is true.'), ] = None om_sdk_required: Annotated[ @@ -3879,7 +3879,7 @@ class Params6(AdCPBaseModel): aspect_ratio: Annotated[ str | None, Field(pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$') ] = None - vast_version: Annotated[VastVersion1 | None, Field(description='Required VAST version.')] = None + vast_version: Annotated[VastVersion2 | None, Field(description='Required VAST version.')] = None vpaid_enabled: Annotated[ bool | None, Field( diff --git a/src/adcp/types/generated_poc/core/delivery_metrics.py b/src/adcp/types/generated_poc/core/delivery_metrics.py index 03cf77f6..a736f2db 100644 --- a/src/adcp/types/generated_poc/core/delivery_metrics.py +++ b/src/adcp/types/generated_poc/core/delivery_metrics.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/delivery_metrics.json -# timestamp: 2026-06-01T00:32:59+00:00 +# timestamp: 2026-07-29T02:07:25+00:00 from __future__ import annotations @@ -205,7 +205,11 @@ class DeliveryMetrics(AdCPBaseModel): ] = None completion_rate: Annotated[ float | None, - Field(description='Completion rate (completed_views/impressions)', ge=0.0, le=1.0), + Field( + description='Completion rate (completed_views/impressions). Null indicates the metric is not applicable to this package/buy (e.g. completion rate on a non-video buy).', + ge=0.0, + le=1.0, + ), ] = None conversions: Annotated[ float | None, @@ -320,7 +324,10 @@ class DeliveryMetrics(AdCPBaseModel): ), ] = None quartile_data: Annotated[ - QuartileData | None, Field(description='Audio/video quartile completion data') + QuartileData | None, + Field( + description='Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).' + ), ] = None dooh_metrics: Annotated[ DoohMetrics | None, diff --git a/src/adcp/types/generated_poc/creative/list_creatives_response.py b/src/adcp/types/generated_poc/creative/list_creatives_response.py index f3511194..41d2c09a 100644 --- a/src/adcp/types/generated_poc/creative/list_creatives_response.py +++ b/src/adcp/types/generated_poc/creative/list_creatives_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/list_creatives_response.json -# timestamp: 2026-06-12T11:05:47+00:00 +# timestamp: 2026-07-29T02:07:25+00:00 from __future__ import annotations @@ -8,13 +8,15 @@ from typing import Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import AwareDatetime, ConfigDict, Field, RootModel, StringConstraints +from pydantic import AwareDatetime, ConfigDict, Field, RootModel, StringConstraints, model_validator from ..core import account as account_1 +from ..core import canonical_format_kind from ..core import context as context_1 from ..core import creative_item, creative_variable, error from ..core import ext as ext_1 from ..core import format_id as format_id_1 +from ..core import format_option_ref as format_option_ref_1 from ..core import pagination_response, vendor_pricing_option, webhook_activity_record from ..core.assets import asset_union from ..core.protocol_envelope import ProtocolEnvelope @@ -96,6 +98,10 @@ class Purge(AdCPBaseModel): ] +class Assignments1(Assignments): + pass + + class StatusSummary(AdCPBaseModel): model_config = ConfigDict( extra='allow', @@ -139,7 +145,7 @@ class Assets(RootModel[list[asset_union.AssetVariant]]): root: Annotated[list[asset_union.AssetVariant], Field(min_length=1)] -class Creative(AdCPBaseModel): +class Creatives(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) @@ -150,8 +156,22 @@ class Creative(AdCPBaseModel): name: Annotated[str, Field(description='Human-readable creative name')] format_id: Annotated[ format_id_1.FormatReferenceStructuredObject, - Field(description='Format identifier specifying which format this creative conforms to'), + Field( + description='Legacy named-format path. Structured format identifier specifying which legacy format this creative conforms to. Mutually exclusive with `format_kind`.' + ), ] + format_kind: Annotated[ + canonical_format_kind.CanonicalFormatKind | None, + Field( + description='3.1+ canonical-format path. The canonical format kind this creative targets. Mutually exclusive with `format_id`.' + ), + ] = None + format_option_ref: Annotated[ + format_option_ref_1.FormatOptionReference | None, + Field( + description='Optional 3.1+ reference to the concrete canonical format option this creative targets. Required when `format_kind` alone is ambiguous in the enclosing product context.' + ), + ] = None status: Annotated[ creative_status.CreativeStatus, Field(description='Current approval status of the creative') ] @@ -223,6 +243,118 @@ class Creative(AdCPBaseModel): ] = None + @model_validator(mode='after') + def _reject_canonical_format_ref(self) -> Creatives: + if self.format_kind is not None: + raise ValueError('format_id and format_kind are mutually exclusive') + return self + + +class Creatives1(AdCPBaseModel): + model_config = ConfigDict( + extra='allow', + ) + creative_id: Annotated[str, Field(description='Unique identifier for the creative')] + account: Annotated[ + account_1.Account | None, Field(description='Account that owns this creative') + ] = None + name: Annotated[str, Field(description='Human-readable creative name')] + format_id: Annotated[ + format_id_1.FormatReferenceStructuredObject | None, + Field( + description='Legacy named-format path. Structured format identifier specifying which legacy format this creative conforms to. Mutually exclusive with `format_kind`.' + ), + ] = None + format_kind: Annotated[ + canonical_format_kind.CanonicalFormatKind, + Field( + description='3.1+ canonical-format path. The canonical format kind this creative targets. Mutually exclusive with `format_id`.' + ), + ] + format_option_ref: Annotated[ + format_option_ref_1.FormatOptionReference | None, + Field( + description='Optional 3.1+ reference to the concrete canonical format option this creative targets. Required when `format_kind` alone is ambiguous in the enclosing product context.' + ), + ] = None + status: Annotated[ + creative_status.CreativeStatus, Field(description='Current approval status of the creative') + ] + created_date: Annotated[AwareDatetime, Field(description='When the creative was created')] + updated_date: Annotated[AwareDatetime, Field(description='When the creative was last modified')] + assets: Annotated[ + dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets] | None, + Field( + description='Assets for this creative, keyed by asset_id. Each slot value is either a single asset object or an array of asset objects (for slots with `min`/`max > 1`). Each asset value carries an `asset_type` discriminator that selects the matching asset schema.' + ), + ] = None + tags: Annotated[ + list[str] | None, Field(description='User-defined tags for organization and searchability') + ] = None + concept_id: Annotated[ + str | None, + Field( + description='Creative concept this creative belongs to. Concepts group related creatives across sizes and formats.' + ), + ] = None + concept_name: Annotated[str | None, Field(description='Human-readable concept name')] = None + variables: Annotated[ + list[creative_variable.CreativeVariable] | None, + Field( + description='Dynamic content variables (DCO slots) for this creative. Included when include_variables=true.' + ), + ] = None + assignments: Annotated[ + Assignments1 | None, + Field(description='Current package assignments (included when include_assignments=true)'), + ] = None + snapshot: Annotated[ + Snapshot | None, + Field( + description='Lightweight delivery snapshot (included when include_snapshot=true). For detailed performance analytics, use get_creative_delivery.' + ), + ] = None + snapshot_unavailable_reason: Annotated[ + snapshot_unavailable_reason_1.SnapshotUnavailableReason | None, + Field( + description='Machine-readable reason the snapshot is omitted. Present only when include_snapshot was true and snapshot data is unavailable for this creative.' + ), + ] = None + items: Annotated[ + list[creative_item.CreativeItem] | None, + Field( + description='Items for multi-asset formats like carousels and native ads (included when include_items=true)' + ), + ] = None + pricing_options: Annotated[ + list[vendor_pricing_option.VendorPricingOption] | None, + Field( + description='Pricing options for using this creative (serving, delivery). Used by ad servers and library agents. Transformation agents expose format-level pricing on list_creative_formats instead. Present when include_pricing=true and account provided. The buyer passes the applied pricing_option_id in report_usage.', + min_length=1, + ), + ] = None + purge: Annotated[ + Purge | None, + Field( + description="Tombstone block — present only when this record is a soft-purged creative surfaced via `include_purged: true`. The record's `status` field reflects the last status before purge (frozen — buyers MUST treat the creative as gone; assignments, snapshot, and serving operations no longer apply). Tombstones surface for the seller's webhook activity retention window (30 days from `purge.at`). Hard purges (`purge_kind: hard` on the webhook) do not surface on this read — the [`creative.purged`](https://adcontextprotocol.org/schemas/v3/creative/creative-purged-webhook.json) webhook is the only signal." + ), + ] = None + webhook_activity: Annotated[ + list[webhook_activity_record.WebhookActivityRecord] | None, + Field( + description='Recent webhook fires scoped to this creative — `creative.status_changed` and `creative.purged` deliveries. Present only when the request set `include_webhook_activity: true`. Each item is a `webhook-activity-record`; the `notification_type` field discriminates between status changes and purges. The `ext_1.creative_id` slot MAY be populated on records nested inside larger reads where the parent does not already key the array; on `list_creatives` the parent creative_id is unambiguous and `ext_1.creative_id` MAY be omitted. Retention: 30 days from `completed_at` (MUST). See `snapshot-and-log.mdx § Webhook activity log pattern` for the full normative contract.', + max_length=200, + ), + ] = None + + + @model_validator(mode='after') + def _reject_legacy_format_ref(self) -> Creatives1: + if self.format_id is not None: + raise ValueError('format_id and format_kind are mutually exclusive') + return self + + class ListCreativesResponse(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict( extra='allow', @@ -232,7 +364,8 @@ class ListCreativesResponse(AdcpVersionEnvelope, ProtocolEnvelope): ] pagination: pagination_response.PaginationResponse creatives: Annotated[ - Sequence[Creative], Field(description='Array of creative assets matching the query') + Sequence[Creatives | Creatives1], + Field(description='Array of creative assets matching the query'), ] format_summary: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9_-]+$')], int] | None, diff --git a/src/adcp/types/generated_poc/creative/sync_creatives_request.py b/src/adcp/types/generated_poc/creative/sync_creatives_request.py index 56345f19..ce2bb572 100644 --- a/src/adcp/types/generated_poc/creative/sync_creatives_request.py +++ b/src/adcp/types/generated_poc/creative/sync_creatives_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/sync_creatives_request.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-07-29T02:07:25+00:00 from __future__ import annotations @@ -89,7 +89,7 @@ class SyncCreativesRequest(AdcpVersionEnvelope): dry_run: Annotated[ bool | None, Field( - description='When true, preview changes without applying them. Returns what would be created/updated/deleted.' + description="When true, rehearse this sync_creatives operation without applying it. Validates the actual trafficking request in the seller's current context, including library upsert semantics, creative IDs, assignments, account-scoped gates, and seller policies, then returns what would be created/updated/deleted. This is distinct from validate_input, which only validates manifest structure against canonical/product format targets." ), ] = False validation_mode: Annotated[ diff --git a/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py b/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py index 3b5a10f7..f1460b8e 100644 --- a/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +++ b/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/get_media_buy_delivery_response.json -# timestamp: 2026-06-04T19:44:00+00:00 +# timestamp: 2026-07-29T02:07:25+00:00 from __future__ import annotations @@ -177,7 +177,7 @@ class AggregatedTotals(AdCPBaseModel): completion_rate: Annotated[ float | None, Field( - description='Aggregate completion rate across all media buys (weighted by impressions, not a simple average of per-buy rates)', + description='Aggregate completion rate across all media buys (weighted by impressions, not a simple average of per-buy rates). Null indicates the metric is not applicable to the aggregated buys (e.g. all non-video inventory).', ge=0.0, le=1.0, ), diff --git a/src/adcp/types/generated_poc/protocol/get_adcp_capabilities_response.py b/src/adcp/types/generated_poc/protocol/get_adcp_capabilities_response.py index b0ce223f..6beff17b 100644 --- a/src/adcp/types/generated_poc/protocol/get_adcp_capabilities_response.py +++ b/src/adcp/types/generated_poc/protocol/get_adcp_capabilities_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/get_adcp_capabilities_response.json -# timestamp: 2026-07-01T07:44:23+00:00 +# timestamp: 2026-07-29T02:07:25+00:00 from __future__ import annotations @@ -70,7 +70,7 @@ class Idempotency(AdCPBaseModel): in_flight_max_seconds: Annotated[ int | None, Field( - description="Maximum lifetime in seconds of an in-flight idempotency row before the seller releases it per L1/security.mdx rule 9 (treat the in-flight attempt as failed if the handler does not complete within this bound). Buyer SDKs use this value to compute a retry budget when they see `IDEMPOTENCY_IN_FLIGHT` — cap individual retry waits at this value rather than the much-wider `replay_ttl_seconds` ceiling. Optional in 3.1 (additive declaration); SDKs that don't see the field fall back to rule 9's order-of-magnitude SHOULD heuristic. Required when `supported: true` in 4.0. MUST be no greater than `replay_ttl_seconds` (a bound larger than the replay window is vacuous — any retry past the TTL hits IDEMPOTENCY_EXPIRED regardless of in-flight state); validators MUST enforce this cross-field constraint at the test layer since JSON Schema cannot express field-relative bounds. A buyer that observes `error.details.retry_after` exceeding this value MAY treat that as a seller bug — the in-flight row cannot legitimately outlive the bound the seller declared.", + description="Maximum lifetime in seconds of an in-flight idempotency row before the seller releases it per L1/security.mdx rule 9 (treat the in-flight attempt as failed if the handler does not complete within this bound). Buyer SDKs use this value to compute a retry budget when they see `IDEMPOTENCY_IN_FLIGHT` — cap individual retry waits at this value rather than the much-wider `replay_ttl_seconds` ceiling. Optional in 3.1 (additive declaration); SDKs that don't see the field fall back to rule 9's order-of-magnitude SHOULD heuristic. Required when `supported: true` in 4.0. MUST be no greater than `replay_ttl_seconds` (a bound larger than the replay window is vacuous — any retry past the TTL hits IDEMPOTENCY_EXPIRED regardless of in-flight state); validators MUST enforce this cross-field constraint at the test layer since JSON Schema cannot express field-relative bounds. A buyer that observes top-level `error.retry_after` exceeding this value MAY treat that as a seller bug — the in-flight row cannot legitimately outlive the bound the seller declared.", ge=1, le=604800, ), @@ -83,7 +83,7 @@ class Idempotency(AdCPBaseModel): ] = False -class Idempotency3(AdCPBaseModel): +class Idempotency1(AdCPBaseModel): supported: Annotated[ Literal[False], Field(description='Discriminator. False means the seller does not deduplicate retries.'), @@ -121,7 +121,7 @@ class Adcp(AdCPBaseModel): ), ] = None idempotency: Annotated[ - Idempotency | Idempotency3, + Idempotency | Idempotency1, Field( description='Idempotency semantics for mutating requests. Sellers MUST declare whether they honor idempotency_key replay protection so buyers can reason about safe retry behavior. Modeled as a discriminated union on the supported boolean so that code generators produce two named types (IdempotencySupported, IdempotencyUnsupported) with the replay_ttl_seconds invariant enforced at the type level — draft-07 if/then would be dropped by most generators (openapi-typescript, zod-to-json-schema, datamodel-code-generator pre-0.25, quicktype). Clients MUST NOT assume a default — a seller without this declaration is non-compliant and should be treated as unsafe for retry-sensitive operations.' ), @@ -307,7 +307,7 @@ class VendorMetricOptimization(AdCPBaseModel): ] = None -class SupportedTarget3(StrEnum): +class SupportedTarget1(StrEnum): cost_per = 'cost_per' per_ad_spend = 'per_ad_spend' maximize_value = 'maximize_value' @@ -533,7 +533,7 @@ class Governance(AdCPBaseModel): ] = None -class Type12(StrEnum): +class Type9(StrEnum): mcp = 'mcp' a2a = 'a2a' @@ -542,7 +542,7 @@ class Transport(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - type: Annotated[Type12, Field(description='Protocol transport type')] + type: Annotated[Type9, Field(description='Protocol transport type')] url: Annotated[AnyUrl, Field(description='Agent endpoint URL for this transport')] @@ -555,7 +555,7 @@ class Endpoint(AdCPBaseModel): ), ] preferred: Annotated[ - Type12 | None, Field(description='Preferred transport when host supports multiple') + Type9 | None, Field(description='Preferred transport when host supports multiple') ] = None @@ -1093,7 +1093,7 @@ class ConversionTracking(AdCPBaseModel): ), ] = None supported_targets: Annotated[ - list[SupportedTarget3] | None, + list[SupportedTarget1] | None, Field( description='Event-goal target kinds this seller can compute against. Buyers should only submit event-kind optimization goals whose target.kind is listed here — sellers MUST reject goals with unlisted target kinds. When omitted, only target-less event goals (maximize conversion count within budget) are guaranteed; sellers MAY accept specific target kinds but buyers should not rely on it. Named to parallel `metric_optimization.supported_targets` at the product level — same concept (which target kinds are supported), one at seller-capability granularity and one at product granularity.', min_length=1, diff --git a/src/adcp/types/generated_poc/signals/activate_signal_request.py b/src/adcp/types/generated_poc/signals/activate_signal_request.py index 3ca46b09..5a56c272 100644 --- a/src/adcp/types/generated_poc/signals/activate_signal_request.py +++ b/src/adcp/types/generated_poc/signals/activate_signal_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: signals/activate_signal_request.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-07-29T02:07:25+00:00 from __future__ import annotations @@ -50,6 +50,15 @@ class ActivateSignalRequest(AdcpVersionEnvelope): description="The pricing option selected from the signal's pricing_options in the get_signals response. Required when the signal has pricing options. Records the buyer's pricing commitment at activation time; pass this same value in report_usage for billing verification." ), ] = None + governance_context: Annotated[ + str | None, + Field( + description='Opaque governance context returned by check_governance for this signal activation. Required when the account has a registered governance agent; signal agents MUST reject governed activations that omit a valid context.', + max_length=4096, + min_length=1, + pattern='^[\\x20-\\x7E]+$', + ), + ] = None account: Annotated[ account_ref.AccountReference | None, Field( diff --git a/tests/fixtures/public_api_snapshot.json b/tests/fixtures/public_api_snapshot.json index 18180ab2..3b4fefd9 100644 --- a/tests/fixtures/public_api_snapshot.json +++ b/tests/fixtures/public_api_snapshot.json @@ -850,8 +850,11 @@ "ListContentStandardsSuccessResponse", "ListCreativeFormatsRequest", "ListCreativeFormatsResponse", + "ListCreativesCanonicalCreative", "ListCreativesCreative", + "ListCreativesCreativeItem", "ListCreativesField", + "ListCreativesLegacyCreative", "ListCreativesRequest", "ListCreativesResponse", "ListCreativesSort", diff --git a/tests/test_canonical_formats_registry.py b/tests/test_canonical_formats_registry.py index 750c4b74..232075ad 100644 --- a/tests/test_canonical_formats_registry.py +++ b/tests/test_canonical_formats_registry.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + import pytest from adcp.canonical_formats import ( @@ -31,18 +33,47 @@ def test_loader_returns_equal_content_each_call() -> None: assert a == b -def test_initial_registry_has_seven_pure_structural_entries() -> None: - """3.1 ships 7 pure-structural fallback entries per the registry docstring. - A change to this count is a vocabulary-governance event.""" +def _canonical_value(value: Any) -> str: + return getattr(value, "value", value) + + +def _structural_patterns(registry: V1V2CanonicalFormatMappingRegistry, canonical: str) -> list[Any]: + return [ + mapping.v1_pattern.structural + for mapping in registry.mappings + if hasattr(mapping.v1_pattern, "structural") + and _canonical_value(mapping.v2.canonical) == canonical + ] + + +def _structural_pattern( + registry: V1V2CanonicalFormatMappingRegistry, + canonical: str, + *, + vast_versions: list[str] | None = None, + asset_types: list[str] | None = None, +) -> Any: + for pattern in _structural_patterns(registry, canonical): + data = pattern.model_dump(exclude_none=True) + if vast_versions is not None and data.get("vast_versions") != vast_versions: + continue + if asset_types is not None and data.get("asset_types") != asset_types: + continue + return pattern + raise AssertionError(f"No structural mapping found for canonical={canonical!r}") + + +def test_registry_has_literal_entries_plus_seven_structural_fallbacks() -> None: + """3.1.8 ships literal mappings plus 7 structural fallback entries. + A change to these counts is a vocabulary-governance event.""" registry = load_default_registry() - assert len(registry.mappings) == 7 - # Every initial entry is structural — no literal globs as of 3.1. - for mapping in registry.mappings: - # ``v1_pattern`` is the discriminated union; the structural branch - # exposes ``.structural``, the glob branch exposes ``.format_id_glob``. - assert hasattr( - mapping.v1_pattern, "structural" - ), f"3.1 baseline expected pure-structural; got {mapping.v1_pattern!r}" + structural = [m for m in registry.mappings if hasattr(m.v1_pattern, "structural")] + literal = [m for m in registry.mappings if hasattr(m.v1_pattern, "format_id_glob")] + + assert registry.version == "1.2.0" + assert len(registry.mappings) == 29 + assert len(literal) == 22 + assert len(structural) == 7 # --------------------------------------------------------------------------- @@ -80,8 +111,7 @@ def test_glob_match_treats_regex_metachars_as_literal() -> None: def test_structural_match_vast_42_against_vast_4_plus_pattern() -> None: registry = load_default_registry() - # Entry 0 in the test fixture is the VAST ≥4.0 entry. - pattern = registry.mappings[0].v1_pattern.structural + pattern = _structural_pattern(registry, "video_vast", vast_versions=[">=4.0"]) assert structural_match( asset_types=["vast"], @@ -92,7 +122,7 @@ def test_structural_match_vast_42_against_vast_4_plus_pattern() -> None: def test_structural_match_vast_30_misses_vast_4_plus_pattern() -> None: registry = load_default_registry() - pattern = registry.mappings[0].v1_pattern.structural + pattern = _structural_pattern(registry, "video_vast", vast_versions=[">=4.0"]) assert not structural_match( asset_types=["vast"], @@ -103,8 +133,7 @@ def test_structural_match_vast_30_misses_vast_4_plus_pattern() -> None: def test_structural_match_vast_30_hits_legacy_pattern() -> None: registry = load_default_registry() - # Entry 1 is the legacy VAST 3.x / 2.x entry. - pattern = registry.mappings[1].v1_pattern.structural + pattern = _structural_pattern(registry, "video_vast", vast_versions=["3.x", "2.x"]) assert structural_match( asset_types=["vast"], @@ -120,8 +149,7 @@ def test_structural_match_vast_30_hits_legacy_pattern() -> None: def test_structural_match_misses_when_asset_type_absent() -> None: registry = load_default_registry() - # Entry 3 is the zip → html5 entry. - pattern = registry.mappings[3].v1_pattern.structural + pattern = _structural_pattern(registry, "html5", asset_types=["zip"]) assert not structural_match(asset_types=["url"], pattern=pattern) assert structural_match(asset_types=["zip"], pattern=pattern) @@ -131,8 +159,7 @@ def test_structural_match_with_extra_asset_types_still_matches() -> None: """The pattern's asset_types is a *subset* requirement — adding more asset_types in the v1 format doesn't disqualify the match.""" registry = load_default_registry() - # Entry 4 is the video → video_hosted entry. - pattern = registry.mappings[4].v1_pattern.structural + pattern = _structural_pattern(registry, "video_hosted", asset_types=["video"]) assert structural_match(asset_types=["video", "url"], pattern=pattern) diff --git a/tests/test_canonical_formats_v1_to_v2.py b/tests/test_canonical_formats_v1_to_v2.py index dd3d10e6..b2440747 100644 --- a/tests/test_canonical_formats_v1_to_v2.py +++ b/tests/test_canonical_formats_v1_to_v2.py @@ -10,6 +10,8 @@ import json from pathlib import Path +import pytest + from adcp.canonical_formats import ( project_v1_catalog_to_v2, project_v1_format_to_declaration, @@ -78,6 +80,45 @@ def test_slots_override_threads_into_params() -> None: assert slots[0]["asset_group_id"] == "image_main" +# --------------------------------------------------------------------------- +# Step 2 — registry glob match (no explicit canonical) +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("format_id", "kind", "params"), + [ + ("display_300x250", CanonicalFormatKind.image, {"width": 300, "height": 250}), + ("video_30s", CanonicalFormatKind.video_hosted, {"duration_ms_exact": 30000}), + ( + "video_640x360_vast", + CanonicalFormatKind.video_vast, + {"width": 640, "height": 360}, + ), + ("audio_15s", CanonicalFormatKind.audio_hosted, {"duration_ms_exact": 15000}), + ], +) +def test_registry_literal_mappings_project_canonical_params( + format_id: str, + kind: CanonicalFormatKind, + params: dict[str, object], +) -> None: + result = project_v1_format_to_declaration( + { + "format_id": { + "agent_url": "https://creative.adcontextprotocol.org", + "id": format_id, + } + } + ) + + assert result.declaration is not None + assert result.declaration.format_kind is kind + assert result.declaration.params == params + assert result.declaration.v1_format_ref[0].id == format_id + assert result.advisories == [] + + # --------------------------------------------------------------------------- # Step 3 — registry structural match (no explicit canonical) # --------------------------------------------------------------------------- @@ -182,10 +223,10 @@ def test_step1_threads_registry_params_when_seller_annotates_kind_only() -> None declared ``vast_version`` / dimensions / etc. That's the code-reviewer's MUST-FIX #1. """ - # No registry glob matches this id (3.1 has zero literal globs) so - # the test exercises the structural intent without depending on a - # specific glob entry: assert seller-asserted slots win, registry - # params (when found) thread in alongside. + # This id intentionally misses the literal-glob registry entries, so + # the test exercises the partial-annotation path without depending on + # a specific literal entry: seller-asserted slots win, while registry + # params thread in only when a literal hit exists. v1 = { "format_id": {"agent_url": "https://creative.adcontextprotocol.org", "id": "x"}, "canonical": {"kind": "video_vast"}, # partial — no slots_override, no asset_source diff --git a/tests/test_collision_aliases.py b/tests/test_collision_aliases.py index 641602bf..3695f596 100644 --- a/tests/test_collision_aliases.py +++ b/tests/test_collision_aliases.py @@ -16,12 +16,16 @@ import importlib import pytest +from pydantic import BaseModel, TypeAdapter, ValidationError # (alias name, source module under generated_poc, base class name in that module) COLLISION_ALIASES: list[tuple[str, str, str]] = [ - # Creative — 5 variants + # Creative — ListCreativesCreative deliberately remains the legacy + # class-shaped alias for subclass compatibility. The 3.1.8 split also + # exposes ListCreativesCanonicalCreative and ListCreativesCreativeItem. ("DeliveryCreative", "creative.get_creative_delivery_response", "Creative"), - ("ListCreativesCreative", "creative.list_creatives_response", "Creative"), + ("ListCreativesCreative", "creative.list_creatives_response", "Creatives"), + ("ListCreativesCanonicalCreative", "creative.list_creatives_response", "Creatives1"), ("SyncCreativesCreative", "creative.sync_creatives_response", "Creative"), ("BuildCreativeCreative", "media_buy.build_creative_response", "Creative"), ("CapabilitiesCreative", "protocol.get_adcp_capabilities_response", "Creative"), @@ -185,22 +189,109 @@ def test_notification_config_authentication_makes_credentials_optional() -> None def test_listing_creative_is_the_rich_shape() -> None: - """list_creatives_response.Creative is the rich record adopters usually - want, distinct from the lean delivery-totals Creative that wins the bare - name. Asserting a couple of marker fields guards the mapping by shape.""" + """ListCreativesCreative remains the subclassable rich legacy record. + + AdCP 3.1.8 split list_creatives response rows into legacy and canonical + branches. The old public alias stays class-shaped because adopters use it + as a base class; the new item alias exposes the response union explicitly. + """ + import adcp.types as types_module from adcp.types import aliases as a + from adcp.types.generated_poc.creative.list_creatives_response import ( + Creatives, + Creatives1, + ) delivery_fields = set(a.DeliveryCreative.model_fields) - listing_fields = set(a.ListCreativesCreative.model_fields) # Delivery variant is the lean totals view. assert "totals" in delivery_fields assert "variant_count" in delivery_fields - # Listing variant carries the full creative record. - assert "status" in listing_fields - assert "assets" in listing_fields - assert "assignments" in listing_fields - assert listing_fields != delivery_fields + assert a.ListCreativesCreative is Creatives + assert a.ListCreativesLegacyCreative is Creatives + assert a.ListCreativesCanonicalCreative is Creatives1 + assert a.ListCreativesCreativeItem == (Creatives | Creatives1) + + for public_name in ( + "ListCreativesCreative", + "ListCreativesLegacyCreative", + "ListCreativesCanonicalCreative", + "ListCreativesCreativeItem", + ): + assert getattr(types_module, public_name) is getattr(a, public_name) + assert public_name in types_module.__all__ + assert public_name in a.__all__ + + class InternalCreative(a.ListCreativesCreative): + internal_id: str | None = None + + assert issubclass(InternalCreative, BaseModel) + assert "status" in InternalCreative.model_fields + + # Both listing branches carry the full creative record. + for listing_cls in (Creatives, Creatives1): + listing_fields = set(listing_cls.model_fields) + assert "status" in listing_fields + assert "assets" in listing_fields + assert "assignments" in listing_fields + assert listing_fields != delivery_fields + + +def _minimal_list_creative(**overrides: object) -> dict[str, object]: + payload: dict[str, object] = { + "creative_id": "cr_123", + "name": "Medium Rectangle", + "format_id": { + "agent_url": "https://creative.example.com", + "id": "display_300x250", + }, + "status": "approved", + "created_date": "2026-01-10T14:00:00Z", + "updated_date": "2026-01-10T14:00:00Z", + } + payload.update(overrides) + return payload + + +def _minimal_list_creatives_response(creative: dict[str, object]) -> dict[str, object]: + return { + "status": "completed", + "query_summary": {"total_matching": 1, "returned": 1}, + "pagination": {"has_more": False, "total_count": 1}, + "creatives": [creative], + } + + +def test_list_creatives_response_enforces_format_reference_xor() -> None: + """Generated response rows must keep the schema oneOf exactness.""" + from adcp.types import aliases as a + from adcp.types.generated_poc.creative.list_creatives_response import ListCreativesResponse + + legacy = _minimal_list_creative() + canonical = _minimal_list_creative(format_id=None, format_kind="image") + canonical.pop("format_id") + + assert isinstance( + ListCreativesResponse.model_validate(_minimal_list_creatives_response(legacy)).creatives[0], + a.ListCreativesLegacyCreative, + ) + assert isinstance( + ListCreativesResponse.model_validate(_minimal_list_creatives_response(canonical)).creatives[ + 0 + ], + a.ListCreativesCanonicalCreative, + ) + assert TypeAdapter(a.ListCreativesCreativeItem).validate_python(legacy) + assert TypeAdapter(a.ListCreativesCreativeItem).validate_python(canonical) + + both = _minimal_list_creative(format_kind="image") + neither = _minimal_list_creative(format_id=None) + neither.pop("format_id") + + with pytest.raises(ValidationError): + ListCreativesResponse.model_validate(_minimal_list_creatives_response(both)) + with pytest.raises(ValidationError): + ListCreativesResponse.model_validate(_minimal_list_creatives_response(neither)) def test_tmpx_macro_aliases_cover_distinct_shapes() -> None: diff --git a/tests/test_registry_sync.py b/tests/test_registry_sync.py index ab1e4a1d..fe066eb9 100644 --- a/tests/test_registry_sync.py +++ b/tests/test_registry_sync.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import json import tempfile from pathlib import Path @@ -42,6 +43,11 @@ def _make_page( return FeedPage(events=events or [], cursor=cursor, has_more=has_more) +async def _wait_for_call_count(get_count, expected: int) -> None: + while get_count() < expected: + await asyncio.sleep(0.01) + + class TestFileCursorStore: @pytest.mark.asyncio async def test_load_returns_none_when_no_file(self): @@ -125,9 +131,7 @@ async def test_saves_cursor_after_poll(self): mock_store.load = AsyncMock(return_value=None) mock_store.save = AsyncMock() - sync = RegistrySync( - mock_client, auth_token="sk_test", cursor_store=mock_store - ) + sync = RegistrySync(mock_client, auth_token="sk_test", cursor_store=mock_store) await sync.poll_once() mock_store.save.assert_called_once_with("new-cursor") @@ -136,17 +140,13 @@ async def test_saves_cursor_after_poll(self): @pytest.mark.asyncio async def test_loads_cursor_on_first_poll(self): mock_client = MagicMock() - mock_client.get_feed = AsyncMock( - return_value=_make_page([], cursor=None) - ) + mock_client.get_feed = AsyncMock(return_value=_make_page([], cursor=None)) mock_store = MagicMock() mock_store.load = AsyncMock(return_value="saved-cursor") mock_store.save = AsyncMock() - sync = RegistrySync( - mock_client, auth_token="sk_test", cursor_store=mock_store - ) + sync = RegistrySync(mock_client, auth_token="sk_test", cursor_store=mock_store) await sync.poll_once() mock_store.load.assert_called_once() @@ -164,9 +164,7 @@ async def test_resets_cursor_on_410(self): mock_store.load = AsyncMock(return_value="old-cursor") mock_store.save = AsyncMock() - sync = RegistrySync( - mock_client, auth_token="sk_test", cursor_store=mock_store - ) + sync = RegistrySync(mock_client, auth_token="sk_test", cursor_store=mock_store) result = await sync.poll_once() assert result == [] @@ -182,9 +180,7 @@ async def test_propagates_non_410_errors(self): mock_store = MagicMock() mock_store.load = AsyncMock(return_value=None) - sync = RegistrySync( - mock_client, auth_token="bad", cursor_store=mock_store - ) + sync = RegistrySync(mock_client, auth_token="bad", cursor_store=mock_store) with pytest.raises(RegistryError) as exc_info: await sync.poll_once() assert exc_info.value.status_code == 401 @@ -219,8 +215,7 @@ async def test_passes_types_filter(self): mock_store.save = AsyncMock() sync = RegistrySync( - mock_client, auth_token="sk_test", - cursor_store=mock_store, types="property.*,agent.*" + mock_client, auth_token="sk_test", cursor_store=mock_store, types="property.*,agent.*" ) await sync.poll_once() @@ -246,9 +241,7 @@ async def fake_get_feed(**kwargs): mock_store.load = AsyncMock(return_value=None) mock_store.save = AsyncMock() - sync = RegistrySync( - mock_client, auth_token="sk", cursor_store=mock_store - ) + sync = RegistrySync(mock_client, auth_token="sk", cursor_store=mock_store) await sync.poll_once() await sync.poll_once() @@ -258,17 +251,13 @@ async def fake_get_feed(**kwargs): @pytest.mark.asyncio async def test_empty_page_does_not_update_cursor(self): mock_client = MagicMock() - mock_client.get_feed = AsyncMock( - return_value=_make_page([], cursor=None) - ) + mock_client.get_feed = AsyncMock(return_value=_make_page([], cursor=None)) mock_store = MagicMock() mock_store.load = AsyncMock(return_value="existing-cursor") mock_store.save = AsyncMock() - sync = RegistrySync( - mock_client, auth_token="sk", cursor_store=mock_store - ) + sync = RegistrySync(mock_client, auth_token="sk", cursor_store=mock_store) result = await sync.poll_once() assert result == [] @@ -277,9 +266,7 @@ async def test_empty_page_does_not_update_cursor(self): @pytest.mark.asyncio async def test_batch_size_capped_at_10000(self): - sync = RegistrySync( - MagicMock(), auth_token="sk", batch_size=99999 - ) + sync = RegistrySync(MagicMock(), auth_token="sk", batch_size=99999) assert sync._batch_size == 10000 @pytest.mark.asyncio @@ -292,8 +279,10 @@ async def test_batch_size_passed_as_limit(self): mock_store.save = AsyncMock() sync = RegistrySync( - mock_client, auth_token="sk", - cursor_store=mock_store, batch_size=500, + mock_client, + auth_token="sk", + cursor_store=mock_store, + batch_size=500, ) await sync.poll_once() @@ -308,9 +297,7 @@ async def test_exact_event_type_match(self): _make_event("e2", "agent.created"), ] mock_client = MagicMock() - mock_client.get_feed = AsyncMock( - return_value=_make_page(events, cursor="e2") - ) + mock_client.get_feed = AsyncMock(return_value=_make_page(events, cursor="e2")) received: list[str] = [] @@ -327,9 +314,7 @@ async def handler(event: FeedEvent) -> None: async def test_multiple_handlers_same_pattern(self): events = [_make_event("e1", "property.created")] mock_client = MagicMock() - mock_client.get_feed = AsyncMock( - return_value=_make_page(events, cursor="e1") - ) + mock_client.get_feed = AsyncMock(return_value=_make_page(events, cursor="e1")) counts = [0, 0] @@ -350,9 +335,7 @@ async def handler_b(event: FeedEvent) -> None: async def test_no_match_pattern_not_called(self): events = [_make_event("e1", "property.created")] mock_client = MagicMock() - mock_client.get_feed = AsyncMock( - return_value=_make_page(events, cursor="e1") - ) + mock_client.get_feed = AsyncMock(return_value=_make_page(events, cursor="e1")) received: list[str] = [] @@ -385,11 +368,12 @@ async def fake_get_feed(**kwargs): mock_store.save = AsyncMock() sync = RegistrySync( - mock_client, auth_token="sk_test", - cursor_store=mock_store, poll_interval=0.01, + mock_client, + auth_token="sk_test", + cursor_store=mock_store, + poll_interval=0.01, ) - import asyncio task = asyncio.create_task(sync.start()) await asyncio.sleep(0.05) await sync.stop() @@ -421,15 +405,18 @@ async def fake_get_feed(**kwargs): mock_store.save = AsyncMock() sync = RegistrySync( - mock_client, auth_token="sk", - cursor_store=mock_store, poll_interval=0.01, + mock_client, + auth_token="sk", + cursor_store=mock_store, + poll_interval=0.01, ) - import asyncio task = asyncio.create_task(sync.start()) - await asyncio.sleep(0.05) - await sync.stop() - await task + try: + await asyncio.wait_for(_wait_for_call_count(lambda: call_count, 2), timeout=1.0) + finally: + await sync.stop() + await task # Should have recovered and polled again after the error assert call_count >= 2