Skip to content

feat(schemas): measurement capability block + brand.json metric_categories (#3612)#3652

Merged
bokelley merged 8 commits into
mainfrom
bokelley/measurement-capability
May 1, 2026
Merged

feat(schemas): measurement capability block + brand.json metric_categories (#3612)#3652
bokelley merged 8 commits into
mainfrom
bokelley/measurement-capability

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented Apr 30, 2026

Summary

Adds a measurement capability block to get_adcp_capabilities so measurement agents self-describe their per-metric catalog — the same pattern every other AdCP agent type already uses (sales/creative/governance/brand/buying/signals/rights). Adds optional metric_categories[] to brand.json's brand_agent_entry, paralleling rights agents' available_uses[] / right_types[] for coarse pre-call filtering by AAO.

Closes #3612 (the protocol surface piece of #3586's per-metric catalog discovery).
Unblocks the buyer-proposed vendor-metric flow in #3576 — buyers can now know which vendor metrics to propose on package-request.committed_metrics. Without this PR, the request-side vendor-scoped entries land partially blind.
Unblocks #3613 (AAO crawler + index implementation) — schema for the data the crawler ingests.

The architectural anchor

Every AdCP agent type publishes capabilities at the agent itself via get_adcp_capabilities. brand.json's agents[] array is just the directory; capability info lives at the agent. The one partial exception is rights agents, which put available_uses[] and right_types[] on brand.json for coarse filtering — full pricing/terms still come from the live agent.

This PR follows the rights-agent pattern for measurement — and per protocol-expert feedback, models the capability as a block (presence = support) rather than a supported_protocols value. Measurement agents have one surface (this catalog), not a tool-set with mandatory tasks the way media_buy / signals / governance do. Same precedent as compliance_testing / webhook_signing blocks.

Layer Where Purpose
Coarse filter brand.json agents[type='measurement'].metric_categories[] Cheap directory query — "any attention vendor?"
Canonical catalog Agent's get_adcp_capabilities.measurement.metrics[] Full per-metric data with id, category, standard, accreditations, unit, methodology
Cross-vendor index AAO /api/registry/measurement-vendors Crawled aggregation of capability responses on a TTL (separate PR, #3613)

Schemas added

  • enums/measurement-category.json — closed 12-value enum (attention, viewability, invalid_traffic, brand_safety, brand_lift, incrementality, audience, reach, creative_quality, emissions, outcomes, other). Includes viewability/invalid_traffic/brand_safety (MRC/TAG/GARM) per expert review.
  • get-adcp-capabilities-response.json — new measurement block with metrics[]. Each metric carries metric_id and category (required), plus optional standard_reference, accreditations[] (third-party certification — distinct from standard_reference), unit, description, methodology_url, methodology_version. additionalProperties: false with explicit ext slot. uniqueItems: true on the array.
  • brand.json brand_agent_entry — optional metric_categories[] array.

Why accreditations[] is separate from standard_reference

A metric can implement a published standard (URL pointing at the spec) without holding independent third-party accreditation. Buyers asking "is this MRC-accredited?" need a structured answer that survives URL parsing — every vendor pasting the same MRC URL whether accredited or not gives a false signal of comparability. The split surfaces the distinction at the schema layer.

Doc updates

Worked example (locked into the test suite)

{
  "supported_protocols": ["sponsored_intelligence"],
  "measurement": {
    "metrics": [
      {
        "metric_id": "attention_units",
        "category": "attention",
        "standard_reference": "https://iabtechlab.com/standards/attention-measurement",
        "accreditations": [
          {
            "accrediting_body": "MRC",
            "certification_id": "MRC-ATT-2026-001",
            "valid_until": "2027-12-31",
            "evidence_url": "https://mediaratingcouncil.org/accreditations/attentionvendor"
          }
        ],
        "unit": "score",
        "description": "Eye-tracking-based attention score (0-100). Computed from a panel of opted-in households.",
        "methodology_url": "https://attentionvendor.example/docs/attention-units",
        "methodology_version": "v2.1"
      }
    ]
  }
}

Backwards compatibility

All additions are optional and additive. Sellers without measurement capability are unchanged. Measurement vendors gain a structured catalog surface.

WG review

Hybrid design reached through the discussion thread on #3586#3612. Three independent expert reviews shaped the final shape: moved measurement out of supported_protocols (capability-block pattern), added missing categories, added methodology_version, added structured accreditations[], switched to additionalProperties: false with explicit ext, locked an example into the test suite, added uniqueItems: true on metrics[]. Labeled needs-wg-review.

Test plan

  • npm run build:schemas — clean
  • npm run test:schemas — 7/7
  • npm run test:examples36/36 (added 2: positive measurement example + negative duplicate-rejection)
  • npm run typecheck — clean

Closes #3612.

🤖 Generated with Claude Code

…ories (#3612)

Add a `measurement` capability block to get_adcp_capabilities so
measurement agents self-describe their per-metric catalog — the same
pattern every other AdCP agent type already uses
(sales/creative/governance/brand/buying/signals/rights). Adds optional
`metric_categories[]` to brand.json's brand_agent_entry, paralleling
rights agents' `available_uses[]` / `right_types[]` for coarse
pre-call filtering by AAO.

Schema additions:
- enums/measurement-category.json: 9-value closed enum (attention,
  brand_lift, incrementality, audience, reach, creative_quality,
  emissions, outcomes, other)
- protocol/get-adcp-capabilities-response.json: new measurement block
  with metrics[] (metric_id, category required; standard_reference,
  unit, description, documentation_url optional). Adds "measurement"
  to supported_protocols enum.
- brand.json brand_agent_entry: optional metric_categories[]

Closes #3612. Unblocks #3613 (AAO crawler + index implementation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley added the needs-wg-review Blocked on a working-group decision — surface in WG meeting agendas label Apr 30, 2026
bokelley and others added 5 commits April 30, 2026 06:47
Mirror governance.property_features[].methodology_url naming on the new
measurement.metrics[] block. Same field, same purpose, same name.
Reduces field-name drift across capability blocks for buyer agents
generating code from the schemas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent expert reviews (protocol/product/code) converged on
several fixes:

ARCHITECTURE — drop measurement from supported_protocols.
Per protocol expert: measurement isn't a tool-set with mandatory tasks
the way media_buy / signals / governance are; it's one discovery
surface. Following the compliance_testing / webhook_signing precedent,
the capability block's presence is the support signal — no companion
supported_protocols enum value, no compliance storyboard required.
Closes the "no measurement protocol storyboard exists" issue and
simplifies the architectural story.

ENUM — add three production-shipping categories that were missing.
Both protocol and product experts independently flagged: viewability
(MRC Viewable Impression Measurement Guidelines — IAS, DV, MOAT),
invalid_traffic (TAG/MRC IVT — HUMAN, DV, IAS), brand_safety (GARM
Brand Safety Floor + Suitability Framework). Without these, IAS/DV/
MOAT/HUMAN/GARM all fall back to "other" and the directory query
collapses for the most-shipping production categories.

ACCREDITATIONS — separate "implements a standard" from "third-party
certified."
Per product expert: standard_reference as free-form URL gives a false
signal of comparability — every vendor pastes the same MRC URL whether
accredited or not, and machine queries can't answer "show me MRC-
accredited vendors." Added structured accreditations[] array with
{ accrediting_body, certification_id?, valid_until?, evidence_url? }
shape. accrediting_body open string with examples (the global landscape
includes MRC/ARF/JIC bodies/ABC/BARB/AGOF — closed enum doesn't fit).

METHODOLOGY VERSION — added field to detect silent vendor methodology
changes.
Per protocol expert: vendors revising methodology silently is exactly
the trust problem standard_reference is meant to solve. methodology_
version optional; when present, buyer agents pin the contracted
version on committed_metrics; absence means buyer treats any change
as untracked.

PARITY — switched additionalProperties: true → false + explicit ext;
dropped description.maxLength: 1000 (governance has no cap); added USD
to unit examples (matches paired vendor-metric-value.json); moved
block placement to sit with peer protocol blocks before signing/
identity blocks; tightened "metric_id is the agent's identifier" prose.

STALE REFERENCES — fixed changeset's documentation_url references that
were left over from before the methodology_url rename. Fixed
[MeasurementCategory](#) dead anchor in docs.

DISCOVERY-VS-RATE-CARD CALLOUT — added explicit "this is a discovery
surface, not a rate card" paragraph in docs per product expert: pricing
per impression, minimum measurable inventory, attribution windows, geo
coverage, and SLAs are negotiated per buy via measurement_terms, not
through this catalog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three pre-merge polish items per @bokelley feedback:

1. uniqueItems: true on measurement.metrics[] in
   get-adcp-capabilities-response.json. Duplicate metric_id within one
   vendor's catalog is unambiguously a vendor-side bug; cheap to enforce
   at schema level since metrics[] entries don't carry the BrandRef-tuple
   uniqueness concern that prevented uniqueItems on committed_metrics /
   missing_metrics elsewhere.

2. Locked-down example for the new capability shape in tests/
   example-validation-simple.test.cjs. Two metrics (attention with full
   accreditations[]+methodology_version, emissions with the minimum
   required fields) plus a negative case proving the uniqueItems
   constraint actually rejects duplicates. Buyer-side implementers now
   have a canonical reference for the response shape before the WG locks
   it in. test:examples now 36/36 (was 34/34 — the two new tests are
   the positive + negative for measurement).

3. PR-body cross-reference to #3576 added separately on the GitHub PR
   to call out that this unblocks the buyer-proposed vendor-metric flow
   in #3576's request-side committed_metrics — without #3612, buyers
   can propose vendor metrics but have no way to discover which ones to
   propose.

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

Reverses the earlier "drop from supported_protocols" call. Measurement
will be a real protocol with its own task surface — reporting,
attribution, panel queries — even though only get_adcp_capabilities
ships in this PR. Putting measurement in supported_protocols now means
the slot is correct; future tasks fit in without a re-architecture.

Same model as every other protocol: creative is in supported_protocols
AND has a capability block; governance same. Measurement follows.

Cleanup paired:
- supported_protocols enum: re-add "measurement" with description
  acknowledging the protocol is in development
- enums/adcp-protocol.json: add "measurement" with kebab-case for
  cross-surface task categorization (was paired-enum gap protocol
  expert flagged in round 1)
- docs/protocol/get_adcp_capabilities.mdx: rewrite measurement section
  intro to acknowledge the protocol-in-development state
- tests: example uses ["measurement"] in supported_protocols (was
  faking it with ["sponsored_intelligence"] under the previous "block
  not protocol" call)
- changeset: rewrite the architectural rationale section

Solves the supported_protocols minItems:1 mismatch — measurement-only
agents now have an honest value to populate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`enums/adcp-protocol.json` gained `measurement` in the capability-block
PR; the matching TS const in `adcp-taxonomy.ts` had to follow or the
`adcp-taxonomy` enum-sync test fails CI.

BadgeRole stays narrow on purpose:
- migration 453 CHECK constraint excludes `measurement`
- no measurement specialism storyboards exist yet
- no `ROLE_LABELS` entry to render a measurement badge

`compliance-testing.ts` re-exports BadgeRole from `compliance-db.ts`
instead of aliasing it to `AdcpProtocol`, and `VALID_BADGE_ROLES` in
`badge-svg.ts` is an explicit literal mirroring that narrow list. When
measurement specialisms ship, that PR widens BadgeRole + the migration
+ ROLE_LABELS in lockstep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley and others added 2 commits April 30, 2026 13:15
…ck is the discovery surface

WG pushback on the rights-agent precedent: right_types meaningfully
partition who you'd ever call (a podcast buyer never wants CTV
rights), but measurement categories are correlative — buyers
typically want a basket (viewability + IVT + brand_safety travel
together), so a brand.json coarse-filter doesn't reliably narrow the
agent set. Capability blocks are queryable and cacheable; AAO crawls
them on a TTL anyway. The brand.json field added schema surface,
maintenance burden, and drift risk vs. the canonical catalog without
buying useful filtering.

Removes:
- `brand_agent_entry.metric_categories[]` from `static/schemas/source/brand.json`
- brand.json mentions from `enums/measurement-category.json` description
  (enum stays — still required on each metric in the capability block)
- "Coarse-filter precedent on brand.json" callout from
  `docs/protocol/get_adcp_capabilities.mdx`
- brand.json paragraph from `docs/registry/index.mdx` measurement-vendor section

Changeset rewritten to explain the rationale.

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

WG pushback on the closed category enum:

- Categories overlap (brand_safety measurement vs. governance's
  content_standards), making the boundary fuzzy
- No buyer-side discovery primitive consumes the field yet
- Enum already drifting (Pia flagged display-centric gaps: VCR,
  quartiles, share of voice, ad pod)
- metric_id + description + standard_reference + accreditations[]
  are already structured signal — AAO and buyer agents normalize
  from those without a coarse classification facet

Removes:
- `category` field (was required) on each metric in
  `protocol/get-adcp-capabilities-response.json`
- `enums/measurement-category.json` (no remaining $refs)
- Stale brand.json category prose in
  `core/reporting-capabilities.json` vendor_metrics description
- Test fixtures and capability-block example

Adds:
- Scope subsection in `docs/protocol/get_adcp_capabilities.mdx`
  spelling out what claiming `measurement` actually means
  (parallel to compliance_testing / webhook_signing)

If a category facet earns its keep once #3613's discovery primitive
lands, it can be added back as an open vendor-asserted string with
real query patterns shaping the taxonomy — rather than guessing
upfront.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 12bfb06 into main May 1, 2026
18 checks passed
@bokelley bokelley deleted the bokelley/measurement-capability branch May 1, 2026 02:07
bokelley added a commit that referenced this pull request May 1, 2026
…3613) (#3726)

* feat(registry): measurement-vendor discovery on /api/registry/agents (#3613)

Crawler ingests each measurement agent's get_adcp_capabilities.measurement
block (AdCP 3.x, schema #3652) and the public /api/registry/agents endpoint
gains three filters:

- metric_id=attention_units (exact, repeatable, JSONB containment)
- accreditation=MRC (exact, repeatable; verified_by_aao always false)
- q=attention (substring on metric_id; v1 scope only)

All three imply type=measurement; explicit non-measurement type returns 400.
Filtering at SQL level so no live fan-out per request. sources counts
recomputed against filtered set (sum(sources) === count invariant).

Crawler calls get_adcp_capabilities on agents that expose the tool, parses
the measurement block, and persists to new measurement_capabilities_json
JSONB column. 10s timeout. A measurement fetch failure does not fail the
whole discovery — other capability blocks still land normally.

Per security review:
- Per-field caps at write time (metrics ≤500, description ≤2000,
  metric_id ≤256, URI ≤2048, accreditations/metric ≤32). Reject — don't
  silently truncate — so failure is visible via discovery_error.
- Belt-and-braces 256KB DB CHECK on the column.
- Strip C0 controls + DEL (keep \t, \n).
- Reject <script / javascript: / data:text/html / inline event handlers
  after NFKC normalization.
- URI fields https-only in production.
- q rejects %/_ outright (substring search, not pattern); remaining ILIKE
  escaping uses ESCAPE '\\' (mirrors catalog-db.ts:353 pattern, not
  brand-db.ts which omits the explicit ESCAPE clause).

Closes #3613, closes #3614 (direct-call vs index doc folded into
docs/registry/index.mdx).

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

* fix(migrations): renumber duplicate 459 → 462 (resolves CI dup-migration block)

Two migrations landed on main with prefix 459 (PR #3672 09:14 UTC,
PR #3567 22:31 UTC) without renumbering. Subsequent PRs trip:

- `No duplicate migration numbers` workflow check
- `migrate.test.ts > has no duplicate migration version numbers on disk`
- `loadMigrations` validation in `Server integration tests` and
  `Built migrations against Postgres`

Renumber the later one (`459_create_type_reclassification_log` → 462)
since 460 (identities) and 461 (measurement_capabilities, this branch)
are already taken. Filename reference in
`type-reclassification-log-db.ts` updated to match.

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

* chore(measurement): clarify stripControlChars comment + add \r preservation test

Reviewer noticed the comment claimed "\r is stripped" but the regex
explicitly preserves 0x0D — exact intent was to keep all whitespace
controls (\t, \n, \r), strip everything else in C0 plus DEL. Comment
now matches code; test asserts \r survives.

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

needs-wg-review Blocked on a working-group decision — surface in WG meeting agendas

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Measurement-agent capability response: schema + spec

1 participant