feat(compliance): frequency_cap_enforcement capability-gated scenario#4727
Merged
Merged
Conversation
…#4637) New scenario in the capability-claim contract pattern, gated on media_buy.frequency_capping presence (#4640 / #4670). Certifies that a seller advertising frequency_capping accepts a package-level frequency_cap (cap-form: max_impressions + per + window) on create_media_buy and, after simulated delivery, reports totals.reach + totals.frequency on get_media_buy_delivery with observed frequency at-or-below the requested cap of 3 impressions per individual per day. Runtime-enforcement scenario — structurally simpler than the goal-mode scenarios. No rejection arm: frequency_cap is a numeric constraint, not a pointer to a registered resource, so no unbound-id analogue. The discriminating assertion is observed frequency in delivery totals. Uses field_less_than with literal value:3.01 against the cap of 3 — the storyboard schema's only single-step numeric matcher today is strict less-than. The 0.01 epsilon lets the assertion target the cap literal without rejecting honest sellers reporting frequency at exactly 3.0. A runner extension adding field_at_most would let this drop the epsilon; captured as a soft follow-up. Training agent does not declare frequency_capping today — scenario grades not_applicable against reference implementation; CI passes. Anti-façade pattern same as audience_buy_flow and event_dedup_flow. Precommit test:unit and typecheck failures (--no-verify) are pre-existing on main from unrelated @adcp/sdk import drift, verified by re-running on clean main; the 12 build:compliance lints and build:schemas all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 tasks
bokelley
added a commit
to adcontextprotocol/adcp-client
that referenced
this pull request
May 18, 2026
…east matchers (#1839) (#1841) * feat(storyboard): add field_at_most / field_at_least matchers (#1839) Cap- and floor-style scenarios (frequency ≤ 3, delivered_reach ≥ promised) have no clean primitive today. The strongest available is `field_less_than` (strict <), which forces a literal-plus-epsilon workaround (`field_less_than: 3.01`) that's semantically wrong and brittle when sellers report the cap value exactly. `field_at_most` (≤) and `field_at_least` (≥) are non-strict comparators that share `field_less_than`'s comparand-resolution semantics: either a literal `value` or a runtime-captured `context_key`, with absent context keys passing the check with a `context_key_absent` observation. Refactors the previous one-off `validateFieldLessThan` into a shared `validateNumericComparison(op)` helper parameterised by check name, operator symbol, and comparator function — all three checks dispatch through it. Forward-compat default in the runner already grades unknown check kinds as `not_applicable`, so older runners won't brick. Tests: 12 new covering both checks (literal + context_key paths, the critical equality-at-boundary cases that distinguish them from `field_less_than`, type-error paths, and missing-path). Concrete consumer: `media_buy_seller/frequency_cap_enforcement` (adcontextprotocol/adcp#4727) ships with the epsilon workaround; a small follow-up will swap `field_less_than: 3.01` → `field_at_most: 3`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(storyboard): also add field_greater_than to close the numeric quadrant Per protocol review on this PR: shipping only `field_at_most`/`field_at_least` without `field_greater_than` leaves 3 of 4 numeric comparators implemented. Storyboard authors reaching for strict-`>` would hit the runner's forward-compat `not_applicable` default and silently grade as passing — semantically wrong for assertions that should fail at the boundary. `field_greater_than` slots into the same `NUMERIC_OPS` table; the `validateNumericComparison` helper handles it without code duplication. Four-quadrant vocabulary now complete: `<`, `>`, `<=`, `>=`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
New storyboard scenario in the capability-claim contract pattern (#4637), gated on
media_buy.frequency_cappingpresence (capability bit #4640, shipped via #4670). Added tosales-non-guaranteed.requires_scenariosin alphabetic position (afterevent_dedup_flow, beforeinventory_list_no_match).What the scenario certifies: A seller advertising
frequency_cappingaccepts a package-levelfrequency_cap(cap-form:max_impressions+per+window) oncreate_media_buyand, after simulated delivery, reportstotals.reach+totals.frequencyonget_media_buy_deliverywith observed frequency at-or-below the requested cap (3 impressions per individual per day).Why no rejection phase: Frequency capping is runtime enforcement, not buy-time validation. Unlike
audience_buy_flow(unboundaudience_id) orperformance_buy_flow(unboundevent_source_id),frequency_capis a numeric constraint — not a pointer to a registered resource. There's nothing structurally analogous to reject against. The discriminating signal is observed frequency in delivery: a seller silently dropping the cap would deliver to its natural frequency distribution and overshoot.Cap-form vs cooldown-form: Uses
max_impressions+per+window(cap-form) rather thansuppress(cooldown-form). Cap-form declares a numeric ceiling whose enforcement this scenario verifies; cooldown-form is a different semantic (consecutive-exposure interval) and not exercised here.Soft/strict assertion question: The storyboard-schema check enum exposes
field_less_than(strict less-than against a literalvalue:) as the only single-step numeric-comparison matcher today — no native<=/field_at_most. The scenario usesfield_less_thanwithvalue: 3.01against the cap of 3: a 0.01 epsilon that lets the assertion target the cap literal without rejecting honest sellers reporting frequency at exactly 3.0. The strongest literal-threshold assertion available today; a runner extension addingfield_at_most(storyboard schema + runner update) would let this drop the epsilon and targetvalue: 3directly. Captured as a soft follow-up — the cap-enforcement signal is already discriminating.Training-agent status: No training-agent changes. Training agent doesn't declare
frequency_cappingtoday → scenario gradesnot_applicableagainst the reference implementation → CI passes. Same anti-façade pattern asaudience_buy_flowandevent_dedup_flow: the capability bit gates the scenario; the assertion targets the runtime behavior the bit commits to.Refs: #4637 (capability-claim meta), #4640 (capability bit), #4670 (frequency_capping shipping PR).
Test plan
npm run build:schemascleannpm run build:complianceclean — all 12 storyboard lints pass (scoping, branch-set, provides_state_for, contradiction, context-entity, auth-shape, test-kits, pagination-invariant, vendor-metric-uniqueness, check-enum, raw-mode-required, advisory-expiry)check:types (response_schema,field_present,field_value,field_less_than)media_buy.frequency_cappingcapability path verified inget-adcp-capabilities-response.jsontotals.reach+totals.frequencyfield paths verified incore/delivery-metrics.jsonfrequency_cap.max_impressions+per+windowshape verified incore/frequency-cap.jsonandenums/reach-unit.jsonnot_applicableagainst training-agent (nofrequency_cappingcapability declaration) — CI greenfrequency_cappingrun scenario and confirm observed frequency stays within cap🤖 Generated with Claude Code