Skip to content

feat(compliance): performance_buy_flow scenario for capability-gated CPA buys (closes #4569)#4642

Merged
bokelley merged 2 commits into
mainfrom
bokelley/performance-sales-agent
May 17, 2026
Merged

feat(compliance): performance_buy_flow scenario for capability-gated CPA buys (closes #4569)#4642
bokelley merged 2 commits into
mainfrom
bokelley/performance-sales-agent

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

What it certifies

A non-guaranteed seller that claims conversion_tracking must demonstrate the dots actually connect end-to-end:

  1. sync_event_sources returns an event_source_id that's valid as a binding target on subsequent goals.
  2. create_media_buy with an event-kind optimization_goal (CPA target) referencing the registered source is accepted.
  3. create_media_buy referencing an unregistered event_source_id is rejected with INVALID_REQUEST and error.field set to the offending path. Silent acceptance is a façade — the seller cannot actually optimize against a source it doesn't know about.
  4. log_event against the bound source is forwarded upstream (anti-façade upstream_traffic check carrying the supplied identifier).
  5. get_media_buy_delivery returns first-class conversion metrics — conversions, cost_per_acquisition, and by_package[].by_creative[].conversions. Per-creative attribution is required; without it the buyer can't tell which creatives drove the goal.

What's deliberately NOT in scope

ROAS (target.kind: per_ad_spend) and value-max (target.kind: maximize_value) are NOT asserted. Many sellers — broadcast TV, upper-funnel video, signal-only — declare conversion_tracking honestly but don't compute return-on-ad-spend. Gating the CPA path on conversion_tracking presence is safe; gating ROAS the same way would over-claim. ROAS gets its own scenario gated on the supported_target_kinds capability bit proposed in #4639.

Pattern this establishes

This is the first scenario in the capability-claim contract pattern tracked under #4637: for every non-trivial capability a seller declares in get_adcp_capabilities or at the product level, ship a requires_capability-gated storyboard that proves the claim is honest end-to-end. The audit backlog (audience_buy_flow, event_dedup_flow, reach_buy_flow, etc.) follows this shape.

Test plan

  • npm run build:compliance passes all lints (storyboard scoping, branch-set, provides_state_for, contradictions, context-entity, auth-shape, test-kits, pagination-invariant, vendor-metric uniqueness, check-enum, raw-mode-required, advisory-expiry)
  • npm run typecheck clean (ran via pre-commit)
  • Scenario YAML follows the media_buy_seller category convention and references the canonical schema paths
  • All field_present paths verified against actual response schemas (delivery-metrics.json, get-media-buy-delivery-response.json)
  • WG review of the requires_capability gate shape on a presence-only object — this is the first scenario using present: true
  • Reference adopter (Pinnacle / Acme Outdoor test kit) dry-run

Refs

🤖 Generated with Claude Code

…CPA buys (#4569)

Adds media_buy_seller/performance_buy_flow under sales-non-guaranteed,
gated on media_buy.conversion_tracking presence via requires_capability
(present: true matcher landed in @adcp/client 7.6.0).

Certifies that a seller claiming conversion_tracking actually connects
the dots end-to-end: sync_event_sources → create_media_buy with event-kind
CPA goal → reject unbound source → log_event → attributed delivery with
per-creative conversion breakdown.

ROAS/maximize_value are deliberately out of scope (separate scenario
gated on #4639). First scenario in the capability-claim contract pattern
tracked under #4637.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes from PR #4642 review:

1. Rejection assertion bug — switched check: field_value from `matches:`
   (regex, not in storyboard-schema.yaml — would be silently ignored or
   reject the storyboard) to literal `value:` per core/error.json's
   JSONPath-lite contract.

2. Per-creative conversion attribution assertion dropped from delivery
   phase. Honest implementations vary widely on granularity (retail-media
   per-line, MMP per-campaign, CTV per-placement); requiring per-creative
   would fail honest sellers who advertise conversion_tracking. Will land
   as its own gated scenario under a sub-capability bit in follow-up.

Build clean: all 12 storyboard lints pass.

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

Close the two gaps that prevent the `media_buy_seller/performance_buy_flow`
storyboard scenario (#4642) from passing against the training agent.

- `handleCreateMediaBuy` validates `optimization_goals[].event_sources[].event_source_id`
  against the session's registered sources (via `findEventSourceInSession`,
  which falls back to a global scan to match `log_event` lookup semantics).
  Phantom ids are rejected with INVALID_REQUEST and `error.field` set to the
  literal JSONPath-lite shape (`packages[0].optimization_goals[0].event_sources[0].event_source_id`)
  the scenario asserts on. Silent acceptance would let a seller advertise
  `conversion_tracking` without being able to actually optimize against the
  event source the buyer thinks is bound.
- `get_media_buy_delivery` totals now carry `conversions` and
  `cost_per_acquisition = spend / conversions` when `simulate_delivery`
  injected both — matching the delivery contract for performance buys.
  Sellers that don't compute conversion metrics simply omit the fields
  (no conversions injected => no fields emitted).

Unit tests assert the rejection path with the literal JSONPath, the
success path against a registered source, and CPA emission / omission in
delivery reporting.

Refs #4569 (capability gating), #4637, #4642 (scenario PR).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 0d7452e into main May 17, 2026
19 checks passed
@bokelley bokelley deleted the bokelley/performance-sales-agent branch May 17, 2026 14:30
bokelley added a commit that referenced this pull request May 17, 2026
…ed scenarios (#4637) (#4664)

* feat(compliance): audience_buy_flow + event_dedup_flow scenarios + TA 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>

* fix(training-agent): register sync_audiences in /sales tenant catalog (PR 4664 CI fix)

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 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.

RFC: performance-sales specialism — is conversion-driven selling distinct from sales-social?

1 participant