Skip to content

feat: 3.1.0 follow-ups — impairment_propagation, swap recovery, cardinality#4685

Merged
bokelley merged 4 commits into
mainfrom
bokelley/v3-1-0-followups
May 17, 2026
Merged

feat: 3.1.0 follow-ups — impairment_propagation, swap recovery, cardinality#4685
bokelley merged 4 commits into
mainfrom
bokelley/v3-1-0-followups

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Three 3.1.0 follow-ups from the #4677 expert-review batch. All in the impairment.coherence cluster.

Issues closed

Commits

  1. feat(media-buy): impairment_propagation capability + health/impairments on get_media_buys response (#4683)

    • New capabilities.media_buy.impairment_propagation enum: snapshot (default), webhook_only, out_of_band. Sellers honestly declare how they propagate dependency impairments; webhook_only / out_of_band sellers grade not_applicable for impairment.coherence storyboards.
    • Surfaces health (media-buy-health enum, default ok) and impairments[] on get_media_buys response media_buys[] items. The fields existed on core/media-buy.json but weren't typed on the response schema — storyboards referencing media_buys[0].health previously failed the validations-path lint (the original storyboard: dependency-impact end-to-end scenario #2860 scenario slipped through admin-merge; this closes the gap).
    • Docs: lifecycle.mdx § Compliance extended with capability paragraph.
  2. feat(compliance): swap-assignment as canonical recovery for dependency_impairment (#4682)

    • Phase 5 of media_buy_seller/dependency_impairment rewritten to use update_media_buy with replacement-semantics creative_assignments — sync replacement creative B, swap the package's binding from A (rejected) to B (approved), verify health: ok and impairments[] cleared. Creative A's library status stays rejected but is no longer a dependency.
    • Models the production-canonical recovery (buyers ship a replacement asset, not a re-approval). Same-ID re-approval moves to an opt-in sibling scenario for sellers whose review flow supports reinstatement.
    • Scenario version bumped 1.0.0 → 2.0.0; required_tools adds update_media_buy.
  3. feat(compliance): dependency_impairment_cardinality scenario (#4681)

    • New scenario with two creatives on two packages. Sequential rejection takes cardinality 0 → 1 → 2; sequential swap-recovery takes it 2 → 1 → 0. Catches failure modes the single-creative base can't:
      • Wrong resource_id (right cardinality, wrong target).
      • Over-scoped package_ids when only one creative is rejected.
      • Failure to decrement on partial recovery.
      • Merged-entry sellers (one impairment with both creatives' package_ids when both are rejected).
    • Wired into protocols/media-buy/index.yaml#requires_scenarios.

Three changesets, all minor.

Test plan

  • build:schemas ✓ / build:compliance
  • test:schemas ✓ / test:examples
  • test:storyboard-validations-paths ✓ for the new scenarios (the dependency_impairment.yaml violations introduced by docs+compliance: terminal-buy carve-out (#4635) + dependency-impairment storyboard (#2860) #4677 are also fixed by the get-media-buys-response schema additions in this PR)
  • test:storyboard-{doc-parity,check-enum,branch-sets,test-kits,sample-request-schema,response-schema,context-entity,contradictions,provides-state-for,auth-shape,advisory-expiry,raw-mode-required}
  • Pre-push storyboard matrix ✓ — both scenarios run cleanly against the training agent across all six tenants
  • typecheck / test:json-schema / test:composed / test:docs-nav
  • ⚠️ Three pre-existing failures on origin/main (unchanged by this PR): test:storyboard-scoping on sync_audiences, test:storyboard-context-output-paths on sales-guaranteed/index.yaml, test:storyboard-validations-paths on brand-rights/scenarios/governance_denied.yaml.

Rollout

  • All three changes are additive. Existing sellers default to impairment_propagation: snapshot (current behavior). The new health/impairments fields on the response are optional. The cardinality scenario is new and gracefully grades not_applicable for sellers without multi-package or controller support.
  • Runtime gating in the compliance runner (read the capability, grade not_applicable accordingly) is the adcp-client follow-up; spec-side declaration ships here.
  • After this lands, all three storyboard scenarios (dependency_impairment base, dependency_impairment_cardinality) cleanly grade against the spec contract.

🤖 Generated with Claude Code

bokelley and others added 4 commits May 17, 2026 17:09
…ts on get_media_buys response (#4683)

Two additive spec changes:

1. New capability `capabilities.media_buy.impairment_propagation` with
   three postures — `snapshot` (default), `webhook_only`, `out_of_band` —
   so sellers can honestly declare how they surface dependency impairments.
   Sellers declaring `webhook_only` or `out_of_band` are not graded by
   the impairment.coherence storyboards; their compliance bar is the
   webhook contract or the offline agreement. Each posture is a legitimate
   operational pattern (premium guaranteed → snapshot; high-throughput SSPs
   → webhook_only; long-tail/enterprise-bundled → out_of_band).

2. Adds typed `health` (media-buy-health enum, default ok) and
   `impairments` (array of impairment.json) to get_media_buys response
   media_buys[] items. These fields existed on core/media-buy.json but
   weren't surfaced on the response schema, so storyboards referencing
   media_buys[0].health failed the validations-path lint. Brings the
   response schema in line with the published contract.

Runtime gating on the capability (storyboard runner grades not_applicable
based on the declared posture) is the adcp-client follow-up; spec-side
declaration ships here.

Closes #4683.

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

Rewrite phase 5 of media_buy_seller/dependency_impairment to use
update_media_buy with packages[].creative_assignments replacement
semantics instead of force_creative_status approved on the same id. In
production, rejected → approved on the same creative_id is uncommon;
buyers ship a corrected asset under a new id and update the package's
assignments. Modeling that uncommon path penalized sellers whose review
flow treats rejected as a hard wall (a legitimate design).

New phase 5 sequence:
  - sync replacement creative B (approved baseline)
  - force B to approved via comply_test_controller
  - update_media_buy: packages[$package].creative_assignments = [B]
  - get_media_buys: health: ok, impairments[] empty
    (creative A still rejected in library but no longer a buy dependency)

Same-id re-approval is filed as opt-in sibling scenario
media_buy_seller/dependency_impairment_reapprove_recovery for sellers
whose review flow supports the reinstatement path. Scenario version
bumped 1.0.0 → 2.0.0; required_tools adds update_media_buy.

Closes #4682.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pressure-test the impairment.coherence inverse rule under cardinality.
Two creatives on two packages, sequential rejection 0 → 1 → 2 → 1 → 0
via swap-assignment recovery. Catches failure modes the single-creative
scenario can't:

  - wrong resource_id (right cardinality, wrong target)
  - over-scoped package_ids when only one creative is rejected
  - failure to decrement impairments[] on partial recovery
  - merged-entry sellers (one impairment with both creatives' package_ids
    when both are rejected)

Wired into protocols/media-buy/index.yaml#requires_scenarios. Sellers
without comply_test_controller force_creative_status, multi-package
support, or update_media_buy with replacement-semantic creative_assignments
grade not_applicable. Same impairment_propagation capability gating as
the base scenario.

Closes #4681.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three rounds of expert review (protocol, docs, product). Must-fix items:

- Cardinality scenario: drop unsound `field_value_or_absent value: null`
  cardinality assertions. The check kind passes when the field is absent OR
  equals the value, so a seller emitting a literal-null pad in the array
  silently passes (protocol expert flag). Cardinality enforcement falls
  back to the cross-resource invariant (over-emission produces an
  impairment pointing at a non-offline resource, failing the forward
  rule). A direct `array_length` check kind in the runner is filed as a
  follow-up.

- Capability description (get-adcp-capabilities-response.json): make it
  explicit that `snapshot` is compatible with webhooks — the field is
  about whether the buy snapshot mirrors the impairment, not whether
  webhooks fire. The webhook channel is orthogonal. Mirror the
  clarification on the lifecycle.mdx paragraph.

- Docs voice: replace "declare honestly" / "tend toward" with neutral
  observational framing.

- Cross-link: lifecycle.mdx capability paragraph now links to the schema
  URL so an agent reader reaches the enum definition in one hop.

Deferred to follow-ups (filed separately on this PR's branch before push):
- snapshot_and_webhook combinator (common case missing from current taxonomy)
- out_of_band semantic split (API-mapping-gap vs no-API-at-all)
- rejection_reason + same-gap audit on get_media_buys response schema
- Materiality SHOULD vs MUST tension on impairment.json
- array_length check kind in the compliance runner (adcp-client)
- Cardinality gating: 3.1 vs 3.2 vs separate premium tier
- webhook_only minimum recovery surface
- Swap-recovery uneven across SSP categories (programmatic SSPs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 8650fb0 into main May 17, 2026
23 of 24 checks passed
@bokelley bokelley deleted the bokelley/v3-1-0-followups branch May 17, 2026 21:40
bokelley added a commit to adcontextprotocol/adcp-client that referenced this pull request May 17, 2026
…tions (#1830) (#1831)

* feat(storyboard): add `array_length` check kind for cardinality assertions (#1830)

Adds a dedicated cardinality check so storyboards can assert "exactly N
entries" without the unsound `field_present arr[N-1]` + `field_value_or_absent
arr[N] value: null` workaround — the second clause passes when a seller
emits a literal `null` pad at `arr[N]`.

Supports exact-count (`value: N`) and inclusive bounds (`min` / `max`,
either or both). Mutually exclusive — specifying both forms or neither
fails the check as misconfigured. Non-array resolutions fail with a type
error rather than passing silently.

Wired through both the runtime validator and the in-process
`conformance-replay` harness.

Follow-up to adcontextprotocol/adcp#4685 protocol review.

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

* fix(storyboard): tighten array_length operand validation + replay symmetry

Code-review follow-ups for #1830:

- Reject NaN, fractional, and negative `value` / `min` / `max` at the
  config gate. Previously these slipped through `typeof === 'number'`
  and silently failed every real input (NaN === N is always false;
  fractional `value` never matches `array.length`).
- Reject impossible ranges (`min > max`) at the config gate.
- `conformance-replay` was missing the canonical's `value` + `min`/`max`
  mutual-exclusion guard — a misconfigured storyboard could silently
  pass the replay harness while failing the runtime validator. Add the
  symmetric guard plus the same operand validation so replay tracks
  canonical semantics.
- Align replay's not-an-array message to distinguish `null` (which
  `typeof` mislabels as `'object'`) from real objects.
- JSDoc on the union variant now states this check is non-optional by
  design.

Adds four tests for NaN, fractional, negative-min, and impossible-range
misconfig paths.

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
…#4686, #4687) (#4690)

* feat(media-buy): restructure impairment_propagation as propagation_surfaces array (#4686)

Replace the single-value enum capabilities.media_buy.impairment_propagation
with the non-exclusive array capabilities.media_buy.propagation_surfaces.
The enum couldn't express the common case where a seller propagates on both
the buy snapshot AND fires webhooks — premium guaranteed sellers (GAM,
FreeWheel, CM360) do both, but the previous enum forced a choice between
'snapshot' and 'webhook_only', creating an incentive to declare
'webhook_only' and dodge the snapshot-coherence bar.

Surface values:
  snapshot    — media_buy.health + impairments[] mirror on get_media_buys
  webhook     — notification-type: impairment fires via
                push_notification_config
  out_of_band — channels outside AdCP entirely (email, dashboard,
                partner-specific feeds)

Default when absent: ['snapshot']. Storyboard gating: scenarios grade
not_applicable when propagation_surfaces does not include 'snapshot'.

Pre-release breaking change — the original enum landed in #4685 but
3.1.0 hasn't released yet (GA 2026-05-29), so no deprecation cycle.

Also documents the out_of_band semantic split (proper out-of-band vs
mapping-gap-disguised-as-out-of-band) and prescribes documenting mappings
rather than claiming out_of_band when the seller does have the data
under a non-AdCP field name.

Closes #4686.

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

* fix(media-buy): surface rejection_reason on get_media_buys response items (#4687)

The field existed on core/media-buy.json but wasn't typed on the
get-media-buys-response.json items — same gap-class as health +
impairments[] fixed in #4685.

Completed the full audit of core/media-buy.json against the response
schema: every other field (media_buy_id, account, status, health,
impairments, confirmed_at, cancellation, total_budget, packages,
invoice_recipient, creative_deadline, revision, created_at, updated_at,
ext) is already mirrored. With rejection_reason added, the response is
complete with respect to the canonical media-buy fields. Response-only
fields (currency, start_time, end_time, valid_actions,
available_actions, history, augmented packages with delivery snapshots)
remain — deliberately response-side, not on core/media-buy.json.

Closes #4687.

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

* fix(compliance): expert review feedback on #4690

Three expert reviews (protocol, docs, product). Must-fix items:

- Drop real-brand names from schema description and lifecycle.mdx
  (GAM/FreeWheel/CM360). Replaced with 'premium guaranteed sellers'
  generic descriptor. Playbook rule (no real brands in examples).

- Add explicit 'empty array invalid' clarification to the
  propagation_surfaces description — minItems: 1 already enforces it,
  but readers don't always check the constraint block.

- Fix stale adcp#4683 referent in dependency_impairment.yaml — the
  capability is no longer in #4683's shape; lifecycle.mdx is the live
  cross-reference and is sufficient on its own.

Deferred (filed separately):
- proprietary_field guidance follow-up so the description's SHOULD
  directive doesn't get lost.
- Release notes callout for the value rename webhook_only → webhook in
  3.1.0 GA notes.

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
… (#4733)

When a creative referenced by a media buy's package transitions to rejected,
the buy now surfaces `health: impaired` and an `impairments[]` entry. When the
buyer recovers via assignment swap (update_media_buy replacing the offline
creative with an approved sibling), the impairment clears and health returns
to ok. Closes the dependency_impairment + dependency_impairment_cardinality
storyboards added in #4677/#4685.

- types.ts: Impairment interface + MediaBuyState.impairments
- comply-test-controller.ts: propagateCreativeImpairment walks session.mediaBuys
  on forceCreativeStatus, appending on approved→rejected and removing on the
  inverse; idempotent on re-emission
- task-handlers.ts:
  - get_media_buys emits health + impairments mapped to wire shape
  - update_media_buy recomputes impairments after creative_assignments
    replacement; drops any whose resource_id is no longer referenced
- tenants/comply.ts: wire force_creative_status adapter on /sales (was missing,
  storyboards reported force_scenario_unsupported)

Scenario fixes:
- Both dependency_impairment storyboards restructure sync_creatives + inline
  assignments into sync_creatives + update_media_buy. v6 SalesPlatform.syncCreatives
  doesn't expose request-level assignments to the platform (filed upstream at
  adcp-client#1842). Both surfaces are spec-canonical; this is a v6-SDK
  compatibility choice, not a deprecation.
- dependency_impairment_cardinality also needs bid_price: 10.0 on its packages
  because its brief lands on an auction pricing_option as products[0].pricing_options[0]
  (test-design issue filed at #4732).

Sales floor lifts from 72:340 to 74:380.

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