Skip to content

feat(compliance): audience_buy_flow + event_dedup_flow capability-gated scenarios (#4637)#4664

Merged
bokelley merged 2 commits into
mainfrom
bokelley/audience-and-event-dedup-scenarios
May 17, 2026
Merged

feat(compliance): audience_buy_flow + event_dedup_flow capability-gated scenarios (#4637)#4664
bokelley merged 2 commits into
mainfrom
bokelley/audience-and-event-dedup-scenarios

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Two new capability-gated scenarios in the contract pattern (#4637), both added to sales-non-guaranteed.requires_scenarios, plus a training-agent fix that unblocks one of them.

media_buy_seller/audience_buy_flow

Gated on media_buy.audience_targeting presence (not on the audience-sync specialism — option B from the RFC chat: a DSP can accept buyer-uploaded CRM lists in targeting_overlay without claiming the full audience-sync discovery/deletion lifecycle). Certifies:

  1. sync_audiences returns an audience_id valid as a binding target in targeting_overlay.audience_include[].
  2. create_media_buy with targeting_overlay.audience_include referencing the registered audience is accepted.
  3. create_media_buy with audience_include referencing an unregistered id is rejected (INVALID_REQUEST with error.field set to the literal JSONPath-lite path packages[0].targeting_overlay.audience_include[0]) — silent acceptance would mean the seller cannot actually restrict delivery to the buyer's audience.
  4. Delivery reporting surfaces totals.impressions for the audience-targeted buy. Per-audience attribution breakdown is intentionally deferred (not first-class in delivery-metrics.json today).

Sibling to media_buy_seller/performance_buy_flow on the audience side — the unbound-id rejection is the discriminating assertion, modeled on the event_source_id contract from #4642.

media_buy_seller/event_dedup_flow

Gated on media_buy.conversion_tracking.multi_source_event_dedup equals true. Certifies that the same event_id from two registered event sources attributes to one conversion, not two:

  1. sync_event_sources accepts two sources (pixel + CAPI) sharing event_types.
  2. create_media_buy accepts an event-kind goal whose event_sources array contains BOTH ids.
  3. Two log_event calls with the SAME event_id from each source both succeed at the API level (dedup is attribution-time, not ingestion-time).
  4. get_media_buy_delivery reports totals.conversions equal to 1, not 2.

The training agent does not declare multi_source_event_dedup: true (verified in v6-sales-platform.ts), so this scenario grades not_applicable against the training agent in CI — no training-agent fix is needed. The scenario only runs against sellers that explicitly opt in.

Training-agent fix (audience_buy_flow only)

create_media_buy now rejects targeting_overlay.audience_include / audience_exclude entries whose audience_id was never registered via sync_audiences, with INVALID_REQUEST and error.field set to the literal JSONPath-lite path. Mirrors the event_source_id validation pattern from #4654. Implementation:

  • New server/src/training-agent/audience-handlers.ts with the in-memory store + handleSyncAudiences.
  • sync_audiences wired into the legacy /mcp HANDLER_MAP and the v6 /sales/mcp AudiencePlatform.
  • Validation block in handleCreateMediaBuy walking each package's targeting_overlay.audience_include[] and audience_exclude[] against the audience store.

Four unit tests in server/tests/unit/training-agent.test.ts cover the four contract paths: rejects/accepts × include/exclude.

Refs

Test plan

  • npm run build:schemas clean
  • npm run build:compliance — all 12 lints pass; new scenarios in dist/compliance/latest/protocols/media-buy/scenarios/
  • npx tsc --project server/tsconfig.json --noEmit clean
  • npx vitest run server/tests/unit/training-agent.test.ts — 376/376 pass (includes 4 new audience contract tests + updated tool-count assertion)
  • Storyboard grader run against the training agent — audience_buy_flow should grade passed, event_dedup_flow should grade not_applicable

Scenario design notes

  • error.field uses literal value: only, not matches: regex — field_value doesn't support regex and the field is JSONPath-lite per core/error.json.
  • No per-audience or per-event-source delivery breakdown asserted — neither is first-class in delivery-metrics.json today. Both will land as follow-up scenarios gated on sub-capability bits.
  • audience_exclude follows the same binding contract but is exercised only via unit tests, not a separate scenario phase — one positive + one negative on audience_include is the discriminating signal in YAML.
  • No modification of audience-sync specialism storyboard or its index.yaml.
  • No modification of performance_buy_flow.yaml — reference only.
  • The training agent intentionally does not declare multi_source_event_dedup — keeping it honest as a "doesn't claim what it can't do" reference is the right state.

🤖 Generated with Claude Code

… audience validation (#4637)

Two new capability-gated scenarios in the contract pattern (#4637), added to
sales-non-guaranteed.requires_scenarios:

- media_buy_seller/audience_buy_flow — gated on media_buy.audience_targeting
  presence. Certifies sync_audiences → bound audience_id in targeting →
  unbound id rejected → delivery against an audience-targeted buy. Sibling
  to performance_buy_flow on the audience side; the unbound-id rejection
  is the discriminating assertion. The literal error.field path on
  packages[0].targeting_overlay.audience_include[0] mirrors the
  event_source_id contract from #4642.

- media_buy_seller/event_dedup_flow — gated on
  media_buy.conversion_tracking.multi_source_event_dedup equals true.
  Certifies that the same event_id from two registered event sources
  attributes to one conversion, not two. Sellers without
  multi_source_event_dedup grade not_applicable — the bit gates the
  scenario; the cumulative-count assertion (1, not 2) is the contract.
  The training agent does not declare this bit, so it grades
  not_applicable here; no training-agent fix is needed for this scenario.

Training-agent fix: create_media_buy now rejects
targeting_overlay.audience_include / audience_exclude entries whose
audience_id was never registered via sync_audiences, with INVALID_REQUEST
and error.field set to the literal JSONPath-lite path. Mirrors the
event_source_id validation pattern from #4654. sync_audiences itself is
now wired through the training agent (legacy /mcp HANDLER_MAP and v6
/sales/mcp via AudiencePlatform) so adopters can run the audience scenario
against the reference implementation. Four unit tests cover the four
contract paths (accept/reject × include/exclude).

Three sibling product-level scenarios (reach, clicks, completed_views)
remain blocked on #4651 product-level capability gating RFC.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 67aaaac into main May 17, 2026
19 checks passed
@bokelley bokelley deleted the bokelley/audience-and-event-dedup-scenarios branch May 17, 2026 15:34
bokelley added a commit that referenced this pull request May 18, 2026
… contains: matcher (#4637) (#4722)

Four new storyboard scenarios in the capability-claim contract pattern, all
gated via the `contains:` matcher (shipped in @adcp/client 7.70 —
adcp-client#1817), all added to `sales-non-guaranteed.requires_scenarios`:

- `performance_buy_flow_roas` — gated on
  `media_buy.conversion_tracking.supported_targets` containing
  `per_ad_spend` (#4639). Certifies ROAS goal acceptance, rejection of
  per_ad_spend without value_field, and delivery surfaces
  `conversion_value` + `roas` alongside `conversions` + CPA.

- `reach_buy_flow` — gated on `media_buy.supported_optimization_metrics`
  containing `reach` (#4669). Certifies reach-unit binding, rejection of
  unsupported reach_unit, and delivery surfaces `reach` + `frequency`.

- `clicks_buy_flow` — gated on `media_buy.supported_optimization_metrics`
  containing `clicks` (#4669). Certifies click goal acceptance and
  delivery surfaces `clicks` + `cost_per_click`. No rejection arm —
  clicks is universal in semantics.

- `completed_views_buy_flow` — gated on
  `media_buy.supported_optimization_metrics` containing `completed_views`
  (#4669). Certifies view_duration_seconds binding, rejection of
  unsupported duration (per optimization-goal.json), and delivery
  surfaces `completed_views` + `completion_rate`.

All four grade `not_applicable` against the embedded training agent —
the training agent doesn't declare `supported_targets` or
`supported_optimization_metrics`. Same anti-façade hygiene as
`event_dedup_flow` (#4664): an agent that doesn't claim a capability is
not held to its scenario.

Refs: #4637, #4639, #4669, #4642, #4664, #4651, adcp-client#1817

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

Lands the deferred per-creative conversion attribution work from #4642
as a single PR with two coupled changes:

- New boolean `media_buy.conversion_tracking.per_creative_attribution`
  capability bit in get-adcp-capabilities-response.json. Optional;
  defaults to false. Declares that the seller can attribute conversions
  per creative within a package and surface them via
  media_buy_deliveries[].by_package[].by_creative[].conversions.

- New scenario media_buy_seller/per_creative_conversion_attribution,
  gated on the new bit, added to sales-non-guaranteed.requires_scenarios.
  Registers two creatives via sync_creatives, creates a media buy
  assigning both to one package with a CPA goal, logs two conversion
  events, simulates delivery, and asserts the by_creative[] breakdown
  is populated for both creatives (creative_id + conversions on rows
  0 and 1) — the second-row assertion is the asymmetry check that
  separates honest per-creative attribution from a single-row façade.

Closes the gap deliberately left by performance_buy_flow (#4642), whose
narrative defers per-creative attribution because honest adopters
report at differing granularities: social per-ad, retail-media per-line,
MMP-mediated per-campaign, broadcast/CTV per-placement. Requiring
per-creative in the base CPA scenario would have failed those honest
implementations. The bit gates the scenario; sellers that don't
advertise it grade not_applicable.

log_event's payload (core/event.json) does not carry creative_id —
attributing each event to a specific creative is the seller's internal
click / view-through correlation responsibility, not the buyer's. The
scenario logs two events with distinct event_ids and relies on the
seller's correlation to spread simulate_delivery's conversions across
the two assigned creatives in the by_creative[] breakdown.

No training-agent changes — the reference implementation does not
declare per_creative_attribution, so the scenario grades not_applicable
and CI passes. Same anti-façade pattern as event_dedup_flow (#4664)
and frequency_cap_enforcement (#4640).

Refs: #4725 (capability bit + scenario), #4637 (capability-claim meta),
#4642 (performance_buy_flow that deferred this), #4639 (supported_targets
bit for the sibling ROAS gate).

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

* feat(training-agent): metric-mode capability + validation + derived delivery for clicks/reach/completed_views

Declares seller-level `media_buy.supported_optimization_metrics` (honest
union across catalog products), validates `reach_unit` / `view_duration_seconds`
against product capabilities on create_media_buy, and emits `cost_per_click`
plus goal-gated `reach + frequency` / `completed_views + completion_rate`
on get_media_buy_delivery. Flips three capability-gated storyboards from
`not_applicable` to applicable: clicks_buy_flow, reach_buy_flow,
completed_views_buy_flow. Same forcing-function shape as #4654 and #4664.

Manual rollup pending adcp-client#1818 (SDK seller-level field exposure).

Refs: #4637, #4642, #4654, #4664, #4722.

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

* test(training-agent): add completed_views delivery emission + cost_per_click negative tests (PR 4766 review)

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