Skip to content

feat(schemas): package.committed_metrics — frozen reporting contract on create#3510

Merged
bokelley merged 2 commits into
mainfrom
bokelley/committed-metrics
Apr 29, 2026
Merged

feat(schemas): package.committed_metrics — frozen reporting contract on create#3510
bokelley merged 2 commits into
mainfrom
bokelley/committed-metrics

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Adds 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 is computed against "what the product currently advertises" — a 90-day-old buy is incorrectly judged "clean" because the seller quietly dropped a metric they originally committed to
  • An ops team auditing a 90-day-old buy can't trust an implicit contract reference

Flagged on PR #3472 by the product expert as the primary sell-side audit gap.

Three open design questions resolved

  1. Optional or required? Optional in v1. Forcing the snapshot would break existing implementations on first deployment. With a doc note that buyers SHOULD reconcile against committed_metrics when present and fall back to product live state when absent, sellers adopt incrementally. Expected to become required at the next major.

  2. What snapshots in? The product's full available_metrics at the moment of create_media_buy. NOT the intersection with required_metrics. The product committed to reporting all those metrics; reducing to the intersection would silently drop reporting on metrics the buyer didn't list but the seller has. requested_metrics (on reporting_webhook) remains the buyer's payload-optimization filter — separate concept.

  3. Mutation policy? Frozen at creation. MUST NOT change post-creation. update_media_buy cannot modify either field. Renegotiation requires a new buy. Cleanest contract; mutability with audit trail can be added later if real demand emerges.

Reconciliation

missing_metrics on get_media_buy_delivery:

  • When package.committed_metrics is present → reconcile against the snapshot
  • When absent → fall back to product's current available_metrics (today's behavior)

This makes the v1 behavior of #3472 work for sellers without snapshot infrastructure, and gives sellers WITH snapshot infrastructure the ability to honor a frozen contract for the package's lifetime.

Backwards compatibility

Optional and additive. Sellers without snapshot infrastructure see no change. Buyers incrementally upgrade to consume committed_metrics when present.

Test plan

  • npm run build:schemas — clean
  • npm run test:schemas — 7/7
  • npm run test:examples — 34/34
  • npm run test:json-schema — 255/255
  • npm run test:composed — 32/32
  • npm run typecheck — clean
  • precommit + pre-push — green

Closes #3481.

🤖 Generated with Claude Code

bokelley and others added 2 commits April 29, 2026 06:52
…on create

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) <noreply@anthropic.com>
…, audit-gap

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) <noreply@anthropic.com>
@bokelley
bokelley merged commit 59f1c37 into main Apr 29, 2026
18 checks passed
@bokelley
bokelley deleted the bokelley/committed-metrics branch April 29, 2026 12:26
bokelley added a commit that referenced this pull request Apr 30, 2026
…ymmetric audit

Reshape package.committed_metrics and by_package.missing_metrics from
string arrays into discriminated object arrays covering both standard
and vendor-defined metrics. Each entry carries an explicit `scope`
discriminator (standard | vendor) and committed_metrics entries carry
`committed_at` timestamps so the array doubles as the contract
amendment ledger.

- Day-1 entries share `committed_at = confirmed_at`
- Mid-flight additions appended via update_media_buy with their own
  committed_at, append-only (sellers MUST reject modify/remove with
  validation_error)
- missing_metrics filters by `committed_at < reporting_period.end` so
  metrics committed mid-flight are audited only from commitment forward
  (matches IAB Open Measurement §4.3 precedent)
- Standalone committed_vendor_metrics deleted; vendor entries live in
  the unified array with scope: "vendor"

Vendor metric accountability is no longer scoped by metric type — the
advisory-vs-accountable distinction now lives at the contract layer.
Any metric stamped in committed_metrics is accountable, regardless of
scope. Sellers who can't credibly attest to a vendor metric SHOULD
NOT stamp it; absence keeps it advisory.

Closes #3518 (every entry timestamped, so amendments are just new
entries) and #3519 (vendor entries live in unified missing_metrics).
Supersedes the parallel-array design from #3510 (merged hours ago,
zero GA adopters; cleaner final shape lands before adoption hardens).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 30, 2026
Add committed_metrics_supported boolean to media-buy-features.json.
Closes the buyer-side detection gap from #3510 — absence of
committed_metrics on a buy response was indistinguishable between
"seller didn't snapshot this buy" (missing infra) and "seller chose
not to snapshot for adversarial reasons."

With this flag, buyers can require sellers that commit to populating
committed_metrics — and use it as a get_products required_features
filter to narrow the catalog accordingly.

