Skip to content

feat(schemas): required_metrics filter + missing_metrics accountability#3472

Merged
bokelley merged 3 commits into
mainfrom
bokelley/required-metrics-accountability
Apr 28, 2026
Merged

feat(schemas): required_metrics filter + missing_metrics accountability#3472
bokelley merged 3 commits into
mainfrom
bokelley/required-metrics-accountability

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Adds end-to-end metric accountability through the media buy lifecycle. Builds on the enum reconciliation in #3453.

  • filters.required_metrics on get_products — buyer declares the metric vocabulary they need at discovery (e.g., ["completed_views"] for a CTV CPCV buy). Sellers MUST silently exclude products whose reporting_capabilities.available_metrics is not a superset (filter-not-fail; do not return an error). Derived ratios (completion_rate, ctr, roas, etc.) are satisfied when their underlying scalars are supported — buyers should list the underlying scalars.
  • by_package[].missing_metrics on get_media_buy_delivery — seller surfaces any metric the product's available_metrics advertised but did not populate in this report. Empty array (or absent) = clean delivery; non-empty = accountability breach. Derived ratios MUST NOT appear here.
  • measurement_accountability storyboard — exercises the lifecycle from discovery → buy → simulated delivery → delivery report. Schema-level contract enforced now; semantic enforcement (verifying sellers honestly populate missing_metrics) deferred to a follow-up that extends the test controller with metric-omission scenarios.

No additional field is added to create_media_buy — the product's declared available_metrics carries forward as the contract; measurement_terms and performance_standards already cover guarantee-level commitments at the package level.

Why minor

Two additive optional fields and one new storyboard. No existing seller behavior changes; reports without missing_metrics are interpreted as clean (field absent = no breach), and buyers omitting required_metrics see today's behavior.

Backwards compatibility

  • Existing sellers conformant without changes (both fields optional)
  • Buyers can adopt incrementally — required_metrics filter only fires when populated; missing_metrics consumers can ignore the field if not present

Hint kind follow-up

A dedicated metric_accountability_breach storyboard hint kind (with Diagnose/Locate/Fix/Verify formatter) is deferred to a follow-up @adcp/client PR. For now, breach is detectable via standard schema validation on the delivery response and the runner's field_present check on populated metrics.

Refs #3460.

Test plan

  • npm run build:schemas — clean
  • npm run test:schemas — 7/7 passed
  • npm run test:examples — 34/34 passed
  • npm run test:json-schema — 255/255 passed
  • npm run test:composed — 32/32 passed
  • npm run typecheck — clean
  • npm run build:compliance — storyboard lints clean (scoping, branch-set, contradiction, context-entity, auth-shape, test-kits, pagination)
  • precommit (test:unit, test:test-dynamic-imports, typecheck) — green
  • pre-push link check — green

🤖 Generated with Claude Code

bokelley and others added 2 commits April 28, 2026 14:40
Adds end-to-end metric accountability through the media buy lifecycle:

- `filters.required_metrics` on get_products lets buyers declare metric
  capability requirements at discovery (e.g., `["completed_views"]` for a
  CTV CPCV buy). Sellers MUST silently exclude products whose
  `reporting_capabilities.available_metrics` is not a superset
  (filter-not-fail; do not return an error). Derived ratios are satisfied
  by their underlying scalars.

- `by_package[].missing_metrics` on get_media_buy_delivery surfaces any
  metric the product's `available_metrics` advertised but the seller did
  not populate in this report. Empty (or absent) = clean; non-empty =
  accountability breach. Derived ratios MUST NOT appear here.

Adds a measurement_accountability storyboard exercising the lifecycle
from discovery through delivery. Schema-level contract is enforced now;
semantic enforcement (verifying the seller honestly populates
missing_metrics) is left to a follow-up test-controller extension.

Refs #3460.

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

