diff --git a/.changeset/available-installment-statuses-canonical.md b/.changeset/available-installment-statuses-canonical.md new file mode 100644 index 00000000000..7c676f882bb --- /dev/null +++ b/.changeset/available-installment-statuses-canonical.md @@ -0,0 +1,9 @@ +--- +"adcontextprotocol": minor +--- + +Move the experimental `available_installment_statuses` (`media_buy.installment_lifecycle`, #8116) from `core/reporting-capabilities.json` to `core/canonical-reporting-capabilities.json` (adcp#8501). The definition, `x-status: experimental` and `x-added-in: 3.3.0` are unchanged. + +`core/reporting-capabilities.json` is used only by the deprecated `get_products` product, and 3.3 adds no field that exists only on a deprecated task. The canonical product's reporting capabilities, returned by `list_products`, `request_proposals` and `refine_proposals`, already carry `supports_installment_breakdown`, and the opened schema (DR-0036) now takes the status list beside it. + +**This changes 3.3.0-beta.0 through beta.5.** The betas are immutable. A seller built from one declares `available_installment_statuses` on the canonical product's `reporting_capabilities` and stops sending it on a `get_products` product. The installment lifecycle docs and experimental status page say where it lives. diff --git a/docs/media-buy/media-buys/optimization-reporting.mdx b/docs/media-buy/media-buys/optimization-reporting.mdx index b15fe743f9b..c7c288afffb 100644 --- a/docs/media-buy/media-buys/optimization-reporting.mdx +++ b/docs/media-buy/media-buys/optimization-reporting.mdx @@ -1701,7 +1701,7 @@ A `by_installment` row can follow one installment from plan to proof. `installme - **Publication.** `publication` has a required `published_at` and a `posts[]` array with at least one entry. Each entry is a [`published_post`](https://adcontextprotocol.org/schemas/v3/core/assets/published-post-asset.json) reference, one per place the installment was published, so cross-posts are separate entries and a buyer can pass an entry to [`sync_creatives`](/docs/creative/task-reference/sync_creatives) for paid amplification. Optional `evidence` is a capture or archive of the live post. Optional `verified_by` names an independent verifier; when it is absent the record is seller-reported. `publication` MUST be absent on `scheduled`, `tentative`, `postponed`, `cancelled`, and `live` rows. Sellers SHOULD populate it on each `by_installment` row for a digital installment they report as `published`. - **Not a billing event.** `publication` is seller-reported evidence. Billing follows the accepted terms, and invoices surface through [`get_account_financials`](/docs/accounts/tasks/get_account_financials). - **No notification.** There is no new notification type. A buyer reads publication from the next reporting fire or from `get_media_buy_delivery`, and a change-feed seller records a `delivery_report` change. -- **Not reported versus not delivered.** `reporting_capabilities.available_installment_statuses` lists the statuses a seller reports and is meaningful only with `supports_installment_breakdown: true`. A status that is not listed is not reported, so the absence of such rows proves nothing. `published`, `aired`, and `live` are delivery statuses and need not be listed. A seller that has committed to `publication` and cannot yet produce it reports `{ "scope": "standard", "metric_id": "publication" }` in `by_package[].missing_metrics`. +- **Not reported versus not delivered.** `available_installment_statuses` on a canonical product's `reporting_capabilities` (`core/canonical-reporting-capabilities.json`, returned by `list_products`, `request_proposals`, and `refine_proposals`) lists the statuses a seller reports and is meaningful only with `supports_installment_breakdown: true`. The deprecated `get_products` product does not carry it ([adcp#8501](https://github.com/adcontextprotocol/adcp/issues/8501)). A status that is not listed is not reported, so the absence of such rows proves nothing. `published`, `aired`, and `live` are delivery statuses and need not be listed. A seller that has committed to `publication` and cannot yet produce it reports `{ "scope": "standard", "metric_id": "publication" }` in `by_package[].missing_metrics`. - **Compatibility.** Rows with no `installment_status` stay valid. Sellers MUST NOT send rows with `installment_status` `scheduled`, `tentative`, `postponed`, or `cancelled` to callers pinned below AdCP 3.3, because AdCP 3.2 validators require `impressions` on every installment row. A caller with no declared pin is treated as pinned below 3.3, and reporting webhooks follow the pin of the registration that receives them. Sellers also MUST NOT send `available_installment_statuses` to those callers, so their view of the capability is unchanged. 3.2 validators ignore `publication` and `installment_status` on delivery rows. A `published` row that reports views and omits `impressions`, as in the example below, is subject to the general rule for optional `impressions`: a seller MUST NOT omit it toward a caller pinned below AdCP 3.3 and instead omits the row. - **Registry.** A seller that returns `installment_status`, `publication`, or `available_installment_statuses` MUST list `media_buy.installment_lifecycle` in `experimental_features` on [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities). These fields apply to `by_installment` only, not to `by_installment_property`. @@ -1782,11 +1782,11 @@ A `by_installment` row can follow one installment from plan to proof. `installme } ``` -A seller that reports these rows declares them in the product's reporting capabilities: +A seller that reports these rows declares them in the canonical product's reporting capabilities: ```json { - "$schema": "https://adcontextprotocol.org/schemas/v3/core/reporting-capabilities.json", + "$schema": "https://adcontextprotocol.org/schemas/v3/core/canonical-reporting-capabilities.json", "available_reporting_frequencies": ["daily"], "expected_delay_minutes": 240, "timezone": "America/New_York", diff --git a/docs/media-buy/task-reference/get_media_buy_delivery.mdx b/docs/media-buy/task-reference/get_media_buy_delivery.mdx index 656028b734f..005c9d6f340 100644 --- a/docs/media-buy/task-reference/get_media_buy_delivery.mdx +++ b/docs/media-buy/task-reference/get_media_buy_delivery.mdx @@ -1046,7 +1046,7 @@ The optional `creative_id` identifies the creative that aired, matching the pack Delivery metrics on a spot row are optional. Omission means the metric is not yet available in the package's current `measurement_window`, or the product did not declare it in `supports_spot_breakdown.available_metrics`. A later window may return the same `spot_id` with impressions populated. An explicit `impressions: 0` is a measured zero; it does not mean pending or unavailable, and `null` is invalid. -**Experimental in AdCP 3.3: installment lifecycle.** `by_installment` rows MAY carry `installment_status` (`scheduled`, `tentative`, `live`, `postponed`, `cancelled`, `aired`, `published`) and, for a digital installment reported as `published`, a `publication` record: `published_at`, `posts[]` (one [`published_post`](https://adcontextprotocol.org/schemas/v3/core/assets/published-post-asset.json) reference per place it was published, so cross-posts are separate entries), optional `evidence`, and optional `verified_by` (absent means seller-reported). A row with no `installment_status` is a delivery row, as in AdCP 3.2. A `postponed` installment keeps its `installment_ref` with a new `scheduled_at`. A `cancelled` row omits `impressions` and other delivery metrics, and its `spend` reflects only what the accepted terms bill; a seller that declares `cancelled` MUST report a sold installment that will not run as `cancelled` rather than drop the row. `publication` is evidence, not by itself a billing event, and is read from the next reporting fire or from this task. Sellers declare the statuses they report in `reporting_capabilities.available_installment_statuses`, so a missing row is not read as non-delivery. A response served below AdCP 3.3 carries no `scheduled`, `tentative`, `postponed`, or `cancelled` rows ([Serving an older pin](/docs/reference/versioning#serving-an-older-pin)). See [Installment lifecycle and proof of publication](/docs/media-buy/media-buys/optimization-reporting#installment-lifecycle-and-proof-of-publication). +**Experimental in AdCP 3.3: installment lifecycle.** `by_installment` rows MAY carry `installment_status` (`scheduled`, `tentative`, `live`, `postponed`, `cancelled`, `aired`, `published`) and, for a digital installment reported as `published`, a `publication` record: `published_at`, `posts[]` (one [`published_post`](https://adcontextprotocol.org/schemas/v3/core/assets/published-post-asset.json) reference per place it was published, so cross-posts are separate entries), optional `evidence`, and optional `verified_by` (absent means seller-reported). A row with no `installment_status` is a delivery row, as in AdCP 3.2. A `postponed` installment keeps its `installment_ref` with a new `scheduled_at`. A `cancelled` row omits `impressions` and other delivery metrics, and its `spend` reflects only what the accepted terms bill; a seller that declares `cancelled` MUST report a sold installment that will not run as `cancelled` rather than drop the row. `publication` is evidence, not by itself a billing event, and is read from the next reporting fire or from this task. Sellers declare the statuses they report in `available_installment_statuses` on the canonical product's `reporting_capabilities`, so a missing row is not read as non-delivery. A response served below AdCP 3.3 carries no `scheduled`, `tentative`, `postponed`, or `cancelled` rows ([Serving an older pin](/docs/reference/versioning#serving-an-older-pin)). See [Installment lifecycle and proof of publication](/docs/media-buy/media-buys/optimization-reporting#installment-lifecycle-and-proof-of-publication). **Experimental in AdCP 3.3: spot lifecycle.** `spot_status` (`scheduled`, `aired`, `preempted`, `makegood`) lets one `spot_id` track from plan to as-run. `aired_at` is required only on `aired` and `makegood` rows, and on rows with no `spot_status`, which mean `aired`. `scheduled` and `preempted` rows carry `scheduled_at` and no `aired_at`. `makegood` rows carry `replaces_spot_id`, the `spot_id` of the preempted spot; a planned replacement carries it from `scheduled` onward, so the link is visible before it airs. Metrics on a `scheduled` row, such as `grps`, are expectations, not delivery, and are not part of package totals. `break_position` (`position_in_break`, `spots_in_break`, optional `break_id`) places a spot in its commercial break. `supports_spot_breakdown.available_statuses` and `supports_break_position` tell the buyer which of these the seller reports, so a missing row or field is not read as non-delivery. A response served below AdCP 3.3 carries no `scheduled` or `preempted` rows, because AdCP 3.2 requires `aired_at` on every row ([Serving an older pin](/docs/reference/versioning#serving-an-older-pin)). See [Spot lifecycle, makegoods, and break position](/docs/media-buy/media-buys/optimization-reporting#spot-lifecycle-makegoods-and-break-position) for the full rules and a plan-to-as-run example. diff --git a/docs/reference/experimental-status.mdx b/docs/reference/experimental-status.mdx index a8e2e38e07f..f7fdfbb3706 100644 --- a/docs/reference/experimental-status.mdx +++ b/docs/reference/experimental-status.mdx @@ -128,7 +128,7 @@ Schemas marked with `x-status: experimental` are the authoritative source. The c | `media_buy.gender_targeting` | Core `demographics.gender` predicates, product gender capabilities and overlay support/requirements, exact gender and mixed age/gender targeting resolution, the gender capability rollup, authoritative signal gender metadata, and the `sex_gender` governance classification — see [Core gender targeting](/docs/media-buy/advanced-topics/targeting#core-gender-targeting-33). Stable age targeting is excluded. | New portable gender contract without multi-party adoption evidence. Canonical demographic delivery reporting remains age-only in 3.3; this experiment does not add gendered delivery readback. | | `media_buy.product_identity` | The experimental `Product.identity` declaration on [`get_products`](/docs/media-buy/product-discovery/media-products) and [`list_products`](/docs/media-buy/task-reference/list_products) results, plus reach-unit coherence in delivery reporting | Product-level identity availability, the boundary between modeled and identifier-backed reach, and the methodology disclosure remain new cross-channel interoperability semantics. The field is deliberately product-scoped so mixed addressable and non-addressable inventory remains truthful while implementers validate the disclosure and reporting behavior. | | `media_buy.match_keys` | The `core/match-key.json` shape and `enums/match-key-use.json`, `media_buy.supported_match_keys` on [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities), `Product.identity.match_keys`, and `match_keys` on TMP provider registration (#8375). See [Match keys and cross-publisher frequency capping](/docs/media-buy/specification#match-keys-and-cross-publisher-frequency-capping-experimental). | Identity resolution now varies by surface and can change between browser and OS releases without a spec change. The use split (`frequency_cap` vs `identity_match`), the compatibility rule for counting a cap across sellers, and the surface qualifier need integration experience with real identity graphs and TMP deployments before freezing. | -| `media_buy.installment_lifecycle` | The `installment_status` and `publication` fields on `by_installment[]` rows of [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery), the `core/installment-publication.json` schema, the `available_installment_statuses` declaration in product `reporting_capabilities`, the `publication` value in `enums/available-metric.json`, and the `installment_delivery` param of `simulate_delivery` in `comply_test_controller` (#8088; see [Installment lifecycle and proof of publication](/docs/media-buy/media-buys/optimization-reporting#installment-lifecycle-and-proof-of-publication)) | Installment lifecycle and proof of post for creator posts, newsletter editions, and podcast episodes, parallel to `media_buy.spot_lifecycle`. Validated so far by one adopter, Availnet, a creator marketplace. The publication record is seller-reported evidence; a notification when an installment publishes is planned for 3.4 and may reshape how buyers learn of publication. | +| `media_buy.installment_lifecycle` | The `installment_status` and `publication` fields on `by_installment[]` rows of [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery), the `core/installment-publication.json` schema, the `available_installment_statuses` declaration in the canonical product's `reporting_capabilities` (`core/canonical-reporting-capabilities.json`), the `publication` value in `enums/available-metric.json`, and the `installment_delivery` param of `simulate_delivery` in `comply_test_controller` (#8088; see [Installment lifecycle and proof of publication](/docs/media-buy/media-buys/optimization-reporting#installment-lifecycle-and-proof-of-publication)) | Installment lifecycle and proof of post for creator posts, newsletter editions, and podcast episodes, parallel to `media_buy.spot_lifecycle`. Validated so far by one adopter, Availnet, a creator marketplace. The publication record is seller-reported evidence; a notification when an installment publishes is planned for 3.4 and may reshape how buyers learn of publication. | | `media_buy.spot_lifecycle` | The `spot_status`, `scheduled_at`, `replaces_spot_id`, and `break_position` fields on `by_spot[]` rows of [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery), the `enums/spot-status.json` enum, and the `available_statuses` and `supports_break_position` declarations on `supports_spot_breakdown` in product `reporting_capabilities` (#5683, #8009; see [Spot lifecycle, makegoods, and break position](/docs/media-buy/media-buys/optimization-reporting#spot-lifecycle-makegoods-and-break-position)) | Report-time half of linear-TV preemption and makegoods, validated so far by one Dutch linear TV pilot and one US broadcast operator. Contract-time preemption criteria and an interactive replacement-offer flow are planned for 3.4 and may reshape the status vocabulary, the makegood linkage, and how plan rows reconcile with broadcast as-run logs. | | `media_buy.delivery_projection` | The optional `projection` object on top-level `by_package[]` rows of [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery#delivery-projection) (schema: `core/delivery-projection.json`) and the `delivery-projection-basis` and `delivery-projection-end-state` enums (#5850) | Advisory forward-looking projection of a package's end-of-flight delivery, with one production adopter so far. The `complete` / `early_exhaustion` materiality split was authored during implementation rather than ratified in the brief, and the `basis` method-class vocabulary is expected to grow. Sellers declare the feature id in `experimental_features`; there is no capability field to gate it, because the object appears only on delivery responses. | | `media_buy.reporting_delivery` | **Reliable Reporting 1.0**: [`get_reporting_status`](/docs/media-buy/task-reference/get_reporting_status), opt-in [`sync_reporting_status`](/docs/media-buy/task-reference/sync_reporting_status), [`sync_reporting_receipts`](/docs/media-buy/task-reference/sync_reporting_receipts), `sync_accounts.reporting_delivery_configs[]`, reporting notifications, and the `media_buy.reporting_delivery` capability block | Proper-name AdCP 3.2 reporting reliability surface (#6911), explicitly discovered through `reliable_reporting_version: "1.0"`. Core, Managed Delivery, and Reconciled Billing have separate capability-gated conformance storyboards. The [consumer-status migration](/docs/reference/migration/reliable-reporting-status-loop) is an active breaking-change notice: current source adds an opt-in task, which becomes required Core only in the next eligible minor after October 24, 2026. Its `content_mismatch` status, stale-`received` grace window, buyer posting deadline, issue lifecycle fields, escalation contact, the reserved `authoritative_party` field ([#7440](https://github.com/adcontextprotocol/adcp/issues/7440)), and the optional report-definition `source.custody` field, new in 3.3 ([#8185](https://github.com/adcontextprotocol/adcp/issues/8185)) are all additive within that experimental surface. [Superseded snapshot compaction](/docs/reference/migration/3-2-to-3-3#superseded-reporting-snapshots-may-be-compacted) ([#8081](https://github.com/adcontextprotocol/adcp/issues/8081)) is a second active breaking-change notice: sellers may compact older superseded snapshot content only in the next eligible minor after November 20, 2026. Multi-party production operation and provider-specific compatibility still need implementation experience before freezing. | diff --git a/static/schemas/source/core/canonical-reporting-capabilities.json b/static/schemas/source/core/canonical-reporting-capabilities.json index 6dfb48c7cec..c57a6ce0f10 100644 --- a/static/schemas/source/core/canonical-reporting-capabilities.json +++ b/static/schemas/source/core/canonical-reporting-capabilities.json @@ -43,6 +43,14 @@ "supports_property_breakdown": { "type": "boolean" }, "supports_collection_breakdown": { "type": "boolean" }, "supports_installment_breakdown": { "type": "boolean" }, + "available_installment_statuses": { + "type": "array", + "x-status": "experimental", + "x-added-in": "3.3.0", + "items": { "$ref": "/schemas/enums/installment-status.json" }, + "uniqueItems": true, + "description": "Installment lifecycle statuses this product reports in by_installment. Meaningful only with supports_installment_breakdown: true. Absent, or an empty array, means by_installment carries delivery rows only, with no installment_status, as in AdCP 3.2. When present, a status that is not listed is not reported, so the absence of such rows proves nothing and a buyer MUST NOT read it as nothing scheduled, postponed, or cancelled. 'published', 'aired', and 'live' are delivery statuses and need not be listed. Sellers MUST NOT emit this field to callers pinned below AdCP 3.3, or to callers with no declared pin." + }, "supports_collection_property_breakdown": { "type": "boolean" }, "supports_installment_property_breakdown": { "type": "boolean" }, "supports_placement_property_breakdown": { "type": "boolean" }, diff --git a/static/schemas/source/core/reporting-capabilities.json b/static/schemas/source/core/reporting-capabilities.json index 25200b30b20..6558da59fb4 100644 --- a/static/schemas/source/core/reporting-capabilities.json +++ b/static/schemas/source/core/reporting-capabilities.json @@ -143,14 +143,6 @@ "type": "boolean", "description": "Whether this product supports installment breakdowns in delivery reporting (by_installment within by_package)." }, - "available_installment_statuses": { - "type": "array", - "x-status": "experimental", - "x-added-in": "3.3.0", - "items": { "$ref": "/schemas/enums/installment-status.json" }, - "uniqueItems": true, - "description": "Installment lifecycle statuses this product reports in by_installment. Meaningful only with supports_installment_breakdown: true. Absent, or an empty array, means by_installment carries delivery rows only, with no installment_status, as in AdCP 3.2. When present, a status that is not listed is not reported, so the absence of such rows proves nothing and a buyer MUST NOT read it as nothing scheduled, postponed, or cancelled. 'published', 'aired', and 'live' are delivery statuses and need not be listed. Sellers MUST NOT emit this field to callers pinned below AdCP 3.3, or to callers with no declared pin." - }, "supports_collection_property_breakdown": { "type": "boolean", "description": "Whether this product supports collection × property intersection reporting (by_collection_property within by_package)." diff --git a/tests/installment-lifecycle-reporting.test.cjs b/tests/installment-lifecycle-reporting.test.cjs index 98e44a1efdd..d07708c96df 100644 --- a/tests/installment-lifecycle-reporting.test.cjs +++ b/tests/installment-lifecycle-reporting.test.cjs @@ -43,22 +43,22 @@ const publication = { describe("installment lifecycle and proof of publication", () => { let validateRow; let validatePublication; - let validateCapabilities; + let validateCanonicalCapabilities; let validateController; before(async () => { - [validateRow, validatePublication, validateCapabilities, validateController] = + [validateRow, validatePublication, validateCanonicalCapabilities, validateController] = await Promise.all([ compile(readSchema("/schemas/core/installment-delivery-metrics.json")), compile(readSchema("/schemas/core/installment-publication.json")), - compile(readSchema("/schemas/core/reporting-capabilities.json")), + compile(readSchema("/schemas/core/canonical-reporting-capabilities.json")), compile(readSchema("/schemas/compliance/comply-test-controller-request.json")), ]); }); it("marks every new node experimental and 3.3.0", () => { const rowProps = readSchema("/schemas/core/installment-delivery-metrics.json").allOf[1].properties; - const caps = readSchema("/schemas/core/reporting-capabilities.json").properties; + const caps = readSchema("/schemas/core/canonical-reporting-capabilities.json").properties; const publicationSchema = readSchema("/schemas/core/installment-publication.json"); const controller = readSchema("/schemas/compliance/comply-test-controller-request.json"); const nodes = [ @@ -165,7 +165,21 @@ describe("installment lifecycle and proof of publication", () => { assert.equal(validatePublication({ ...publication, evidence: { notes: "no url" } }), false); }); - it("declares reportable statuses on reporting_capabilities", () => { + it("declares available_installment_statuses on canonical reporting capabilities only", () => { + const declared = readSchema("/schemas/core/canonical-reporting-capabilities.json").properties + .available_installment_statuses; + assert.equal(declared.type, "array"); + assert.equal(declared.uniqueItems, true); + assert.equal(declared.items.$ref, "/schemas/enums/installment-status.json"); + assert.equal(declared["x-status"], "experimental"); + assert.equal(declared["x-added-in"], "3.3.0"); + // The legacy schema is open, so it still accepts the key as an unknown field; + // assert only that it is not declared (3.3 adds nothing to deprecated get_products). + const legacy = readSchema("/schemas/core/reporting-capabilities.json"); + assert.equal(legacy.properties.available_installment_statuses, undefined); + }); + + it("declares reportable statuses on canonical reporting_capabilities", () => { const base = { available_reporting_frequencies: ["daily"], expected_delay_minutes: 60, @@ -176,15 +190,15 @@ describe("installment lifecycle and proof of publication", () => { supports_installment_breakdown: true, }; assert.equal( - validateCapabilities({ ...base, available_installment_statuses: ["scheduled", "cancelled"] }), + validateCanonicalCapabilities({ ...base, available_installment_statuses: ["scheduled", "cancelled"] }), true ); assert.equal( - validateCapabilities({ ...base, available_installment_statuses: ["scheduled", "scheduled"] }), + validateCanonicalCapabilities({ ...base, available_installment_statuses: ["scheduled", "scheduled"] }), false ); assert.equal( - validateCapabilities({ ...base, available_installment_statuses: ["skipped"] }), + validateCanonicalCapabilities({ ...base, available_installment_statuses: ["skipped"] }), false ); });