From 2d2767edc24546ddb10098ec9b02a4d8f6cb6587 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 29 Apr 2026 06:52:04 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(schemas):=20package.committed=5Fmetric?= =?UTF-8?q?s=20=E2=80=94=20frozen=20reporting=20contract=20on=20create?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds package.committed_metrics and package.committed_vendor_metrics — frozen snapshots of the product's reporting_capabilities at create_media_buy response time. Closes #3481. Resolves the audit gap flagged on PR #3472: without per-package snapshots, missing_metrics is computed against the product's live state, so a 90-day-old buy is incorrectly judged "clean" when the seller quietly drops a metric they originally committed to. missing_metrics on get_media_buy_delivery now reconciles against committed_metrics when present, falling back to product.available_metrics when absent. Optional in v1 (sellers adopt incrementally; absence is conformant); MUST NOT change post-creation (update_media_buy cannot modify; renegotiation requires a new buy); snapshots full available_metrics, not the intersection with required_metrics. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/committed-metrics.md | 75 +++++++++++++++++++ .../task-reference/create_media_buy.mdx | 8 ++ .../task-reference/get_media_buy_delivery.mdx | 2 +- static/schemas/source/core/package.json | 29 +++++++ .../get-media-buy-delivery-response.json | 2 +- 5 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 .changeset/committed-metrics.md diff --git a/.changeset/committed-metrics.md b/.changeset/committed-metrics.md new file mode 100644 index 0000000000..e797266857 --- /dev/null +++ b/.changeset/committed-metrics.md @@ -0,0 +1,75 @@ +--- +"adcontextprotocol": minor +--- + +Add `package.committed_metrics` and `package.committed_vendor_metrics` — +frozen snapshots of the product's `reporting_capabilities.available_metrics` +and `vendor_metrics` stamped at `create_media_buy` response time. Closes +#3481. + +**The audit gap.** PR #3472 established that the product's +`available_metrics` becomes the binding reporting contract carried into +the resulting media buy. That holds **only if** the product is immutable +AND the seller stores a snapshot at buy creation. Neither is guaranteed: + +- Products mutate (sellers add/remove metrics from `available_metrics` + as their reporting infrastructure evolves) +- Without a per-package snapshot, `missing_metrics` on + `get_media_buy_delivery` is computed against "what the product + *currently* advertises" — a 90-day-old buy is incorrectly judged as + "clean" because the seller quietly dropped a metric they originally + committed to +- An ops team auditing a 90-day-old buy will not trust an implicit + contract reference + +This was flagged on PR #3472 by the product expert as the primary +sell-side audit gap. + +**Changes.** + +- `core/package.json`: new `committed_metrics: AvailableMetric[]` field + and new `committed_vendor_metrics: { vendor, metric_id }[]` field. Both + optional in v1; sellers without per-package snapshot infrastructure + fall back to the product's live state (absence is conformant). Both + MUST NOT change post-creation — `update_media_buy` cannot modify them. + Renegotiating the metric contract requires a new buy. +- `media-buy/get-media-buy-delivery-response.json`: `missing_metrics` + description updated to declare the reconciliation source — when + `committed_metrics` is present, that is the contract; when absent, + fall back to the product's current `available_metrics`. +- `docs/media-buy/task-reference/create_media_buy.mdx`: new "Reporting + contract on confirmed packages" subsection documenting the snapshot + semantics, immutability, and v1-optional posture. +- `docs/media-buy/task-reference/get_media_buy_delivery.mdx`: bullet + updated to point at the reconciliation source. + +**Design choices spelled out (resolves the three open questions on #3481).** + +1. **Optional or required?** Optional. Forcing the snapshot at v1 would + break existing implementations on first deployment. Optional with a + doc note that "buyers SHOULD reconcile against `committed_metrics` + when present and fall back to the product's live state when absent" + lets sellers adopt incrementally. Expected to become required at the + next major. + +2. **What snapshots into `committed_metrics`?** The product's full + `reporting_capabilities.available_metrics` at the moment of + `create_media_buy`, NOT the intersection with the buyer's + `required_metrics` filter. The product committed to reporting all + those metrics; reducing to the intersection would silently drop + reporting on metrics the buyer didn't explicitly list but the seller + still has. `requested_metrics` (on `reporting_webhook`) remains the + buyer's payload-optimization filter — a separate concept. + +3. **Mutation policy?** Frozen at creation, MUST NOT change post-creation. + `update_media_buy` cannot modify `committed_metrics` or + `committed_vendor_metrics`. If the buyer/seller need to renegotiate, + that's a new buy. This is the cleanest contract; mutability with + audit trail can be added later if real demand emerges. + +**Backwards compatibility.** Optional and additive. Sellers without +snapshot infrastructure fall back to the implicit contract (product's +current state) — this matches the v1 behavior of #3472. Buyers can +incrementally upgrade to consume `committed_metrics` when present. + +Closes #3481. diff --git a/docs/media-buy/task-reference/create_media_buy.mdx b/docs/media-buy/task-reference/create_media_buy.mdx index 355e3ffec8..dcb7959ed6 100644 --- a/docs/media-buy/task-reference/create_media_buy.mdx +++ b/docs/media-buy/task-reference/create_media_buy.mdx @@ -212,6 +212,14 @@ npx @adcp/client@latest \ | `creative_deadline` | ISO 8601 timestamp for creative upload deadline | | `packages` | Array of created packages with complete state. Packages may include per-package `creative_deadline` when different from the media buy deadline. | +#### Reporting contract on confirmed packages + +Each package in the response MAY carry `committed_metrics` and `committed_vendor_metrics` — frozen snapshots of the product's `reporting_capabilities.available_metrics` and `vendor_metrics` at the moment the buy was created. These become the binding reporting contract for the package's lifetime, independent of subsequent product mutations: + +- `missing_metrics` on `get_media_buy_delivery` is computed against `committed_metrics` when present (against the product's current `available_metrics` when absent). +- The fields MUST NOT change post-creation. `update_media_buy` cannot modify them; renegotiating the metric contract requires a new buy. +- Optional in v1 (sellers without per-package snapshot infrastructure can adopt incrementally; absence is conformant). Expected to become required at the next major. + ### Error Response | Field | Description | 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 d0a20e705d..1843f9d049 100644 --- a/docs/media-buy/task-reference/get_media_buy_delivery.mdx +++ b/docs/media-buy/task-reference/get_media_buy_delivery.mdx @@ -586,7 +586,7 @@ The `by_package` array provides per-package delivery details with these key fiel - **`pricing_model`**: How the package is billed (cpm, cpcv, cpp, etc.) **Accountability**: -- **`missing_metrics`**: Metrics the product's `reporting_capabilities.available_metrics` advertised but that are not populated in this report. Empty array (or absent) indicates clean delivery against the declared contract; non-empty signals an accountability breach. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`. +- **`missing_metrics`**: Metrics the binding reporting contract advertised but that are not populated in this report. Reconciled against `package.committed_metrics` (the frozen snapshot stamped at `create_media_buy`) when present; falls back to the product's current `reporting_capabilities.available_metrics` when absent. Empty array (or absent) indicates clean delivery against the contract; non-empty signals an accountability breach. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`. - **`vendor_metric_values`**: Reported values for vendor-defined metrics that the product's `reporting_capabilities.vendor_metrics` declared (proprietary attention, emissions, panel demographics, brand-lift surveys, etc.). Each entry carries `{ vendor, metric_id, value, unit?, measurable_impressions?, breakdown? }`. The `measurable_impressions` field is the coverage denominator — vendor measurement is rarely 100% of delivery, since vendors only score impressions where their SDK fires or their panel matches. Buyers compute coverage as `measurable_impressions / impressions`. When `measurable_impressions` is absent, coverage is unspecified — buyers MUST NOT compute a coverage rate or assume full coverage. When a declared vendor metric is omitted entirely from this array, infer no measurement happened (no integration). **Key Distinction**: `paused` reflects buyer control, while `delivery_status` reflects system reality. A package can be not paused but have `delivery_status: "budget_exhausted"`. diff --git a/static/schemas/source/core/package.json b/static/schemas/source/core/package.json index 8984067238..6437e0d0a8 100644 --- a/static/schemas/source/core/package.json +++ b/static/schemas/source/core/package.json @@ -71,6 +71,35 @@ }, "minItems": 1 }, + "committed_metrics": { + "type": "array", + "description": "Frozen snapshot of the product's `reporting_capabilities.available_metrics` at the moment this package was created. Becomes the binding reporting contract for the package's lifetime, independent of subsequent product mutations. The seller stamps this on the `create_media_buy` response; once set, it MUST NOT change for the lifetime of the package — `update_media_buy` cannot modify it. If the buyer/seller need to renegotiate the metric contract, that is a new buy. The `missing_metrics` field on `get_media_buy_delivery` is computed against `committed_metrics` when present, and against the product's current `available_metrics` when absent (sellers without per-package snapshot infrastructure can adopt incrementally; absence is conformant). Optional in v1; expected to become required at the next major once the audit pattern is established.", + "items": { + "$ref": "/schemas/enums/available-metric.json" + }, + "uniqueItems": true, + "examples": [ + ["impressions", "spend", "completed_views", "completion_rate"], + ["impressions", "spend", "clicks", "ctr", "viewability"] + ] + }, + "committed_vendor_metrics": { + "type": "array", + "description": "Frozen snapshot of the product's `reporting_capabilities.vendor_metrics` at the moment this package was created. Same contract semantics as `committed_metrics` but for vendor-defined metrics — buyers reconcile delivery against the snapshotted vendor list, not the product's live state. Optional in v1; absent means sellers without snapshot infrastructure fall back to the current `vendor_metrics` declaration. MUST NOT change post-creation.", + "items": { + "type": "object", + "properties": { + "vendor": { + "$ref": "/schemas/core/brand-ref.json" + }, + "metric_id": { + "$ref": "/schemas/core/vendor-metric-id.json" + } + }, + "required": ["vendor", "metric_id"], + "additionalProperties": false + } + }, "creative_assignments": { "type": "array", "description": "Creative assets assigned to this package", diff --git a/static/schemas/source/media-buy/get-media-buy-delivery-response.json b/static/schemas/source/media-buy/get-media-buy-delivery-response.json index 9d259701fb..debea99cb5 100644 --- a/static/schemas/source/media-buy/get-media-buy-delivery-response.json +++ b/static/schemas/source/media-buy/get-media-buy-delivery-response.json @@ -282,7 +282,7 @@ }, "missing_metrics": { "type": "array", - "description": "Metrics that the product's `reporting_capabilities.available_metrics` declared but that are NOT populated in this report. Empty array (or absent) indicates clean delivery against the declared contract. Non-empty signals an accountability breach — the seller advertised the capability at discovery but did not produce the value here. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`.", + "description": "Metrics that the binding reporting contract declared but that are NOT populated in this report. Reconciliation source: when the package carries `committed_metrics` (frozen snapshot stamped at create_media_buy), `missing_metrics` is computed against THAT — independent of subsequent product mutations. When `committed_metrics` is absent, fall back to the product's current `reporting_capabilities.available_metrics`. Empty array (or absent) indicates clean delivery against the contract. Non-empty signals an accountability breach — the seller committed to the metric but did not produce the value here. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`.", "items": { "$ref": "/schemas/enums/available-metric.json" }, From ea2faf18e65f194ab44977f1780e5e5e12b17282 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 29 Apr 2026 08:20:30 -0400 Subject: [PATCH 2/2] =?UTF-8?q?refactor(schemas):=20tighten=20committed=5F?= =?UTF-8?q?metrics=20=E2=80=94=20minItems,=20immutability,=20audit-gap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review fixes per expert feedback on #3510: 1. Add minItems: 1 to both committed_metrics and committed_vendor_metrics. Protocol expert flagged that empty array vs absent created semantic ambiguity ("committed to nothing" vs "didn't snapshot"). Schema-level forbid eliminates the ambiguity — absence is the only "no snapshot" signal. 2. Tighten immutability prose: sellers MUST reject update_media_buy attempts to modify these fields with validation_error (suggested code IMMUTABLE_FIELD). Schema-level enforcement still requires a parallel change to package-update.json (filed as follow-up). 3. Acknowledge the fallback as a known audit gap. Both reviewers flagged that "optional with silent fallback" creates plausible deniability — the spec creates a contract that sellers can opt out of without buyers detecting it. Documented explicitly: sellers that omit committed_metrics accept the audit risk; buyers SHOULD treat absence as "no audit-grade contract" rather than "clean delivery." Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/media-buy/task-reference/create_media_buy.mdx | 5 +++-- static/schemas/source/core/package.json | 11 ++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/media-buy/task-reference/create_media_buy.mdx b/docs/media-buy/task-reference/create_media_buy.mdx index dcb7959ed6..c3a4b41d2b 100644 --- a/docs/media-buy/task-reference/create_media_buy.mdx +++ b/docs/media-buy/task-reference/create_media_buy.mdx @@ -217,8 +217,9 @@ npx @adcp/client@latest \ Each package in the response MAY carry `committed_metrics` and `committed_vendor_metrics` — frozen snapshots of the product's `reporting_capabilities.available_metrics` and `vendor_metrics` at the moment the buy was created. These become the binding reporting contract for the package's lifetime, independent of subsequent product mutations: - `missing_metrics` on `get_media_buy_delivery` is computed against `committed_metrics` when present (against the product's current `available_metrics` when absent). -- The fields MUST NOT change post-creation. `update_media_buy` cannot modify them; renegotiating the metric contract requires a new buy. -- Optional in v1 (sellers without per-package snapshot infrastructure can adopt incrementally; absence is conformant). Expected to become required at the next major. +- The fields MUST NOT change post-creation. Sellers MUST reject `update_media_buy` requests that attempt to modify either field with a `validation_error` (suggested code: `IMMUTABLE_FIELD`). Renegotiating the metric contract requires a new buy. +- Optional in v1 — sellers without per-package snapshot infrastructure can adopt incrementally. Absence is conformant, but it carries a known audit gap: without the snapshot, `missing_metrics` reconciles against the product's live state at report time, which may not reflect what was committed at create time. Sellers that omit `committed_metrics` accept this risk; buyers SHOULD treat the absence as "no audit-grade contract" rather than "clean delivery." Expected to become required at the next major. +- Empty array is rejected at the schema level (`minItems: 1`) — absence is the only "no snapshot" signal, eliminating ambiguity between "I committed to nothing" and "I didn't snapshot." ### Error Response diff --git a/static/schemas/source/core/package.json b/static/schemas/source/core/package.json index 6437e0d0a8..b4bcdb027e 100644 --- a/static/schemas/source/core/package.json +++ b/static/schemas/source/core/package.json @@ -73,10 +73,11 @@ }, "committed_metrics": { "type": "array", - "description": "Frozen snapshot of the product's `reporting_capabilities.available_metrics` at the moment this package was created. Becomes the binding reporting contract for the package's lifetime, independent of subsequent product mutations. The seller stamps this on the `create_media_buy` response; once set, it MUST NOT change for the lifetime of the package — `update_media_buy` cannot modify it. If the buyer/seller need to renegotiate the metric contract, that is a new buy. The `missing_metrics` field on `get_media_buy_delivery` is computed against `committed_metrics` when present, and against the product's current `available_metrics` when absent (sellers without per-package snapshot infrastructure can adopt incrementally; absence is conformant). Optional in v1; expected to become required at the next major once the audit pattern is established.", + "description": "Frozen snapshot of the product's `reporting_capabilities.available_metrics` at the moment this package was created. Becomes the binding reporting contract for the package's lifetime, independent of subsequent product mutations. The seller stamps this on the `create_media_buy` response; once set, it MUST NOT change for the lifetime of the package. Sellers MUST reject `update_media_buy` requests that attempt to modify `committed_metrics` with a `validation_error` (suggested code: `IMMUTABLE_FIELD`). If the buyer/seller need to renegotiate the metric contract, that is a new buy. The `missing_metrics` field on `get_media_buy_delivery` is computed against `committed_metrics` when present, and against the product's current `available_metrics` when absent. Optional in v1: sellers without per-package snapshot infrastructure can adopt incrementally — absence is conformant, but it carries a known audit gap (without the snapshot, `missing_metrics` reconciles against the product's live state at report time, which may not reflect the contract at create time; sellers that omit `committed_metrics` accept this risk). Expected to become required at the next major once the audit pattern is established. Empty array (`[]`) is a valid frozen snapshot meaning 'this package commits to no metrics' — semantically distinct from omitting the field.", "items": { "$ref": "/schemas/enums/available-metric.json" }, + "minItems": 1, "uniqueItems": true, "examples": [ ["impressions", "spend", "completed_views", "completion_rate"], @@ -85,7 +86,7 @@ }, "committed_vendor_metrics": { "type": "array", - "description": "Frozen snapshot of the product's `reporting_capabilities.vendor_metrics` at the moment this package was created. Same contract semantics as `committed_metrics` but for vendor-defined metrics — buyers reconcile delivery against the snapshotted vendor list, not the product's live state. Optional in v1; absent means sellers without snapshot infrastructure fall back to the current `vendor_metrics` declaration. MUST NOT change post-creation.", + "description": "Frozen snapshot of the product's `reporting_capabilities.vendor_metrics` at the moment this package was created. Same contract semantics as `committed_metrics` but for vendor-defined metrics — buyers reconcile delivery against the snapshotted vendor list, not the product's live state. Optional in v1; absent means sellers without snapshot infrastructure fall back to the current `vendor_metrics` declaration (same audit-gap caveat as `committed_metrics`). MUST NOT change post-creation; sellers MUST reject `update_media_buy` attempts to modify with `validation_error`.", "items": { "type": "object", "properties": { @@ -98,7 +99,11 @@ }, "required": ["vendor", "metric_id"], "additionalProperties": false - } + }, + "minItems": 1, + "examples": [ + [{ "vendor": { "domain": "attentionvendor.example" }, "metric_id": "attention_units" }] + ] }, "creative_assignments": { "type": "array",