One flag (not two) because the unified committed_metrics array (per
#3576) covers both standard and vendor-defined entries — one snapshot
capability, one flag.

MUST timing is atomic with the flag — sellers declaring true MUST
populate committed_metrics on create_media_buy responses AND honor
append-only mid-flight additions on update_media_buy. Advisory-only
flags leave the audit gap exploitable.

Closes #3517.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 2, 2026
…ymmetric audit

Reshape package.committed_metrics and by_package.missing_metrics from
string arrays into discriminated object arrays covering both standard
and vendor-defined metrics. Each entry carries an explicit `scope`
discriminator (standard | vendor) and committed_metrics entries carry
`committed_at` timestamps so the array doubles as the contract
amendment ledger.

- Day-1 entries share `committed_at = confirmed_at`
- Mid-flight additions appended via update_media_buy with their own
  committed_at, append-only (sellers MUST reject modify/remove with
  validation_error)
- missing_metrics filters by `committed_at < reporting_period.end` so
  metrics committed mid-flight are audited only from commitment forward
  (matches IAB Open Measurement §4.3 precedent)
- Standalone committed_vendor_metrics deleted; vendor entries live in
  the unified array with scope: "vendor"

Vendor metric accountability is no longer scoped by metric type — the
advisory-vs-accountable distinction now lives at the contract layer.
Any metric stamped in committed_metrics is accountable, regardless of
scope. Sellers who can't credibly attest to a vendor metric SHOULD
NOT stamp it; absence keeps it advisory.

Closes #3518 (every entry timestamped, so amendments are just new
entries) and #3519 (vendor entries live in unified missing_metrics).
Supersedes the parallel-array design from #3510 (merged hours ago,
zero GA adopters; cleaner final shape lands before adoption hardens).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 2, 2026
…ymmetric audit (#3576)

* feat(schemas): unify metric accountability — timestamped contract + symmetric audit

Reshape package.committed_metrics and by_package.missing_metrics from
string arrays into discriminated object arrays covering both standard
and vendor-defined metrics. Each entry carries an explicit `scope`
discriminator (standard | vendor) and committed_metrics entries carry
`committed_at` timestamps so the array doubles as the contract
amendment ledger.

- Day-1 entries share `committed_at = confirmed_at`
- Mid-flight additions appended via update_media_buy with their own
  committed_at, append-only (sellers MUST reject modify/remove with
  validation_error)
- missing_metrics filters by `committed_at < reporting_period.end` so
  metrics committed mid-flight are audited only from commitment forward
  (matches IAB Open Measurement §4.3 precedent)
- Standalone committed_vendor_metrics deleted; vendor entries live in
  the unified array with scope: "vendor"

Vendor metric accountability is no longer scoped by metric type — the
advisory-vs-accountable distinction now lives at the contract layer.
Any metric stamped in committed_metrics is accountable, regardless of
scope. Sellers who can't credibly attest to a vendor metric SHOULD
NOT stamp it; absence keeps it advisory.

Closes #3518 (every entry timestamped, so amendments are just new
entries) and #3519 (vendor entries live in unified missing_metrics).
Supersedes the parallel-array design from #3510 (merged hours ago,
zero GA adopters; cleaner final shape lands before adoption hardens).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(schemas): add measurement-standard qualifier on standard-scope contract entries

Adds an optional `qualifier` object on `committed_metrics` and
`missing_metrics` standard-scope entries, with a single defined key
in v1: `viewability_standard` (mrc | groupm).

Without this, a contract entry like
`{scope: "standard", metric_id: "viewable_rate"}` is ambiguous —
MRC and GroupM viewability are materially different thresholds and
not comparable, so the audit channel can't tell whether the seller
delivered the standard the buyer expected. The qualifier lets a
buyer commit to MRC and have a GroupM-only delivery report flagged
as missing the MRC commitment.

The qualifier object is closed (`additionalProperties: false`); future
qualifiers (completion threshold, reach unit) get added explicitly in
subsequent minors rather than via free-form keys.

Came out of a field discussion where a partner proposed an `ext`-level
viewability rollup at root `aggregated_totals` — the right place to
handle standard-disambiguation is the per-package contract entry, not
the root aggregate. Root aggregation is a separate question.

Wired in:
- core/package.json: add optional qualifier to standard-scope item in
  committed_metrics. Updated description and example.
- media-buy/get-media-buy-delivery-response.json: add the same optional
  qualifier to the standard-scope item in missing_metrics, with a
  description noting it must match the corresponding committed_metrics
  qualifier.
- docs/media-buy/task-reference/create_media_buy.mdx: document the
  qualifier in the reporting-contract worked example and how it
  affects reconciliation.
- docs/media-buy/task-reference/get_media_buy_delivery.mdx: update
  the missing_metrics field doc with the qualifier symmetry.
- .changeset/unified-metric-accountability.md: add the
  measurement-standard-qualifier section.

Tests: schemas (7/7), examples (34/34), json-schema (255/255),
composed (32/32) all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(schemas): fold #3578 — buyer-proposed committed_metrics on create request

Add committed_metrics to package-request.json so buyers can propose the
reporting contract on create_media_buy, parallel to how measurement_terms
and performance_standards already work. Closes #3578 by folding into
#3576's unified-metric-accountability scope rather than shipping
separately — keeps the proposal/acceptance/audit surface coherent.

Negotiation pattern matches the existing precedent:
- Buyer proposes committed_metrics on the package request
- Seller accepts → echoes on response with committed_at stamped
- Seller rejects → TERMS_REJECTED with explanation of which entries
  were unworkable
- Seller normalizes → echoes a different but compatible list; buyer
  retries with the normalized terms

Request-side entries use the same {scope, metric_id, vendor?}
discriminated shape as response-side, MINUS committed_at — that
timestamp is stamped by the seller on accept, not proposed by the
buyer.

Constraints: each scope:standard entry's metric_id MUST be in the
product's available_metrics; each scope:vendor entry's (vendor,
metric_id) MUST appear in the product's vendor_metrics. Sellers
SHOULD reject TERMS_REJECTED when the proposal exceeds product
capability.

Closes #3578.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(schemas): regenerate index.json after rebase on main

Pre-push hook caught version mismatch — main's published version state
required regenerating the schema registry. No semantic changes; pure
build artifact catchup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

package.committed_metrics — frozen reporting contract on create_media_buy response

1 participant