Skip to content

feat(compliance): frequency_cap_enforcement capability-gated scenario#4727

Merged
bokelley merged 1 commit into
mainfrom
bokelley/4640-frequency-cap-enforcement-scenario
May 18, 2026
Merged

feat(compliance): frequency_cap_enforcement capability-gated scenario#4727
bokelley merged 1 commit into
mainfrom
bokelley/4640-frequency-cap-enforcement-scenario

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

New storyboard scenario in the capability-claim contract pattern (#4637), gated on media_buy.frequency_capping presence (capability bit #4640, shipped via #4670). Added to sales-non-guaranteed.requires_scenarios in alphabetic position (after event_dedup_flow, before inventory_list_no_match).

What the scenario certifies: 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 (3 impressions per individual per day).

Why no rejection phase: Frequency capping is runtime enforcement, not buy-time validation. Unlike audience_buy_flow (unbound audience_id) or performance_buy_flow (unbound event_source_id), frequency_cap is 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 than suppress (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 literal value:) as the only single-step numeric-comparison matcher today — no native <= / field_at_most. The scenario uses field_less_than with value: 3.01 against 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 adding field_at_most (storyboard schema + runner update) would let this drop the epsilon and target value: 3 directly. 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_capping today → scenario grades not_applicable against the reference implementation → CI passes. Same anti-façade pattern as audience_buy_flow and event_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:schemas clean
  • npm run build:compliance clean — 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)
  • Scenario YAML uses only enumerated check: types (response_schema, field_present, field_value, field_less_than)
  • media_buy.frequency_capping capability path verified in get-adcp-capabilities-response.json
  • totals.reach + totals.frequency field paths verified in core/delivery-metrics.json
  • frequency_cap.max_impressions + per + window shape verified in core/frequency-cap.json and enums/reach-unit.json
  • Scenario grades not_applicable against training-agent (no frequency_capping capability declaration) — CI green
  • Adopter verification — sellers declaring frequency_capping run scenario and confirm observed frequency stays within cap

🤖 Generated with Claude Code

…#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>
@bokelley bokelley merged commit 93e570b into main May 18, 2026
19 checks passed
@bokelley bokelley deleted the bokelley/4640-frequency-cap-enforcement-scenario branch May 18, 2026 09:57
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>
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