Expert review surfaced an inconsistency in the carve-out: ratios like
`completion_rate` are listed in `aggregated_totals` (sellers ARE expected to
report them per `delivery-metrics.json`) but were excluded from
`missing_metrics` (so absence wasn't flagged). DSP UIs (TTD/DV360/Yahoo)
also surface these as first-class report fields, not derivations. Drop the
carve-out — `missing_metrics` is now the symmetric mirror of
`available_metrics`, no name-class exceptions. Buyers/sellers self-resolve
ratios via derivation when needed.

Tightens window-maturation language from "should" to MUST per RFC 2119
guidance — prevents false-positive accountability breaches during the live
window for broadcast/CTV channels with phased measurement (live → c3 → c7).

Adds binding-contract one-liner to `required_metrics` description and
`get_products.mdx`: the product's declared `available_metrics` is what
flows into the buy and computes `missing_metrics` at delivery time.

Tightens the storyboard with a `field_value_or_absent` check on
`missing_metrics` to actually assert empty/absent on the clean path.

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

Copy link
Copy Markdown
Contributor Author

Issue #3482 proposes filter_diagnostics on get-products-response.json — same surface as this PR; consider folding before merge or confirm follow-up.


Generated by Claude Code

@bokelley

Copy link
Copy Markdown
Contributor Author

Issue #3481 proposes package.committed_metrics: AvailableMetric[] on the create_media_buy response — a frozen snapshot of available_metrics at buy creation, so missing_metrics reconciles against the contract at buy time rather than the live product state. Same metric-accountability surface as this PR; consider folding before merge or confirm follow-up.


Generated by Claude Code

The test agent's simulate_delivery scenario only accepts impressions/spend/
clicks — not completed_views — so asserting field_present on it fails
against today's controller. Schema validation (response_schema) and the
missing_metrics empty-or-absent check still verify the contract surface.
Semantic enforcement (seller actually populates the metric) stays
deferred to the test-controller extension already tracked under #3460.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit dececcd into main Apr 28, 2026
16 of 17 checks passed
@bokelley bokelley deleted the bokelley/required-metrics-accountability branch April 28, 2026 23:54
bokelley added a commit that referenced this pull request Apr 29, 2026
…ts_sold, new_to_brand_units) (#3507)

* feat(schemas): five missing metric scalars (CPCV, CPM, downloads, units_sold, new_to_brand_units)

Adds five scalars production reporting carries today but had no enum entry:

- cost_per_completed_view — CTV CPCV pricing scalar
- cost_per_thousand — CPM (universal pricing scalar across CTV/display/
  audio/DOOH; conspicuous absence next to cost_per_click before this PR)
- downloads — IAB-standard scalar for audio/podcast (Podcast Measurement
  Technical Guidelines 2.x methodology); distinct from views
- units_sold — Retail-media commerce scalar; distinct from conversions
  (one transaction may carry multiple units)
- new_to_brand_units — Retail-media count of units sold to first-time
  buyers; parallel to existing new_to_brand_rate

All five added as type:number minimum:0 properties on delivery-metrics.json
plus enum entries on available-metric.json. Doc list in
optimization-reporting.mdx updated.

Closes the missing-scalars sub-item of #3460. Other sub-items already
resolved by recent merges:
- Closed-vs-open enum: resolved by #3492 (vendor-metric extensions)
- completion_rate derived ratio: resolved by drop-carve-out call in #3472
DBCFM cross-check remains as a human follow-up (David Porzelt).

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

* refactor(schemas): cost_per_thousand → cpm, drop brand names, tighten descriptions

Three review fixes per expert feedback on #3507:

1. Rename cost_per_thousand → cpm. Both protocol and product expert
   independently flagged that pricing-model.json:8 already uses the
   canonical "cpm" token; calling the scalar cost_per_thousand creates a
   translation mismatch. Operators say "CPM"; OpenRTB, GAM (eCPM),
   TTD all use cpm. Now buyers cross-walk pricing_model→reported scalar
   without a lookup table.

2. Remove real brand names from schema descriptions. delivery-metrics.json
   embedded a real list (Amazon Ads, Walmart Connect, Roundel, Sam's MAP,
   Instacart Ads, Target Roundel) that ships in the published spec —
   playbook policy bars real brand names in schemas. Replaced with
   neutral phrasing. (Bonus: Roundel IS Target's network, so the dup was
   confused.) Same for downloads (Megaphone/Art19/Spotify/Apple Podcasts
   list scrubbed).

3. Tighten descriptions:
   - cpm: fix operator-precedence prose to (spend / impressions) × 1000;
     expand channel list to include mobile/web video and native.
   - units_sold: note attribution windows are platform-specific; sellers
     SHOULD declare via measurement_windows or measurement_terms rather
     than encoding in the scalar name (no _14d/_30d sprawl).
   - new_to_brand_rate (existing field): clarify it's a fraction of
     conversions/transactions, distinguishing it from the new
     new_to_brand_units count.
   - new_to_brand_units: cross-reference the rate field for clarity.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 29, 2026
* feat(schemas): filter_diagnostics on get_products response

Adds optional filter_diagnostics block to get-products-response.json —
non-fatal observability for the filter-not-fail empty-result UX gap that
both pre-build expert reviewers independently flagged on PR #3472.

Shape: { total_candidates, excluded_by: { <filter_name>: { count, values?,
notes? } } }. Optional, additive, counts only (never product names) to
avoid competitive-intel leakage on adjacent campaigns or seller inventory.

Disambiguates "no inventory matches the brief" from "your required_metrics
filter excluded everything" without breaking the filter-not-fail
convention. Sellers still silently exclude unmatched products; this block
is observability, not error reporting.

Counting semantics intentionally loose — sellers vary on ANY-filter vs
ONLY-this-filter counting. Spec documents counts as approximate triage
signal, not exact accounting. Tightening would force every seller to
implement the same filter-evaluation ordering, which AdCP shouldn't
mandate.

Closes #3482.

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

* refactor(schemas): tighten filter_diagnostics — semantics field, closed envelope, typed values

Three review fixes per expert feedback on #3508:

1. Add semantics: "only" | "any" | "approximate" enum on the parent
   filter_diagnostics block. Both reviewers flagged that "approximate"
   alone isn't RFC-2119 enough — buyers writing automated triage will
   file tickets when arithmetic doesn't add up. Sellers self-declare
   how their counts are computed; buyers inspect before doing math.
   Recommended value is "only" (counts products that would have been
   included if not for this filter alone — the value that's actually
   useful for "which filter killed my result set").

2. Close the per-filter detail envelope (additionalProperties: false).
   Sellers wanting to extend should use the response-level ext field;
   the inner shape stays sealed.

3. Constrain values to oneOf: [{type:string},{type:object}] instead of
   the open items: {}. Each filter's value type is known; buyers can
   at least know they're dealing with strings or objects without a
   per-filter switch statement.

Plus: document that total_candidates and excluded_by are independently
optional — sellers whose baseline catalog size is sensitive MAY emit
excluded_by without total_candidates.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 29, 2026
…on create (#3510)

* feat(schemas): package.committed_metrics — frozen reporting contract 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>

* refactor(schemas): tighten committed_metrics — minItems, immutability, 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>

---------

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.

1 participant