feat(compliance): performance_buy_flow scenario for capability-gated CPA buys (closes #4569)#4642
Merged
Merged
Conversation
…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>
Merged
5 tasks
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>
Merged
5 tasks
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>
6 tasks
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>
Merged
5 tasks
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>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
media_buy_seller/performance_buy_flow— the first capability-claim contract scenario formedia_buy.conversion_tracking. Gated on conversion_tracking presence viarequires_capability: present: true(runner support landed in @adcp/client 7.6.0).sales-non-guaranteed.requires_scenarios. Sellers without conversion_tracking gradenot_applicable, not failing.What it certifies
A non-guaranteed seller that claims
conversion_trackingmust demonstrate the dots actually connect end-to-end:sync_event_sourcesreturns anevent_source_idthat's valid as a binding target on subsequent goals.create_media_buywith an event-kindoptimization_goal(CPA target) referencing the registered source is accepted.create_media_buyreferencing an unregisteredevent_source_idis rejected withINVALID_REQUESTanderror.fieldset to the offending path. Silent acceptance is a façade — the seller cannot actually optimize against a source it doesn't know about.log_eventagainst the bound source is forwarded upstream (anti-façadeupstream_trafficcheck carrying the supplied identifier).get_media_buy_deliveryreturns first-class conversion metrics —conversions,cost_per_acquisition, andby_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 — declareconversion_trackinghonestly 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 thesupported_target_kindscapability 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_capabilitiesor at the product level, ship arequires_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:compliancepasses 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 typecheckclean (ran via pre-commit)media_buy_sellercategory convention and references the canonical schema pathsfield_presentpaths verified against actual response schemas (delivery-metrics.json,get-media-buy-delivery-response.json)requires_capabilitygate shape on a presence-only object — this is the first scenario usingpresent: trueRefs
requires_capability: present: truematcher (shipped in 7.6.0)🤖 Generated with Claude Code