@adcp/sdk 7.11.0 → 7.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/compliance/cache/3.1.0-rc.2/domains/brand/index.yaml +160 -0
  2. package/compliance/cache/3.1.0-rc.2/domains/brand/scenarios/distributed_brand_resolution.yaml +415 -0
  3. package/compliance/cache/3.1.0-rc.2/domains/brand/scenarios/single_side_trust_extension.yaml +454 -0
  4. package/compliance/cache/3.1.0-rc.2/domains/creative/index.yaml +339 -0
  5. package/compliance/cache/3.1.0-rc.2/domains/creative/scenarios/billing_out_of_band.yaml +153 -0
  6. package/compliance/cache/3.1.0-rc.2/domains/creative/scenarios/canonical_supported_formats.yaml +212 -0
  7. package/compliance/cache/3.1.0-rc.2/domains/creative/scenarios/creative_lifecycle_webhooks.yaml +389 -0
  8. package/compliance/cache/3.1.0-rc.2/domains/creative/scenarios/native_in_feed.yaml +543 -0
  9. package/compliance/cache/3.1.0-rc.2/domains/governance/index.yaml +682 -0
  10. package/compliance/cache/3.1.0-rc.2/domains/media-buy/index.yaml +789 -0
  11. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/audience_buy_flow.yaml +380 -0
  12. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/available_actions.yaml +565 -0
  13. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/billing_finality_delivery.yaml +354 -0
  14. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/canonical_formats.yaml +861 -0
  15. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/clicks_buy_flow.yaml +264 -0
  16. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/completed_views_buy_flow.yaml +344 -0
  17. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/create_media_buy_async.yaml +234 -0
  18. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/creative_fate_after_cancellation.yaml +419 -0
  19. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/creative_reception.yaml +247 -0
  20. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/delivery_reporting.yaml +357 -0
  21. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/dependency_impairment.yaml +633 -0
  22. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/dependency_impairment_cardinality.yaml +800 -0
  23. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/event_dedup_flow.yaml +399 -0
  24. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/frequency_cap_enforcement.yaml +309 -0
  25. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/governance_approved.yaml +214 -0
  26. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/governance_conditions.yaml +199 -0
  27. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/governance_denied.yaml +204 -0
  28. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/governance_denied_recovery.yaml +252 -0
  29. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/invalid_transitions.yaml +289 -0
  30. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/inventory_list_no_match.yaml +148 -0
  31. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/inventory_list_targeting.yaml +276 -0
  32. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/measurement_accountability.yaml +244 -0
  33. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/measurement_terms_rejected.yaml +203 -0
  34. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/package_correlation_legacy_fallback.yaml +113 -0
  35. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/pending_creatives_to_start.yaml +292 -0
  36. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/per_creative_conversion_attribution.yaml +500 -0
  37. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/performance_buy_flow.yaml +428 -0
  38. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/performance_buy_flow_roas.yaml +470 -0
  39. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/product_signal_targeting.yaml +373 -0
  40. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/proposal_finalize.yaml +399 -0
  41. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/proposal_finalize_asap_timing.yaml +264 -0
  42. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/proposal_not_found_errors.yaml +257 -0
  43. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/provenance_audit_observation.yaml +333 -0
  44. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/provenance_enforcement.yaml +517 -0
  45. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/provenance_truth_of_claim.yaml +294 -0
  46. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/reach_buy_flow.yaml +823 -0
  47. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/refine_finalize_exclusivity.yaml +360 -0
  48. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/refine_products.yaml +148 -0
  49. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/vendor_metric_accountability.yaml +293 -0
  50. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/vendor_metric_catalog_precondition.yaml +307 -0
  51. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/vendor_metric_optimization_flow.yaml +576 -0
  52. package/compliance/cache/3.1.0-rc.2/domains/media-buy/state-machine.yaml +442 -0
  53. package/compliance/cache/3.1.0-rc.2/domains/signals/index.yaml +266 -0
  54. package/compliance/cache/3.1.0-rc.2/domains/sponsored-intelligence/index.yaml +256 -0
  55. package/compliance/cache/3.1.0-rc.2/index.json +356 -0
  56. package/compliance/cache/3.1.0-rc.2/protocols/brand/index.yaml +160 -0
  57. package/compliance/cache/3.1.0-rc.2/protocols/brand/scenarios/distributed_brand_resolution.yaml +415 -0
  58. package/compliance/cache/3.1.0-rc.2/protocols/brand/scenarios/single_side_trust_extension.yaml +454 -0
  59. package/compliance/cache/3.1.0-rc.2/protocols/creative/index.yaml +339 -0
  60. package/compliance/cache/3.1.0-rc.2/protocols/creative/scenarios/billing_out_of_band.yaml +153 -0
  61. package/compliance/cache/3.1.0-rc.2/protocols/creative/scenarios/canonical_supported_formats.yaml +212 -0
  62. package/compliance/cache/3.1.0-rc.2/protocols/creative/scenarios/creative_lifecycle_webhooks.yaml +389 -0
  63. package/compliance/cache/3.1.0-rc.2/protocols/creative/scenarios/native_in_feed.yaml +543 -0
  64. package/compliance/cache/3.1.0-rc.2/protocols/governance/index.yaml +682 -0
  65. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/index.yaml +789 -0
  66. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/audience_buy_flow.yaml +380 -0
  67. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/available_actions.yaml +565 -0
  68. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/billing_finality_delivery.yaml +354 -0
  69. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/canonical_formats.yaml +861 -0
  70. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/clicks_buy_flow.yaml +264 -0
  71. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/completed_views_buy_flow.yaml +344 -0
  72. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/create_media_buy_async.yaml +234 -0
  73. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml +419 -0
  74. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/creative_reception.yaml +247 -0
  75. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/delivery_reporting.yaml +357 -0
  76. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/dependency_impairment.yaml +633 -0
  77. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/dependency_impairment_cardinality.yaml +800 -0
  78. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/event_dedup_flow.yaml +399 -0
  79. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/frequency_cap_enforcement.yaml +309 -0
  80. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/governance_approved.yaml +214 -0
  81. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/governance_conditions.yaml +199 -0
  82. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/governance_denied.yaml +204 -0
  83. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/governance_denied_recovery.yaml +252 -0
  84. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/invalid_transitions.yaml +289 -0
  85. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/inventory_list_no_match.yaml +148 -0
  86. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/inventory_list_targeting.yaml +276 -0
  87. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/measurement_accountability.yaml +244 -0
  88. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/measurement_terms_rejected.yaml +203 -0
  89. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/package_correlation_legacy_fallback.yaml +113 -0
  90. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/pending_creatives_to_start.yaml +292 -0
  91. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/per_creative_conversion_attribution.yaml +500 -0
  92. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/performance_buy_flow.yaml +428 -0
  93. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/performance_buy_flow_roas.yaml +470 -0
  94. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/product_signal_targeting.yaml +373 -0
  95. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/proposal_finalize.yaml +399 -0
  96. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/proposal_finalize_asap_timing.yaml +264 -0
  97. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/proposal_not_found_errors.yaml +257 -0
  98. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/provenance_audit_observation.yaml +333 -0
  99. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/provenance_enforcement.yaml +517 -0
  100. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/provenance_truth_of_claim.yaml +294 -0
  101. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/reach_buy_flow.yaml +823 -0
  102. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/refine_finalize_exclusivity.yaml +360 -0
  103. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/refine_products.yaml +148 -0
  104. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/vendor_metric_accountability.yaml +293 -0
  105. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/vendor_metric_catalog_precondition.yaml +307 -0
  106. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/vendor_metric_optimization_flow.yaml +576 -0
  107. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/state-machine.yaml +442 -0
  108. package/compliance/cache/3.1.0-rc.2/protocols/signals/index.yaml +266 -0
  109. package/compliance/cache/3.1.0-rc.2/protocols/sponsored-intelligence/index.yaml +256 -0
  110. package/compliance/cache/3.1.0-rc.2/specialisms/audience-sync/index.yaml +313 -0
  111. package/compliance/cache/3.1.0-rc.2/specialisms/brand-rights/index.yaml +350 -0
  112. package/compliance/cache/3.1.0-rc.2/specialisms/brand-rights/scenarios/governance_denied.yaml +226 -0
  113. package/compliance/cache/3.1.0-rc.2/specialisms/collection-lists/index.yaml +359 -0
  114. package/compliance/cache/3.1.0-rc.2/specialisms/content-standards/index.yaml +572 -0
  115. package/compliance/cache/3.1.0-rc.2/specialisms/creative-ad-server/index.yaml +409 -0
  116. package/compliance/cache/3.1.0-rc.2/specialisms/creative-generative/generative-seller.yaml +807 -0
  117. package/compliance/cache/3.1.0-rc.2/specialisms/creative-generative/index.yaml +758 -0
  118. package/compliance/cache/3.1.0-rc.2/specialisms/creative-template/index.yaml +510 -0
  119. package/compliance/cache/3.1.0-rc.2/specialisms/governance-aware-seller/index.yaml +143 -0
  120. package/compliance/cache/3.1.0-rc.2/specialisms/governance-aware-seller/scenarios/governance_multi_agent_rejected.yaml +117 -0
  121. package/compliance/cache/3.1.0-rc.2/specialisms/governance-delivery-monitor/index.yaml +441 -0
  122. package/compliance/cache/3.1.0-rc.2/specialisms/governance-spend-authority/denied.yaml +221 -0
  123. package/compliance/cache/3.1.0-rc.2/specialisms/governance-spend-authority/index.yaml +330 -0
  124. package/compliance/cache/3.1.0-rc.2/specialisms/property-lists/index.yaml +482 -0
  125. package/compliance/cache/3.1.0-rc.2/specialisms/sales-broadcast-tv/index.yaml +738 -0
  126. package/compliance/cache/3.1.0-rc.2/specialisms/sales-catalog-driven/index.yaml +840 -0
  127. package/compliance/cache/3.1.0-rc.2/specialisms/sales-guaranteed/index.yaml +601 -0
  128. package/compliance/cache/3.1.0-rc.2/specialisms/sales-non-guaranteed/index.yaml +546 -0
  129. package/compliance/cache/3.1.0-rc.2/specialisms/sales-proposal-mode/index.yaml +586 -0
  130. package/compliance/cache/3.1.0-rc.2/specialisms/sales-social/index.yaml +919 -0
  131. package/compliance/cache/3.1.0-rc.2/specialisms/signal-marketplace/index.yaml +424 -0
  132. package/compliance/cache/3.1.0-rc.2/specialisms/signal-marketplace/scenarios/governance_denied.yaml +210 -0
  133. package/compliance/cache/3.1.0-rc.2/specialisms/signal-owned/index.yaml +317 -0
  134. package/compliance/cache/3.1.0-rc.2/specialisms/sponsored-intelligence/index.yaml +59 -0
  135. package/compliance/cache/3.1.0-rc.2/test-kits/acme-outdoor-live.yaml +78 -0
  136. package/compliance/cache/3.1.0-rc.2/test-kits/acme-outdoor.yaml +223 -0
  137. package/compliance/cache/3.1.0-rc.2/test-kits/billing-gate-runner.yaml +115 -0
  138. package/compliance/cache/3.1.0-rc.2/test-kits/bistro-oranje.yaml +126 -0
  139. package/compliance/cache/3.1.0-rc.2/test-kits/distributed-brand-runner.yaml +281 -0
  140. package/compliance/cache/3.1.0-rc.2/test-kits/nova-motors.yaml +262 -0
  141. package/compliance/cache/3.1.0-rc.2/test-kits/osei-natural.yaml +126 -0
  142. package/compliance/cache/3.1.0-rc.2/test-kits/parallel-dispatch-runner.yaml +196 -0
  143. package/compliance/cache/3.1.0-rc.2/test-kits/rate-limit-trip-runner.yaml +172 -0
  144. package/compliance/cache/3.1.0-rc.2/test-kits/signed-requests-runner.yaml +155 -0
  145. package/compliance/cache/3.1.0-rc.2/test-kits/single-side-trust-runner.yaml +294 -0
  146. package/compliance/cache/3.1.0-rc.2/test-kits/substitution-observer-runner.yaml +688 -0
  147. package/compliance/cache/3.1.0-rc.2/test-kits/summit-foods.yaml +125 -0
  148. package/compliance/cache/3.1.0-rc.2/test-kits/webhook-receiver-runner.yaml +265 -0
  149. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/001-minimal-plan.json +43 -0
  150. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/002-full-plan.json +217 -0
  151. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/003-bookkeeping-stripped.json +60 -0
  152. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/004a-human-review-omitted.json +43 -0
  153. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/004b-human-review-explicit-null.json +49 -0
  154. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/005a-policy-categories-order-1.json +53 -0
  155. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/005b-policy-categories-order-2.json +57 -0
  156. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/006a-ext-trace-v1.json +49 -0
  157. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/006b-ext-trace-v2.json +53 -0
  158. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/007-unicode-objectives.json +43 -0
  159. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/008-numeric-canonicalization.json +65 -0
  160. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/README.md +220 -0
  161. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/canonicalization.json +241 -0
  162. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/keys.json +60 -0
  163. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/001-no-signature-header.json +24 -0
  164. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/002-wrong-tag.json +26 -0
  165. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/003-expired-signature.json +26 -0
  166. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/004-window-too-long.json +26 -0
  167. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/005-alg-not-allowed.json +26 -0
  168. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/006-missing-covered-component.json +26 -0
  169. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/007-missing-content-digest.json +26 -0
  170. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/008-unknown-keyid.json +26 -0
  171. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/009-key-ops-missing-verify.json +27 -0
  172. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/010-content-digest-mismatch.json +33 -0
  173. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/011-malformed-header.json +27 -0
  174. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/012-missing-expires-param.json +26 -0
  175. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/013-expires-le-created.json +27 -0
  176. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/014-missing-nonce-param.json +27 -0
  177. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/015-signature-invalid.json +28 -0
  178. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/016-replayed-nonce.json +35 -0
  179. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/017-key-revoked.json +38 -0
  180. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/018-digest-covered-when-forbidden.json +28 -0
  181. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/019-signature-without-signature-input.json +26 -0
  182. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/020-rate-abuse.json +34 -0
  183. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/021-duplicate-signature-input-label.json +31 -0
  184. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/022-multi-valued-content-type.json +31 -0
  185. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/023-multi-valued-content-digest.json +32 -0
  186. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/024-unquoted-string-param.json +31 -0
  187. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/025-jwk-alg-crv-mismatch.json +43 -0
  188. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/026-non-ascii-host.json +31 -0
  189. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/027-webhook-registration-authentication-unsigned.json +25 -0
  190. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/028-unsigned-protocol-method-required.json +26 -0
  191. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/001-basic-post.json +30 -0
  192. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/002-post-with-content-digest.json +31 -0
  193. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/003-es256-post.json +30 -0
  194. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/004-multiple-signature-labels.json +26 -0
  195. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/005-default-port-stripped.json +30 -0
  196. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/006-dot-segment-path.json +30 -0
  197. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/007-query-byte-preserved.json +30 -0
  198. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/008-percent-encoded-path.json +30 -0
  199. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/009-percent-encoded-unreserved-decoded.json +30 -0
  200. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/010-percent-encoded-slash-preserved.json +30 -0
  201. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/011-ipv6-authority.json +30 -0
  202. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/012-ipv6-authority-default-port-stripped.json +30 -0
  203. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/README.md +211 -0
  204. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/keys.json +61 -0
  205. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/001-wrong-tag.json +26 -0
  206. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/002-expired-signature.json +26 -0
  207. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/003-window-too-long.json +26 -0
  208. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/004-alg-not-allowed.json +26 -0
  209. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/005-missing-authority-component.json +26 -0
  210. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/006-missing-content-digest.json +25 -0
  211. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/007-unknown-keyid.json +26 -0
  212. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/008-wrong-adcp-use.json +26 -0
  213. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/009-content-digest-mismatch.json +26 -0
  214. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/010-malformed-signature-input.json +26 -0
  215. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/011-signature-without-input.json +25 -0
  216. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/012-missing-expires-param.json +26 -0
  217. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/013-expires-le-created.json +26 -0
  218. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/014-missing-nonce-param.json +26 -0
  219. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/015-signature-invalid.json +26 -0
  220. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/016-replayed-nonce.json +37 -0
  221. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/017-key-revoked.json +32 -0
  222. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/018-rate-abuse.json +33 -0
  223. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/019-revocation-stale.json +32 -0
  224. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/020-key-ops-missing-verify.json +41 -0
  225. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/021-base64-alphabet-mixing.json +26 -0
  226. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/001-basic-post.json +24 -0
  227. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/002-es256-post.json +24 -0
  228. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/003-multiple-signature-labels.json +24 -0
  229. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/004-default-port-stripped.json +24 -0
  230. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/005-percent-encoded-path.json +24 -0
  231. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/006-query-byte-preserved.json +24 -0
  232. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/007-body-without-idempotency-key.json +25 -0
  233. package/compliance/cache/3.1.0-rc.2/universal/billing-gate-dispatch.yaml +450 -0
  234. package/compliance/cache/3.1.0-rc.2/universal/canonical-format-validate-input.yaml +640 -0
  235. package/compliance/cache/3.1.0-rc.2/universal/capability-discovery.yaml +125 -0
  236. package/compliance/cache/3.1.0-rc.2/universal/collection-lists-pagination-integrity.yaml +306 -0
  237. package/compliance/cache/3.1.0-rc.2/universal/comply-controller-mode-gate.yaml +141 -0
  238. package/compliance/cache/3.1.0-rc.2/universal/content-standards-pagination-integrity.yaml +326 -0
  239. package/compliance/cache/3.1.0-rc.2/universal/deterministic-testing.yaml +1430 -0
  240. package/compliance/cache/3.1.0-rc.2/universal/error-compliance-signals.yaml +377 -0
  241. package/compliance/cache/3.1.0-rc.2/universal/error-compliance.yaml +528 -0
  242. package/compliance/cache/3.1.0-rc.2/universal/fictional-entities.yaml +307 -0
  243. package/compliance/cache/3.1.0-rc.2/universal/get-media-buys-pagination-integrity.yaml +160 -0
  244. package/compliance/cache/3.1.0-rc.2/universal/get-signals-pagination-integrity.yaml +210 -0
  245. package/compliance/cache/3.1.0-rc.2/universal/idempotency.yaml +861 -0
  246. package/compliance/cache/3.1.0-rc.2/universal/notification-config-event-scope.yaml +119 -0
  247. package/compliance/cache/3.1.0-rc.2/universal/notification-config-lifecycle.yaml +337 -0
  248. package/compliance/cache/3.1.0-rc.2/universal/notification-config-rejections.yaml +107 -0
  249. package/compliance/cache/3.1.0-rc.2/universal/pagination-integrity-creative-formats.yaml +265 -0
  250. package/compliance/cache/3.1.0-rc.2/universal/pagination-integrity-list-accounts.yaml +245 -0
  251. package/compliance/cache/3.1.0-rc.2/universal/pagination-integrity.yaml +263 -0
  252. package/compliance/cache/3.1.0-rc.2/universal/property-lists-pagination-integrity.yaml +307 -0
  253. package/compliance/cache/3.1.0-rc.2/universal/read-tool-idempotency.yaml +405 -0
  254. package/compliance/cache/3.1.0-rc.2/universal/runner-output-contract.yaml +1285 -0
  255. package/compliance/cache/3.1.0-rc.2/universal/schema-validation-signals.yaml +181 -0
  256. package/compliance/cache/3.1.0-rc.2/universal/schema-validation.yaml +548 -0
  257. package/compliance/cache/3.1.0-rc.2/universal/security.yaml +539 -0
  258. package/compliance/cache/3.1.0-rc.2/universal/signed-requests.yaml +217 -0
  259. package/compliance/cache/3.1.0-rc.2/universal/stale-response-advisory.yaml +295 -0
  260. package/compliance/cache/3.1.0-rc.2/universal/storyboard-schema.yaml +2194 -0
  261. package/compliance/cache/3.1.0-rc.2/universal/v3-envelope-integrity.yaml +117 -0
  262. package/compliance/cache/3.1.0-rc.2/universal/version-negotiation.yaml +130 -0
  263. package/compliance/cache/3.1.0-rc.2/universal/webhook-emission.yaml +411 -0
  264. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-bulk-webhooks.yaml +82 -0
  265. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-product-webhooks.yaml +83 -0
  266. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-products.yaml +151 -0
  267. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-signal-webhooks.yaml +83 -0
  268. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-signals.yaml +149 -0
  269. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  270. package/dist/lib/testing/storyboard/default-invariants.js +23 -0
  271. package/dist/lib/testing/storyboard/default-invariants.js.map +1 -1
  272. package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
  273. package/dist/lib/testing/storyboard/runner.js +84 -21
  274. package/dist/lib/testing/storyboard/runner.js.map +1 -1
  275. package/dist/lib/testing/storyboard/types.d.ts +21 -0
  276. package/dist/lib/testing/storyboard/types.d.ts.map +1 -1
  277. package/dist/lib/testing/storyboard/types.js.map +1 -1
  278. package/dist/lib/testing/types.d.ts +9 -0
  279. package/dist/lib/testing/types.d.ts.map +1 -1
  280. package/dist/lib/version.d.ts +3 -3
  281. package/dist/lib/version.js +3 -3
  282. package/package.json +1 -1
@@ -0,0 +1,2194 @@
1
+ # Storyboard Definition Schema
2
+ #
3
+ # Storyboards are narrative test workflows that walk agent builders through
4
+ # the sequence of calls their agent will receive, with context at each step.
5
+ #
6
+ # Each storyboard targets a specific agent interaction model and
7
+ # describes the flow from a caller's perspective.
8
+
9
+ # --- Schema definition ---
10
+
11
+ # A storyboard file must conform to this structure:
12
+ #
13
+ # id: string (unique identifier, e.g., "creative_template")
14
+ # version: string (semver, e.g., "1.0.0")
15
+ # title: string (human-readable title)
16
+ # category: string — matches the storyboard's `id` top-level segment.
17
+ #
18
+ # For *specialism* storyboards, this is the snake_case form of an entry in
19
+ # /schemas/enums/specialism.json (the wire-protocol enum declared by agents
20
+ # on `get_adcp_capabilities.specialisms[]`). The protocol enum is
21
+ # authoritative — when adding a new specialism storyboard, add the kebab-case
22
+ # form to specialism.json first. The current specialism categories
23
+ # (snake_case form) are:
24
+ #
25
+ # Sales: sales_guaranteed | sales_non_guaranteed | sales_proposal_mode
26
+ # | sales_catalog_driven | sales_broadcast_tv | sales_social
27
+ # Creative: creative_ad_server | creative_generative | creative_template
28
+ # Governance: content_standards | property_lists | collection_lists
29
+ # | governance_aware_seller | governance_delivery_monitor
30
+ # | governance_spend_authority
31
+ # Signals: signal_marketplace | signal_owned
32
+ # Brand: brand_rights
33
+ # Audiences: audience_sync
34
+ #
35
+ # For *universal / domain-level* storyboards, the category is descriptive
36
+ # only (not on the wire). Examples in current use: capability_discovery,
37
+ # schema_validation, error_compliance, security_baseline, si_baseline,
38
+ # media_buy_seller, media_buy_governance_escalation, signed_requests,
39
+ # idempotency, webhook_emission, deterministic_testing,
40
+ # runner_output_contract, v3_envelope_integrity. New universal storyboards
41
+ # pick a descriptive snake_case identifier; see existing files under
42
+ # static/compliance/source/universal/ and protocols/ for the conventions.
43
+ #
44
+ # Scenario variants use <category>/<variant> form, e.g.
45
+ # governance_spend_authority/denied, creative_generative/seller,
46
+ # brand_rights/governance_denied, media_buy_seller/provenance_enforcement.
47
+ # summary: string (one-line description for listings)
48
+ # track: enum (optional — compliance track this storyboard contributes to:
49
+ # core | products | media_buy | creative | reporting | governance |
50
+ # campaign_governance | signals | si | audiences | error_handling | brand | security)
51
+ # required_tools: string[] (optional — tool names the storyboard uses; empty for protocol-level tests.
52
+ # Lenient any-of: missing all listed tools triggers a coverage-gap skip
53
+ # (skip_result.reason: missing_tool at step scope). See `required_any_of_tools`
54
+ # below for the strict per-family load-time gate that grades requirement_unmet.)
55
+ # required_any_of_tools: array of OR-family objects (optional — declarative one-of-N
56
+ # tool-advertisement gate. Each entry is { tools: string[], rationale?: string };
57
+ # at least one tool in each family MUST be advertised by the agent under test.
58
+ # Multiple entries are AND-combined: every family must be satisfied independently.
59
+ # Distinct from `required_tools` (which is the lenient any-of for storyboard
60
+ # applicability — missing all listed tools triggers a coverage-gap skip).
61
+ # See the dedicated `required_any_of_tools` block below for full semantics.
62
+ # narrative: string (paragraph explaining the overall flow)
63
+ #
64
+ # requires_scenarios: string[] (optional — scenario IDs from storyboards/scenarios/ that must pass alongside this storyboard.
65
+ # Scenarios are small, focused behavior tests (e.g., "media_buy_seller/accepts_governance").
66
+ # The compliance engine resolves and runs them alongside the main storyboard.
67
+ #
68
+ # Flag flow across `requires_scenarios`:
69
+ # A `branch_set` flag contributed in a step of storyboard X is
70
+ # considered asserted if EITHER (a) a step in X's own phases asserts
71
+ # it via `assert_contribution` with `check: any_of, allowed_values:
72
+ # [<flag>]`, OR (b) a scenario Y listed in `X.requires_scenarios`
73
+ # asserts it via the same check. The `orphan_contribution` lint
74
+ # honors this flow.
75
+ #
76
+ # Reverse flow (a contribution in scenario Y asserted in parent X)
77
+ # is NOT supported — scenarios MUST be internally self-grading.
78
+ # The restriction preserves standalone lintability of scenarios
79
+ # and prevents cycles in the composition graph. Scenarios that
80
+ # contribute a flag without asserting it in their own phases will
81
+ # surface as `orphan_contribution` when linted standalone.
82
+ #
83
+ # Scenario IDs in `requires_scenarios` MUST match the referenced
84
+ # file's top-level `id:` exactly, and the referenced scenario file
85
+ # MUST exist in the source tree at build time. Duplicate IDs
86
+ # across files are a build-time error.)
87
+ #
88
+ # default_agent: string (optional — logical agent key used by multi-agent
89
+ # storyboard runners to route steps that do not have a unique specialism
90
+ # claimant in the runtime agents map. Resolved by the runner against the
91
+ # `agents` option passed to `runStoryboard({ agents: { sales: …, governance: …, … } })`.
92
+ #
93
+ # Key shape: free-form non-empty string, matched verbatim against the
94
+ # runtime `agents` map's keys. The spec does NOT constrain the key to the
95
+ # specialism enum (`sales`, `signals`, `governance`, `creative`, `brand`)
96
+ # because production multi-agent topologies legitimately fan out per-property
97
+ # (`nyt_sales`, `wsj_sales`), per-region (`sales_eu`, `sales_us`), or
98
+ # per-brand-rights-holder. Authors choose the convention that matches their
99
+ # operator's CI invocation; portability across operators is the author's
100
+ # concern, not the spec's.
101
+ #
102
+ # When to set it. Storyboards that exercise cross-domain tools — e.g.,
103
+ # `sync_creatives`, `list_creative_formats` — do not name a single specialism
104
+ # the runner can match. The author has the most context for which logical
105
+ # tenant should receive these calls (usually "wherever the seller is":
106
+ # `sales`). Encoding `default_agent: sales` once in the YAML beats
107
+ # re-asserting it on every CI invocation. (Note: `comply_test_controller`
108
+ # is routed via `prerequisites.controller_seeding`, not this field — the
109
+ # controller is a back-channel, not a specialism claimant.)
110
+ #
111
+ # Resolution order (runner contract — see adcp-client#1066, adcp-client#1355):
112
+ # 1. Step-level `agent:` override (if declared on the step).
113
+ # 2. Specialism-claimant match against the runtime agents map (matched
114
+ # via each agent's `get_adcp_capabilities.supported_protocols`):
115
+ # - Exactly one agent claims the step's task's specialism → route there.
116
+ # - Zero claimants → fall through to slot 3.
117
+ # - Two or more claimants → runner MUST grade the step `unrouted_step`
118
+ # rather than picking arbitrarily. Multi-claim is an operator-config
119
+ # error; the storyboard cannot disambiguate it. Slot 3/4 do NOT
120
+ # rescue this case — silently picking one would mask the misconfig.
121
+ # 3. Storyboard-level `default_agent` (THIS field) resolved against the
122
+ # runtime agents map. When the field is set but the key is absent from
123
+ # the map, the runner MUST grade the step `default_agent_unresolved`
124
+ # and MUST NOT fall through to slot 4 — silent fallback would invisibly
125
+ # override the storyboard author's encoded intent. Slot 4 fires only
126
+ # when the storyboard does NOT declare this field.
127
+ # 4. Run-options `default_agent` passed to `runStoryboard({ default_agent })`.
128
+ # Same key-resolution rule as slot 3: set-but-unmatched grades
129
+ # `default_agent_unresolved`; unset falls through to slot 5.
130
+ # 5. Fail-fast — runner raises `unrouted_step` and grades the step failed.
131
+ #
132
+ # Single-agent runs ignore this field entirely — there is no map to resolve
133
+ # against, every step routes to the only configured agent. Authors SHOULD
134
+ # still declare it for clarity; multi-agent runners treat it as advisory and
135
+ # single-agent runners as a no-op.
136
+ #
137
+ # Validation. The `<key>` MUST be a non-empty string. Validation that the
138
+ # key resolves to a configured tenant is a runtime concern (per the slot 3
139
+ # rule above), not a schema concern — the same storyboard runs against
140
+ # different topologies.)
141
+ #
142
+ # requires: string[] (optional — storyboard-level runtime requirement gate.
143
+ # Known values: controller | seeded_state | real_wire (forward-compat:
144
+ # unrecognized values are NOT schema violations — runners emit
145
+ # requirement_unmet at runtime; see Forward compatibility below).
146
+ # Default when absent: [real_wire] — untagged storyboards run unchanged.
147
+ # minItems: 1 — runners MUST fail-load on requires: [] (empty array) because
148
+ # no distinct semantic exists between the empty set and omitting the field;
149
+ # the error message SHOULD read "use requires: [real_wire] for the default
150
+ # behavior, or omit the field entirely."
151
+ #
152
+ # Values and availability:
153
+ #
154
+ # controller — gate on agent advertising comply_test_controller.
155
+ # Available when the agent's get_adcp_capabilities response
156
+ # includes comply_test_controller in its tool list. When
157
+ # unmet, the runner MUST emit skip_result.reason:
158
+ # requirement_unmet with detail naming "controller".
159
+ # Orthogonal to prerequisites.controller_seeding: the two
160
+ # fields are independent. controller_seeding is an execution-
161
+ # phase instruction ("inject a fixtures phase before the main
162
+ # phases run"); requires: [controller] is a load-phase gate
163
+ # ("skip the whole storyboard if the capability is absent").
164
+ # Storyboards may declare both: the gate fires first, and
165
+ # controller_seeding takes effect only if the gate passes.
166
+ # Declaring controller_seeding: true without requires:
167
+ # [controller] is also valid — mid-run, individual steps
168
+ # grade missing_test_controller if the agent lacks the
169
+ # tool; requires: [controller] coalesces that into a single
170
+ # whole-storyboard skip at load time instead.
171
+ # Mid-run transient loss: if the load-phase gate passes
172
+ # (the agent advertised comply_test_controller at load
173
+ # time) but the tool becomes unavailable mid-run, the
174
+ # runner MUST grade the affected step missing_test_controller
175
+ # per the existing per-step path, NOT a retroactive
176
+ # requirement_unmet at storyboard scope.
177
+ #
178
+ # seeded_state — gate on operator asserting out-of-band state provisioning.
179
+ # Available when the operator declares (e.g., via a CLI flag
180
+ # or runner option) that prerequisite state has been seeded
181
+ # externally before the run. When unmet, the runner MUST emit
182
+ # skip_result.reason: requirement_unmet with detail naming
183
+ # "seeded_state". Scenarios fail naturally on the first
184
+ # stateful step if state is not actually present.
185
+ # ANTI-GAMING: trust signals (verified-live badges, Tier-2
186
+ # production scores) MUST NOT consume requires: [seeded_state]
187
+ # pass results without independent verification. The field
188
+ # widens the graded set; it does not attest the state.
189
+ # This is an operator-governance norm; the runner cannot
190
+ # enforce it on behalf of downstream certification dashboards.
191
+ #
192
+ # real_wire — reserved for a future --mock-only execution mode. Current
193
+ # behavior: no runtime effect (gate is always met). Authors
194
+ # SHOULD omit this value rather than listing it explicitly,
195
+ # since the default when the field is absent is semantically
196
+ # equivalent to [real_wire] today (this equivalence holds
197
+ # only while --mock-only is not active; the --mock-only
198
+ # inversion may break it — see below).
199
+ # IMPORTANT: the future --mock-only semantics INVERT the
200
+ # skip direction compared to other values — this value will
201
+ # cause a storyboard to skip when --mock-only is active (i.e.,
202
+ # skip-when-mocked, pass-when-live), opposite to the
203
+ # skip-when-absent logic of controller and seeded_state.
204
+ # Whether the default-when-absent (omitting `requires`)
205
+ # also triggers the skip under --mock-only is to be defined
206
+ # when --mock-only ships; until then, authors MUST NOT rely
207
+ # on any particular behavior for omitted vs. explicit
208
+ # real_wire under a mock-only runner.
209
+ # Composing real_wire with other values (e.g., requires:
210
+ # [controller, real_wire]) is syntactically valid but the
211
+ # combined behavior under --mock-only is unspecified; authors
212
+ # SHOULD NOT mix real_wire with other values until --mock-only
213
+ # semantics are defined.
214
+ # Harnesses that consume this value MUST account for the
215
+ # skip-direction inversion when --mock-only support lands.
216
+ #
217
+ # Forward compatibility: runners encountering a requires value they do not
218
+ # recognize MUST emit skip_result.reason: requirement_unmet (not fail-load)
219
+ # and MUST name the unrecognized value in skip_result.detail so dashboards
220
+ # can surface it. This matches runner-output-contract.yaml > requirement_unmet
221
+ # and is consistent with the forward-compat posture throughout the harness —
222
+ # an unrecognized gate is not a coverage gap (not_applicable would signal
223
+ # "agent did not claim the protocol"), it is an unmet requirement. Only
224
+ # requires: [] (empty array) is a hard fail-load — that is a schema
225
+ # constraint, not a semantic gate, and schema violations are always loader
226
+ # errors.
227
+ #
228
+ # Interaction with per-step requires_tool: the two fields are independent.
229
+ # requires gates the whole storyboard at load time; requires_tool gates
230
+ # individual steps at execution time. Both may be declared on the same
231
+ # storyboard.)
232
+ #
233
+ # required_any_of_tools: array (optional — declarative one-of-N tool-advertisement
234
+ # gate at storyboard load time. Each array entry is an OR-family object:
235
+ #
236
+ # required_any_of_tools:
237
+ # - tools: [list_accounts, sync_accounts]
238
+ # rationale: "AdCP 3.0.9 §accounts/overview"
239
+ #
240
+ # Per-entry object shape:
241
+ # tools: string[] — tool names; at least one MUST be advertised by the agent
242
+ # under test. minItems: 2 (a single-tool family collapses
243
+ # to `required_tools`; encode it there instead).
244
+ # rationale: string (optional) — human-readable spec citation surfaced in
245
+ # skip messages so dashboards can attribute the gate to
246
+ # the underlying normative requirement. Storyboard-author-
247
+ # controlled string; runners that render this value into
248
+ # LLM-facing surfaces MUST fence per
249
+ # runner-output-contract.yaml > rendered_output_fencing
250
+ # (the value flows into skip_result.detail, which is
251
+ # already on the fenced list — this is defense-in-depth
252
+ # for renderers that consume rationale directly).
253
+ #
254
+ # Multiple entries are AND-combined: every family in the array MUST be
255
+ # satisfied independently. Empty array is fail-load — omit the field entirely
256
+ # for "no requirement" (same posture as `requires`).
257
+ #
258
+ # Distinct from `required_tools`. `required_tools` is the lenient any-of for
259
+ # storyboard applicability — missing all listed tools triggers a coverage-gap
260
+ # skip. `required_any_of_tools` is the strict variant: missing the family is
261
+ # still a skip (not a fail-load — see "Runner behavior" below), but with
262
+ # distinct `detail` attribution so the skip aggregates against the spec rule
263
+ # rather than the storyboard scope. The two compose cleanly on the same
264
+ # storyboard.
265
+ #
266
+ # Distinct from `provides_state_for`. `provides_state_for` is a per-step
267
+ # substitution mechanism that waives a downstream cascade when a peer step
268
+ # establishes equivalent state at runtime. `required_any_of_tools` is a
269
+ # storyboard-load-time gate on tool *advertisement* — it fires before any
270
+ # step executes and does not interact with step-level substitution. The two
271
+ # operate on different axes (storyboard scope vs step scope; advertisement-
272
+ # time vs run-time) and may coexist on the same storyboard.
273
+ #
274
+ # Runner behavior. When an agent advertises NONE of the tools in any family,
275
+ # the runner MUST emit at storyboard scope:
276
+ #
277
+ # skip_result.reason = requirement_unmet
278
+ # skip_result.detail = "missing_required_tool_family: needs <tool_a> or <tool_b>[ or <tool_c>...] (<rationale>)"
279
+ #
280
+ # The `missing_required_tool_family:` sub-reason prefix in `detail` is the
281
+ # canonical attribution string (see runner-output-contract.yaml >
282
+ # `requirement_unmet` for the full enumeration of recognized detail
283
+ # sub-reasons and the canonical wire shape for separators when more than one
284
+ # gate fails). This matches the same `requirement_unmet`-with-detail pattern
285
+ # already used for `requires:` gates — no new top-level reason enum is added
286
+ # to `runner-output-contract.yaml`, so this field ships without bumping the
287
+ # contract version.
288
+ #
289
+ # Per-family OR semantics. A family is satisfied as soon as the agent
290
+ # advertises ANY tool in `tools[]`. The runner MUST NOT additionally require
291
+ # that the named tools are exercised by storyboard steps — this field gates
292
+ # on tool *advertisement*, not on step execution. Per-step gating remains
293
+ # the job of `requires_tool` on individual steps.
294
+ #
295
+ # Forward compatibility. Runners encountering this field on a storyboard
296
+ # they otherwise support MUST honor it. Runners that pre-date this field
297
+ # will load and execute the storyboard as if the field were absent; this
298
+ # is acceptable because the universal account-discovery rule (the first
299
+ # use case) is also enforced by the SDK's runner-level conformance gate,
300
+ # which fires independently of per-storyboard tagging. Future runners that
301
+ # migrate to per-storyboard attribution (see adcp-client#1642) consume this
302
+ # field for richer skip-cause aggregation.
303
+ #
304
+ # Composition with other gates. `required_any_of_tools` is evaluated at
305
+ # load time alongside `requires`. Either gate failing produces
306
+ # skip_result.reason: requirement_unmet at storyboard scope. Runners SHOULD
307
+ # emit a single sub-reason in `detail` when only one gate is unmet. When
308
+ # multiple gates fail, runners MAY aggregate sub-reasons per the wire shape
309
+ # pinned in runner-output-contract.yaml > `requirement_unmet`; aggregated
310
+ # `detail` strings are intended for human display and SHOULD NOT be
311
+ # parsed beyond the first sub-reason by automated consumers. Dashboards
312
+ # that need exhaustive multi-gate attribution SHOULD surface a multi-gate
313
+ # indicator separately rather than splitting `detail` themselves.)
314
+ #
315
+ # agent:
316
+ # interaction_model: enum (stateless_transform | stateful_preloaded | stateful_push | stateless_generate | media_buy_seller | marketplace_catalog | owned_signals | si_platform | brand_rights_holder | governance_agent)
317
+ # capabilities: string[] (legacy descriptive capability labels; bundle selection is driven by get_adcp_capabilities.supported_protocols and specialisms)
318
+ # examples: string[] (real-world examples: "Celtra", "Innovid")
319
+ #
320
+ # caller:
321
+ # role: string (who initiates the calls: "buyer_agent", "orchestrator", "dsp")
322
+ # example: string (e.g., "Scope3", "Pinnacle Agency")
323
+ #
324
+ # prerequisites:
325
+ # description: string (what must be true before running this storyboard)
326
+ # test_kit: string (reference to a test kit file, e.g., "test-kits/acme-outdoor.yaml").
327
+ # Always a single path — test kits are not composed through this field. See
328
+ # "Test kit flavors" below for the two shapes a test kit may take and how
329
+ # storyboards compose brand identity with harness coordination when they need
330
+ # both.
331
+ # controller_seeding: boolean (optional — when true, the runner auto-injects a
332
+ # fixtures phase that seeds every entry in the storyboard's top-level
333
+ # `fixtures:` block via `comply_test_controller` before the main phases run.
334
+ # Storyboards that hardcode fixture IDs in sample_request payloads SHOULD
335
+ # declare this and populate `fixtures:`. Missing `seed_*` support on the
336
+ # agent grades the storyboard `not_applicable`, not failed.)
337
+ #
338
+ # fixtures: object (optional — declarative prerequisite state the runner seeds via
339
+ # comply_test_controller before executing phases. Structure:
340
+ # fixtures:
341
+ # products:
342
+ # - product_id: "test-product"
343
+ # delivery_type: "non_guaranteed"
344
+ # pricing_options:
345
+ # - pricing_option_id: "test-pricing"
346
+ # pricing_model: "cpm"
347
+ # creatives:
348
+ # - creative_id: "campaign_hero_video"
349
+ # status: "approved"
350
+ # format_id: { id: "video_30s" }
351
+ # plans:
352
+ # - plan_id: "gov_acme_q2_2027"
353
+ # budget: { total: 30000, currency: "USD" }
354
+ # media_buys:
355
+ # - media_buy_id: "mb_acme_q2_2026_auction"
356
+ # status: "active"
357
+ # Each top-level key maps to a seed_* scenario (products → seed_product,
358
+ # pricing_options → seed_pricing_option, creatives → seed_creative,
359
+ # plans → seed_plan, media_buys → seed_media_buy). Storyboards that need an
360
+ # external measurement vendor catalog SHOULD add an explicit
361
+ # comply_test_controller step with scenario: seed_measurement_catalog. Product
362
+ # fixtures may carry `measurement_catalogs[]` entries only as a compatibility
363
+ # fallback when a storyboard needs to keep that snapshot beside the product
364
+ # contract. Foreign-key dependency DAG the runner MUST honor when auto-seeding:
365
+ #
366
+ # product ──┬─→ pricing_option
367
+ # ├─→ plan
368
+ # └─→ media_buy
369
+ # creative ────→ media_buy
370
+ # plan ────────→ media_buy
371
+ #
372
+ # Products seed before pricing_options and plans that reference them;
373
+ # products, creatives, and plans all seed before media_buys that reference
374
+ # them. See docs/building/implementation/comply-test-controller.mdx for the
375
+ # full seeding semantics and per-scenario param shapes.
376
+ #
377
+ # Storyboards SHOULD prefer the `fixtures:` block over hardcoded sample_request
378
+ # IDs whose existence the runner cannot verify.
379
+ #
380
+ # Hardcoded-literal fixtures in sample_request (e.g., product_id: "test-product"
381
+ # without a matching `fixtures:` entry) are permitted through 3.x for existing
382
+ # storyboards that pre-seed matching IDs out of band, and SHOULD migrate to the
383
+ # `fixtures:` block before 4.0. New storyboards authored after this spec edit
384
+ # MUST NOT hardcode fixture IDs without a corresponding `fixtures:` block or a
385
+ # test-kit substitution.)
386
+ #
387
+ # --- Test kit flavors ---
388
+ #
389
+ # Files under `static/compliance/source/test-kits/` come in two shapes. The
390
+ # distinction is not enforced by a separate field today — it's identified by
391
+ # which fixture data the kit carries.
392
+ #
393
+ # Brand kit — carries a brand identity and authentication fixture. Declares:
394
+ # id string (short identifier)
395
+ # auth.api_key string (`demo-<kit>-v1` pattern; the demo Bearer the
396
+ # runner sends on positive api-key probes)
397
+ # OR
398
+ # auth.basic object (the demo HTTP Basic credential the runner sends
399
+ # on positive Basic probes). Shape is either:
400
+ # username: string
401
+ # password: string
402
+ # or:
403
+ # credentials: string
404
+ # where `credentials` is the unencoded `username:password`
405
+ # pair used to build the Authorization header.
406
+ # auth.probe_task string (the protected read the runner calls under
407
+ # `auth: none` and with random-invalid credentials)
408
+ # brand object (house + brand_id + names + logos + colors +
409
+ # fonts + tone — full brand.json payload)
410
+ # + optional products, creatives, pricing_options, destinations, etc.
411
+ #
412
+ # Today's brand kits: acme-outdoor, bistro-oranje, nova-motors, osei-natural,
413
+ # summit-foods. Used by storyboards that exercise brand-specific AdCP protocol
414
+ # flows (creative, media-buy, signals, governance, etc.).
415
+ #
416
+ # Runner contract — carries a harness coordination contract. Declares:
417
+ # id string (short identifier)
418
+ # applies_to object or list (which storyboards/specialisms consume
419
+ # this contract, e.g., `{ universal_storyboard: signed-requests }`
420
+ # or `{ specialism: sales-catalog-driven }`)
421
+ # + contract-specific fields describing how the runner should pre-configure
422
+ # the agent under test (e.g., `receiver_urls:`, `retry_replay_contract:`
423
+ # for webhook-receiver-runner; `endpoint_modes:` for signed-requests)
424
+ #
425
+ # Today's runner contracts: signed-requests-runner, substitution-observer-
426
+ # runner, webhook-receiver-runner. They carry no credentials today — the
427
+ # runner injects no auth for these storyboards because the storyboards test
428
+ # harness infrastructure (signature verification, webhook receiver
429
+ # coordination, URL substitution observability), not brand-specific AdCP
430
+ # flows. A future runner contract could legitimately carry its own test-
431
+ # coordination principal (e.g., a callback-authenticating receiver), so the
432
+ # "no credentials" property is temporal, not structural.
433
+ #
434
+ # Enforcement. `scripts/lint-storyboard-test-kits.cjs` fails the build if a
435
+ # kit under `test-kits/` declares none of `auth.api_key`, `auth.basic`, or
436
+ # `applies_to` — that's the bimodal partition this section describes. The lint
437
+ # tolerates kits that declare both a credential marker and `applies_to` (the
438
+ # future-branded-runner case above).
439
+ #
440
+ # Composition. Storyboards that need BOTH a brand identity AND a harness
441
+ # contract declare the brand kit in `prerequisites.test_kit` and opt into the
442
+ # runner contract through the `requires_contract: <runner_id>` field on
443
+ # specific assertion tasks that need it — today that's `expect_substitution_safe`
444
+ # and `expect_webhook*` (see their task definitions below for the per-task
445
+ # field shape). When the named contract is not in scope for the runner, the
446
+ # task grades as `not_applicable` rather than failing. Example:
447
+ # `specialisms/sales-catalog-driven/index.yaml` declares
448
+ # `prerequisites.test_kit: "test-kits/acme-outdoor.yaml"` and its
449
+ # `expect_substitution_safe` step carries `requires_contract:
450
+ # substitution_observer_runner`.
451
+ #
452
+ # Storyboards that test pure harness infrastructure without a specific brand
453
+ # (e.g., `universal/signed-requests.yaml`,
454
+ # `universal/webhook-emission.yaml`) point `prerequisites.test_kit` directly
455
+ # at the runner contract and fall back on `task_default:` when any
456
+ # `$test_kit.operations.<name>` reference resolves to null (see Step field
457
+ # docs).
458
+ #
459
+ # `test_kit=<path>` in the contradiction-lint fingerprint disambiguates the
460
+ # two flavors — `auth=kit_default` resolves to the kit's static credential for
461
+ # brand kits and to no-credential for runner contracts, but the two never
462
+ # collide because the `test_kit=` component separates them.
463
+ #
464
+ # Future: if a storyboard ever needs to compose TWO runner contracts
465
+ # simultaneously (e.g., signed-requests + substitution-observer), the step-
466
+ # level `requires_contract:` field will need to accept a list. Not reachable
467
+ # today.
468
+ #
469
+ # See `docs/contributing/storyboard-authoring.md` for the author-workflow
470
+ # side of kit selection.
471
+ #
472
+ # context: object (optional — initial values seeded into the runner's context
473
+ # accumulator before any step executes. Keys become $context.<key> references
474
+ # available throughout the storyboard run. Use this field to provide
475
+ # per-storyboard defaults for values that vary by operator topology (e.g.,
476
+ # a governance agent URL the runner can override at invocation time once the
477
+ # CLI supports --context on `storyboard run`).
478
+ #
479
+ # Example:
480
+ # context:
481
+ # governance_agent_url: "https://test-agent.adcontextprotocol.org"
482
+ #
483
+ # Precedence in the context accumulator (highest to lowest):
484
+ # 1. Step `context_outputs:` captures (populated after a step passes)
485
+ # 2. THIS field (storyboard-root `context:` block, fixed at run start)
486
+ # 3. Test-kit substitutions where explicitly scoped into context
487
+ #
488
+ # Values MUST be scalars (strings, numbers, booleans — JSON scalar types).
489
+ # Object and array values are not supported here — capture structured values
490
+ # from step responses via `context_outputs:` instead.
491
+ #
492
+ # See "Context accumulator and substitution" below for the full precedence
493
+ # contract, runner substitution rules, and unresolved-substitution grading.)
494
+ #
495
+ # phases: array of Phase objects
496
+ #
497
+ # --- Phase ---
498
+ #
499
+ # id: string (unique within storyboard)
500
+ # title: string (human-readable phase title)
501
+ # narrative: string (paragraph explaining this phase from the caller's perspective)
502
+ #
503
+ # depends_on: string[] (optional — overrides the cross-phase cascade default)
504
+ # Default semantics (field absent or undefined): "all prior phases." Every
505
+ # phase implicitly depends on every prior phase, so when ANY prior phase
506
+ # trips its stateful cascade (a stateful step fails or skips for a
507
+ # missing-state reason — see `runner-output-contract.yaml > cascade_rules.
508
+ # default_cascade`), every stateful step in this phase cascade-skips with
509
+ # `prerequisite_failed`. This default preserves the F6 round-2 pattern
510
+ # where setup phases establish state consumed by a later phase.
511
+ #
512
+ # Two override forms:
513
+ #
514
+ # - Independent phase: `depends_on: []` — explicitly declares the phase
515
+ # has no upstream dependencies and runs even if every prior phase
516
+ # tripped. Use for phases whose state derives entirely from in-phase
517
+ # steps (e.g., the phase creates its own media buy / session / account
518
+ # via a `comply_test_controller`-gated step) and that consume no
519
+ # `$context.*` value produced by an earlier phase. The
520
+ # `sole_stateful_step_exemption` in `runner-output-contract.yaml`
521
+ # addresses a related-but-narrower case at the step level; `depends_on:
522
+ # []` is the phase-level escape hatch and applies regardless of how
523
+ # many stateful steps the phase contains.
524
+ #
525
+ # - Targeted dependency: `depends_on: ['phase_id', ...]` — only the
526
+ # named phases gate this phase's cascade. Other phases tripping is
527
+ # irrelevant. Listed phase IDs MUST exist in the same storyboard and
528
+ # MUST be declared earlier in the `phases[]` array.
529
+ #
530
+ # Validation rules (enforced at storyboard load; fail at parse time):
531
+ # - Value MUST be an array of non-empty strings (or absent).
532
+ # - Self-references are rejected.
533
+ # - Forward references and unknown phase IDs are rejected.
534
+ # - Empty list is legal and means "no upstream dependencies."
535
+ #
536
+ # Authoring guidance: prefer the default. Use `[]` only when the phase
537
+ # verifiably builds its own state in-phase — read the phase's
538
+ # `context_inputs` and `$context.*` references to confirm no cross-phase
539
+ # coupling exists. Use the targeted form when a phase depends on a
540
+ # specific subset of prior phases (e.g., consumption phase depends only
541
+ # on setup phases, not on sibling consumption phases that may legitimately
542
+ # skip on different adopters).
543
+ #
544
+ # References: adcp-client#1161 (field introduction); adcp-client#1711
545
+ # follow-up + adcp#4750 (deterministic_testing scope tightening that
546
+ # surfaced the documentation gap).
547
+ #
548
+ # steps: array of Step objects
549
+ #
550
+ # --- Step ---
551
+ #
552
+ # id: string (unique within phase)
553
+ # title: string (human-readable step title)
554
+ # narrative: string (what's happening and why)
555
+ # task: string (AdCP task name: list_creative_formats, preview_creative, build_creative, get_products,
556
+ # create_media_buy, sync_accounts, etc. May reference a test-kit field, e.g.
557
+ # "$test_kit.auth.probe_task", in which case `task_default` provides the fallback.)
558
+ # task_default: string (optional — default task name when `task` is a test-kit reference that resolves to null)
559
+ # schema_ref: string (path to request schema, e.g., "creative/list-creative-formats-request.json" or "media-buy/get-products-request.json")
560
+ # response_schema_ref: string (path to response schema)
561
+ # doc_ref: string (path to documentation page)
562
+ # comply_scenario: string (maps to @adcp/client testing scenario, e.g., "creative_sync")
563
+ # expected: string (human-readable description of expected behavior)
564
+ # stateful: boolean (does this step depend on state from a previous step?)
565
+ #
566
+ # sample_request: object (optional — example request payload for this step)
567
+ # Placeholders: `$context.<name>` and `{{prior_step.<id>.<field>}}`. See
568
+ # the "Substitution" section below for the full semantics. For the narrow
569
+ # case where the SDK's per-task request builder discards a body field
570
+ # from `sample_request`, use `context_inputs:` (above) as the
571
+ # post-builder injection path.
572
+ # sample_response: object (optional — example expected response; also surfaces in published docs)
573
+ #
574
+ # auth: "none" | object (optional — overrides the transport's default credentials for this step)
575
+ # auth: none → strip any configured credentials before sending
576
+ # auth: { type: api_key, from_test_kit: true } → use the API key at `auth.api_key` in the test kit
577
+ # auth: { type: api_key, from_test_kit: "auth.principals.<name>.api_key" }
578
+ # → select a named principal within a multi-principal kit
579
+ # (forward-compatible shape; no kit exposes multiple
580
+ # principals today)
581
+ # auth: { type: api_key, value_strategy: random_invalid }
582
+ # → use a per-run random bogus API key (for invalid-key probes).
583
+ # Runner generates `invalid-<32 random bytes>` per run.
584
+ # auth: { type: basic, from_test_kit: "auth.basic" } → use the Basic credential at `auth.basic` in the test kit
585
+ # auth: { type: basic, value_strategy: random_invalid }
586
+ # → use a per-run random bogus Basic credential (for invalid-Basic probes).
587
+ # Runner generates random username/password bytes per run.
588
+ # auth: { type: oauth_bearer, value_strategy: random_invalid_jwt }
589
+ # → use a per-run random JWT-shaped bogus Bearer token.
590
+ # Runner generates `<base64url random>.<base64url random>.<base64url random>` per run.
591
+ #
592
+ # DISALLOWED: `auth: { type: ..., value: "<literal>" }` — literal credentials in
593
+ # storyboard YAML are a code smell (they bind the storyboard to a specific value,
594
+ # can't rotate without rewriting, and leak plaintext identity into source).
595
+ # `lint-storyboard-auth-shape.cjs` (rule `literal_value`) fails the build on this
596
+ # shape. Use `from_test_kit` or `value_strategy` instead. See #2720.
597
+ #
598
+ # omit_idempotency_key: boolean (optional — when true on a task step, the runner
599
+ # skips `applyIdempotencyInvariant` and signals the SDK client via
600
+ # `skipIdempotencyAutoInject` so the request reaches the agent WITHOUT an
601
+ # `idempotency_key`. Use for missing-key rejection vectors — without this flag
602
+ # the SDK may auto-inject a key and the agent never sees the missing-key path.
603
+ # 3.0 used this only for mutating tasks. 3.1 extends `idempotency_key` to every
604
+ # request, so read-tool omission probes SHOULD also set this flag to prevent
605
+ # current or future runner/client auto-injection from masking the vector.)
606
+ #
607
+ # contributes_to: string (optional — marks this step as contributing a named flag
608
+ # that a later assert_contribution step can require)
609
+ # contributes: boolean (optional — shorthand for `contributes_to: <enclosing phase's branch_set.id>`
610
+ # introduced alongside first-class `branch_set:` declarations.
611
+ # Legal only inside a phase that declares `branch_set:`; the runner's
612
+ # loader resolves `contributes: true` to the phase's branch_set.id
613
+ # and raises a storyboard-load error on `contributes: true` outside
614
+ # a branch_set phase or on a step that also declares `contributes_to`.
615
+ # See adcp-client#693 and `lint:storyboard-branch-sets` for the
616
+ # full rule set.)
617
+ # contributes_if: string (optional — runner-evaluated expression gating the contribution)
618
+ # Supported grammar (single form; not a general-purpose DSL):
619
+ # "prior_step.<step_id>.passed" → true if the named prior step in the same storyboard passed
620
+ # Storyboard authors should prefer expressing gating through step ordering when possible.
621
+ #
622
+ # provides_state_for: string | string[] (optional — declares that this step's pass
623
+ # establishes equivalent state for the named peer step(s) in the same phase. Used
624
+ # to rescue the cascade-skip default when one of two interchangeable stateful
625
+ # steps is missing on the agent: e.g., explicit-mode sellers that pre-provision
626
+ # accounts out-of-band declare `sync_accounts` as missing_tool but expose
627
+ # `list_accounts` as the canonical alternative — declaring
628
+ # `provides_state_for: sync_accounts` on `list_accounts` tells the runner that
629
+ # the substitute's pass satisfies the same downstream state contract.
630
+ #
631
+ # Grammar parallels `contributes_to`: declared on the substitute step, names
632
+ # the target peer(s) whose state it provides. The two fields are independent —
633
+ # `contributes_to` flows branch-set flags into a later `assert_contribution`;
634
+ # `provides_state_for` waives a peer's missing-tool / missing-test-controller
635
+ # cascade so downstream stateful steps still run.
636
+ #
637
+ # Array semantics: `provides_state_for: [A, B]` is ALL-OF — one substitute pass
638
+ # establishes state for both A and B simultaneously. ANY-OF semantics are not
639
+ # supported; if a substitute only conditionally satisfies a peer, model it as
640
+ # two separate substitute steps.
641
+ #
642
+ # Validation rules (enforced by lint:storyboard-provides-state-for, fail at
643
+ # parse time):
644
+ # - Same-phase only. The target step MUST live in the same phase as the
645
+ # substitute step. Cross-phase substitution is rejected; a cross-phase
646
+ # state contract belongs in `context_outputs` / `context_inputs`, not
647
+ # this field.
648
+ # - Target step must exist in the same phase by `id`.
649
+ # - Target step MUST declare `stateful: true`. Stateless peers don't
650
+ # carry a state contract to substitute for.
651
+ # - Substitute step MUST declare `stateful: true`. A stateless step
652
+ # cannot establish equivalent state on the agent side.
653
+ # - Self-references (`provides_state_for: <own_id>`) are rejected.
654
+ # - Cycles (A→B and B→A in the same phase) are rejected. The peer-graph
655
+ # per phase MUST be acyclic.
656
+ #
657
+ # Runner behavior (see runner-output-contract.yaml > skip_result.reasons.peer_substituted):
658
+ # - When the substitute step passes AND the target peer would otherwise
659
+ # grade `missing_tool` / `missing_test_controller`, the runner MUST
660
+ # grade the target peer with skip reason `peer_substituted` (not
661
+ # `missing_tool`) and MUST NOT cascade `prerequisite_failed` to
662
+ # downstream stateful steps in subsequent phases.
663
+ # - When the substitute step itself fails or is skipped, the cascade
664
+ # proceeds as if `provides_state_for` were absent — the substitute
665
+ # did not establish the state it claimed.
666
+ #
667
+ # See adcontextprotocol/adcp#3734 (mechanism rationale) and
668
+ # adcp-client#1130 (the cascade-skip default this field rescues).
669
+ #
670
+ # context_outputs: array of ContextOutput objects (optional — capture values from
671
+ # this step's response into the runner's context accumulator so later steps can
672
+ # reference them via $context.<name>). Each entry:
673
+ # - name: string (the context variable to populate; MUST be unique within the
674
+ # storyboard run)
675
+ # path: string (JSON path against this step's response body, e.g.
676
+ # "media_buy_id", "accounts[0].account_id", "plans[0].plan_id")
677
+ # generate: string (alternative to `path`: mint a value at run start
678
+ # rather than capture from a response. Supported generator names:
679
+ # - uuid_v4 — RFC 4122 v4 UUID
680
+ # - opaque_id — RFC 4122 v4 UUID (alias; the two names exist for
681
+ # spec-vs-implementation framing — use uuid_v4 when
682
+ # the threaded value will appear as a UUID on the
683
+ # wire, opaque_id when callers treat it as an
684
+ # opaque correlation token).
685
+ # Exactly one of `path` or `generate` MUST be set on each entry.
686
+ # Setting both is a storyboard-load failure. Generated values are
687
+ # materialized once per storyboard run and stored under
688
+ # `context.<name>` so later steps' `$context.<name>` substitutions
689
+ # resolve to the same value. Use for deterministic correlation
690
+ # IDs (e.g. threading task_id through force_create_media_buy_arm
691
+ # register → create_media_buy → force_task_completion → tasks/get
692
+ # poll) without authors copy-pasting magic strings. Requires
693
+ # runner >= adcp-client v6.x with `generate` support (see
694
+ # adcp-client#1006); older runners reject the field at load.
695
+ #
696
+ # Example — deterministic task_id threading:
697
+ # context_outputs:
698
+ # - name: forced_task_id
699
+ # generate: uuid_v4
700
+ # # Subsequent steps reference $context.forced_task_id
701
+ # # in sample_request / context_inputs.
702
+ #
703
+ # Special path prefix `task_completion.<inner>`: when the immediate response
704
+ # is a non-terminal task envelope (status `submitted` / `working` /
705
+ # `input-required`, carrying a `task_id`), the runner polls `tasks/get`
706
+ # until the task reaches a terminal state and resolves `<inner>` against
707
+ # the completion artifact's `data` instead of the immediate response. Use
708
+ # for captures whose value only exists on the completion artifact — e.g.
709
+ # the seller-assigned `media_buy_id` on an IO-signing / async-signed HITL
710
+ # flow where `create_media_buy` returns `submitted` and the ID lands on
711
+ # the completion artifact. Without the prefix, the literal key
712
+ # `task_completion` is looked up on the immediate response, which fails
713
+ # as `capture_path_not_resolvable`. Requires runner >= adcp-client v6.7;
714
+ # older runners treat the prefix as a literal key. See adcp-client#1417
715
+ # (rationale) and adcp-client#1426 (implementation).
716
+ #
717
+ # Runner behavior:
718
+ # - Captures occur AFTER the step's validations pass. A failed step MUST NOT
719
+ # populate the context accumulator — downstream $context.<name> references
720
+ # resolve to an unresolved_substitution error, which grades the dependent
721
+ # step as failed rather than the storyboard passing on fabricated state.
722
+ # - Paths that do not resolve to a usable value in the response body (absent,
723
+ # null, or "") are a runner-side grading failure on THIS step
724
+ # (capture_path_not_resolvable), not on the downstream reader — the capture
725
+ # declared a contract that the response did not meet. The capturing step
726
+ # grades as failed; downstream steps that depended on the captured value
727
+ # grade as skipped with reason: prerequisite_failed citing this step.
728
+ # See runner-output-contract.yaml > validation_result for the output shape.
729
+ # - The accumulator is storyboard-run-scoped; values do not leak across runs.
730
+ # - Special path prefix `task_completion.<inner>`: when the immediate
731
+ # response is a non-terminal task envelope (status submitted/working,
732
+ # with a task_id), the runner polls `tasks/get` until the task exits
733
+ # {submitted, working}. On `completed`: resolves <inner> against the
734
+ # `result` payload; a missing path grades this step as
735
+ # capture_path_not_resolvable. On any other status (failed, canceled,
736
+ # rejected, auth-required, input-required, unknown): `result` is absent;
737
+ # the capture grades as capture_path_not_resolvable on this step;
738
+ # downstream $context.<name> references grade as prerequisite_failed
739
+ # citing this step. See adcp-client#1426 for the implementation.
740
+ #
741
+ # context_inputs: array of ContextInput objects (optional — inject captured
742
+ # context values into the request at a specific path AFTER the per-task
743
+ # request builder runs).
744
+ #
745
+ # Shape:
746
+ # context_inputs:
747
+ # - key: product_format_id # name populated by a prior context_outputs capture
748
+ # inject_at: "format_ids[0]" # JSON path inside the request body
749
+ #
750
+ # Do NOT use `context_inputs` unless `$context.<name>` inside
751
+ # `sample_request:` has been tried and verified not to reach the wire.
752
+ # The default substitution path is `$context.<name>` placeholders inside
753
+ # `sample_request:` — keep the request body honest about what the agent
754
+ # will receive. Reasons the default is preferred:
755
+ # - The published sample_response/sample_request pair is what seller
756
+ # engineers read to implement against the storyboard; anything the
757
+ # runner injects out-of-band is invisible there.
758
+ # - `$context.<name>` produces a pointed `unresolved_substitution` failure
759
+ # when the referenced capture is missing. `context_inputs` is
760
+ # silently a no-op on a missing `key` (runner:
761
+ # `if (input.key in context) setPath(...)`) — a typo in `key` or a
762
+ # failed capture produces an empty field rather than a loud error.
763
+ #
764
+ # Legitimate uses:
765
+ # - The @adcp/client per-task request builder for a given tool discards
766
+ # body fields from `sample_request` (the runner's post-builder merge
767
+ # only forwards envelope fields: `context`, `ext`, `idempotency_key`,
768
+ # `push_notification_config`). The observable symptom is that the
769
+ # agent answers with an unfiltered/default result and a round-trip
770
+ # or substitution-observer validation grades on fabricated state.
771
+ # `context_inputs` runs AFTER the builder, so it round-trips the
772
+ # captured value to the wire regardless.
773
+ # - Dependency-aware multi-instance dispatch, where the runner needs to
774
+ # route a step to a specific replica URL derived from a prior step's
775
+ # response (not applicable to single-instance storyboards).
776
+ #
777
+ # Value types are preserved. The captured value is written to `inject_at`
778
+ # as-is — object and numeric captures are NOT stringified, matching the
779
+ # `$context.<name>` behavior described below.
780
+ #
781
+ # Document every SDK-gap-workaround entry inline. The comment MUST name
782
+ # the SDK version that carries the gap and the upstream PR that closes
783
+ # it, so the entry can be removed when we bump past that release. See
784
+ # `static/compliance/source/protocols/media-buy/index.yaml`
785
+ # (`list_formats_integrity` step) for a worked example tied to
786
+ # adcontextprotocol/adcp-client#789.
787
+ #
788
+ # validations: array of Validation objects (optional)
789
+ #
790
+ # expect_error: boolean (optional, default false — when true, the runner expects
791
+ # the agent to return an error response for this step. Paired with `negative_path`
792
+ # to control whether the step's `sample_request` is schema-validated.)
793
+ #
794
+ # negative_path: "schema_invalid" | "payload_well_formed" (optional, default "schema_invalid")
795
+ # Disambiguates two categories of negative-path steps when `expect_error: true`:
796
+ # - schema_invalid (default): the `sample_request` payload is intentionally malformed
797
+ # (missing required field, wrong type, bad enum value, etc.) to verify the agent's
798
+ # validation response. Schema validation is SKIPPED on this step.
799
+ # - payload_well_formed: the `sample_request` payload is schema-valid but the agent
800
+ # rejects it at runtime — wrong state-machine transition, resource not found,
801
+ # governance denial, auth failure, temporal constraint, etc. Schema validation
802
+ # RUNS even though the agent is expected to return an error. Shape drift on these
803
+ # steps is caught statically.
804
+ # Omitting `negative_path` when `expect_error: true` behaves as `schema_invalid`
805
+ # (backwards-compatible default).
806
+ #
807
+ # sample_request_skip_schema: boolean (optional — explicit opt-out of schema
808
+ # validation for a step regardless of `expect_error` / `negative_path`. Use
809
+ # sparingly; prefer `negative_path: schema_invalid` for intentionally malformed
810
+ # negative-path fixtures.)
811
+ #
812
+ # expected_arm: string (optional — names the discriminator value of the `oneOf`
813
+ # arm this step expects the agent to return. When present, both the
814
+ # context-output-paths and validations-paths lints restrict path resolution
815
+ # to the matching arm only. Default behavior (no annotation) accepts a path
816
+ # that resolves through ANY oneOf arm — the right call for "no prior info,"
817
+ # but it lets a step capture or assert on a path that's defined on, say, the
818
+ # Acquired arm even when the storyboard semantically expects the Rejected
819
+ # arm. The annotation tightens that.
820
+ #
821
+ # Match shape: the lint walks each `oneOf` / `anyOf` branch and looks for
822
+ # ANY property with `const: "<expected_arm>"`. The first matching branch
823
+ # is the expected arm; subsequent path resolution restricts to it.
824
+ #
825
+ # Examples:
826
+ # expected_arm: "acquired" # matches AcquireRightsAcquired (status.const = "acquired")
827
+ # expected_arm: "rejected" # matches AcquireRightsRejected
828
+ # expected_arm: "pending_approval" # matches AcquireRightsPendingApproval
829
+ #
830
+ # When `expect_error: true` is set without `expected_arm`, the lint also
831
+ # tries to find an Error arm (one whose `required` list includes `errors`
832
+ # and which has no `status` const) and restricts to it. This covers
833
+ # `acquire_rights` / `creative_approval` / similar response shapes whose
834
+ # Error arm isn't const-discriminated.
835
+ #
836
+ # When the named arm doesn't match any branch, the lint emits an
837
+ # `unknown_expected_arm` violation — an authoring bug to fix at storyboard
838
+ # author time, not at runtime.
839
+ #
840
+ # --- Phase (optional fields for conditional execution) ---
841
+ #
842
+ # optional: boolean (default false — see semantics below)
843
+ # skip_if: string (expression — phase is skipped when the expression evaluates to true,
844
+ # e.g., "!test_kit.auth.api_key")
845
+ # branch_set: object (optional — first-class declaration of branch-set membership;
846
+ # see "Branch sets" section below)
847
+ #
848
+ # Optional phase semantics:
849
+ # - An `optional: true` phase always runs unless `skip_if` evaluates to true.
850
+ # `skip_if` (e.g., "!test_kit.auth.api_key") means "skip this phase when the
851
+ # expression is true"; without `skip_if`, the phase runs every time.
852
+ # - Failures inside an optional phase do NOT fail the overall storyboard on
853
+ # their own — the phase is for accumulating contributions toward a later
854
+ # `assert_contribution` check. The final assert is what fails the storyboard
855
+ # when no optional path succeeded.
856
+ # - A non-optional phase that fails fails the storyboard unconditionally.
857
+ #
858
+ # Branch sets:
859
+ # A branch set is a group of peer optional phases exercising mutually
860
+ # exclusive agent behaviors for the same trigger — e.g., an agent that
861
+ # rejects a past start_time vs. an agent that accepts-and-adjusts. A
862
+ # later `assert_contribution` step over the set's `branch_set.id` fails
863
+ # the storyboard only when NO branch contributed.
864
+ #
865
+ # Minimal example — two peer phases and the assertion that grades them:
866
+ #
867
+ # phases:
868
+ # - id: past_start_reject_path
869
+ # optional: true
870
+ # branch_set:
871
+ # id: past_start_handled
872
+ # semantics: any_of
873
+ # steps:
874
+ # - id: create_buy_past_start_reject
875
+ # task: create_media_buy
876
+ # contributes_to: past_start_handled
877
+ # # ... expect INVALID_REQUEST ...
878
+ #
879
+ # - id: past_start_adjust_path
880
+ # optional: true
881
+ # branch_set:
882
+ # id: past_start_handled
883
+ # semantics: any_of
884
+ # steps:
885
+ # - id: create_buy_past_start_adjust
886
+ # task: create_media_buy
887
+ # contributes_to: past_start_handled
888
+ # # ... expect media_buy_id ...
889
+ #
890
+ # - id: past_start_enforcement
891
+ # steps:
892
+ # - task: assert_contribution
893
+ # validations:
894
+ # - check: any_of
895
+ # allowed_values: [past_start_handled]
896
+ #
897
+ # `@adcp/client` 5.8.0+ also accepts the boolean shorthand
898
+ # `contributes: true` on a step inside a branch_set phase — the runner's
899
+ # loader resolves it to the enclosing phase's `branch_set.id`. The two
900
+ # forms are semantically identical; `contributes: true` is preferred
901
+ # inside a branch_set phase because it eliminates the typo surface (the
902
+ # string form must equal `branch_set.id` exactly). Authors may use
903
+ # either; the lint enforces both.
904
+ #
905
+ # New storyboards MUST declare branch-set membership with a `branch_set:`
906
+ # object. The implicit-detection fallback below exists only so pre-#2633
907
+ # storyboards keep running unchanged.
908
+ #
909
+ # `branch_set:` field shape:
910
+ # - id: non-empty string. MUST equal the `contributes_to` value
911
+ # on every contributing step inside the phase and MUST
912
+ # appear in the `assert_contribution` step's any_of
913
+ # allowed_values.
914
+ # - semantics: `any_of`. No other value is supported today.
915
+ #
916
+ # Per-step contribution signalling is REQUIRED on each contributing step —
917
+ # the phase-level `branch_set:` declares membership but does not replace
918
+ # the step-level flag. Runners accumulate contributions at the step level.
919
+ # Two equivalent forms:
920
+ # - `contributes: true` (preferred inside a branch_set phase; resolves
921
+ # to the enclosing phase's `branch_set.id`)
922
+ # - `contributes_to: <branch_set.id>` (string form; required outside a
923
+ # branch_set phase and must equal `branch_set.id` inside one)
924
+ #
925
+ # Authoring rules (enforced by `lint:storyboard-branch-sets`):
926
+ # - A phase with `branch_set:` MUST set `optional: true`. A
927
+ # non-optional phase's failure would fail the storyboard
928
+ # unconditionally and defeat the any_of semantics.
929
+ # - All phases in a storyboard sharing the same `branch_set.id` MUST
930
+ # share the same `branch_set.semantics`.
931
+ # - A storyboard with a `branch_set:` declaration MUST contain an
932
+ # `assert_contribution` step whose `validations[].check: any_of`
933
+ # includes the `branch_set.id` in `allowed_values`. Any
934
+ # `contributes_to:` / `contributes: true` whose resolved flag has no
935
+ # matching assert_contribution is flagged as `orphan_contribution` —
936
+ # nothing grades it.
937
+ # - Any step inside a branch-set phase that declares `contributes_to`
938
+ # MUST use the value `<branch_set.id>`.
939
+ # - A step MUST NOT declare both `contributes` and `contributes_to`
940
+ # (ambiguous). `contributes: true` is legal only inside a phase
941
+ # that declares `branch_set:`.
942
+ # - If any phase declares `branch_set: { id: X, ... }`, every optional
943
+ # phase in the same storyboard whose steps contribute to X MUST also
944
+ # declare `branch_set: { id: X, ... }`. A mixed-mode storyboard (one
945
+ # peer declared, one peer relying on implicit detection) would be
946
+ # graded as a single-member set by a runner preferring the explicit
947
+ # declaration.
948
+ #
949
+ # Runner grading:
950
+ # Runners MUST prefer the explicit `branch_set:` declaration when
951
+ # present and fall back to implicit detection otherwise (see below).
952
+ # - If at least one peer branch contributed the `branch_set.id`, the
953
+ # non-contributing branch's failing steps MUST be graded with skip
954
+ # reason `peer_branch_taken` (see
955
+ # runner-output-contract.yaml > skip_result.reasons.peer_branch_taken).
956
+ # They MUST NOT surface as `failed` in the storyboard summary, and
957
+ # MUST NOT be conflated with `not_applicable` (reserved for coverage
958
+ # gaps — agent didn't declare the protocol).
959
+ # - If no branch contributed, the final `assert_contribution` step
960
+ # fails the storyboard. Individual branch step failures still grade
961
+ # `failed` so the implementor sees what each branch observed.
962
+ #
963
+ # When to use a branch set:
964
+ # Only when the AdCP spec permits multiple conformant behaviors for
965
+ # the same trigger. If the spec mandates one behavior, write a
966
+ # required phase — a branch set where only one branch is legal
967
+ # weakens the test to any_of and hides regressions. Branch sets are
968
+ # not for optional features (use `skip_if` and coverage flags) or
969
+ # for progressive disclosure (use sequential phases).
970
+ #
971
+ # Authoring guidance: keep branch sets flat — two or three peer phases
972
+ # is typical. Nested branch sets (a branch set whose member is itself a
973
+ # branch set) are out of scope for runners. If a scenario needs deeper
974
+ # branching, restructure into separate storyboards or a linear sequence.
975
+ #
976
+ # Implicit branch-set detection (DEPRECATED — pre-#2633 storyboards only):
977
+ # When no phase in a storyboard declares `branch_set:`, runners fall
978
+ # back to the legacy rule: a branch set is the set of `optional: true`
979
+ # phases whose steps declare the same `contributes_to: <flag>` value
980
+ # as a later `assert_contribution` step's
981
+ # `validations[].check: any_of, allowed_values: [<flag>]` target.
982
+ # Fragile against typos in `contributes_to:` — new storyboards MUST
983
+ # use the explicit form above. This fallback exists only so
984
+ # pre-#2633 storyboards keep running during migration.
985
+ #
986
+ # Cross-storyboard contradiction lint:
987
+ # The `lint:storyboard-contradictions` script groups every step-with-
988
+ # assertions across every storyboard by (task, canonicalized request
989
+ # fingerprint, prior-state fingerprint, env fingerprint) and flags
990
+ # groups whose asserted outcomes disagree in a way no conformant agent
991
+ # can satisfy (success ↔ error, or disjoint error-code sets).
992
+ #
993
+ # Env fingerprint includes the storyboard's top-level `id:`, so two
994
+ # separate storyboards exercising different controller-seeded states
995
+ # are legitimately independent test suites and do not collide. To make
996
+ # two different storyboards' assertions collide — e.g., to catch
997
+ # "storyboard A assumes media_buy_id X is active, storyboard B
998
+ # assumes it is canceled" — use matching `comply_scenario:` values or
999
+ # shared `prerequisites.controller_seeding`.
1000
+ #
1001
+ # Branch-set peers (two optional phases in the same storyboard sharing
1002
+ # a `branch_set.id`) are exempt from contradiction flagging by design:
1003
+ # any_of semantics intentionally asserts mutually exclusive outcomes.
1004
+ #
1005
+ # Mutating-task detection: the lint's prior-state fingerprint partitions
1006
+ # steps by the ordered list of prior MUTATING tasks. A task is mutating
1007
+ # iff its request schema declares `"x-mutates-state": true` at the top
1008
+ # level. The annotation is consumed by
1009
+ # `scripts/lint-storyboard-contradictions.cjs` (which builds the
1010
+ # mutating-task set at lint time) and is the single source of truth for
1011
+ # this concern.
1012
+ #
1013
+ # Decidability rule (apply to any new task):
1014
+ # A task declares `"x-mutates-state": true` iff a conformant compliance
1015
+ # suite could write an assertion — on any later `get_*`, `list_*`, or
1016
+ # follow-up call — whose outcome depends on this request having run.
1017
+ # Equivalently: "this task changes observable server state a later
1018
+ # conformant call may assert against."
1019
+ #
1020
+ # Mutating task classes (illustrative; the decidability rule above is
1021
+ # the actual test):
1022
+ # - Writes (create/update/delete/sync/build) — resource lifecycle.
1023
+ # - State transitions (cancel, approve, reject) — status moves.
1024
+ # - Session-scoped primitives (si_initiate_session, si_send_message,
1025
+ # si_terminate_session) — session lifecycle.
1026
+ # - Reporting/audit writes (log_event, report_usage,
1027
+ # report_plan_outcome, provide_performance_feedback) — mutating
1028
+ # because `get_plan_audit_logs` and billing-summary tasks can assert
1029
+ # against their recorded outputs.
1030
+ #
1031
+ # Non-mutating task classes:
1032
+ # - Read-only tasks (get_*, list_*, check_*, validate_*, preview_*) do
1033
+ # not declare `x-mutates-state` UNLESS the read itself produces
1034
+ # observable state a later task can assert against (e.g., a view
1035
+ # counter that `get_analytics` can read) — those are mutating
1036
+ # regardless of prefix.
1037
+ # - Pure-compute tasks (no state read or write — estimates,
1038
+ # calculations) MUST NOT declare `x-mutates-state`.
1039
+ #
1040
+ # Relationship to idempotency_key (decoupled, do not unify):
1041
+ # `x-mutates-state` declares mutation semantics. `required:
1042
+ # [idempotency_key]` declares the idempotency mechanism. The sets
1043
+ # overlap ~95% but legitimately diverge for naturally-idempotent
1044
+ # mutations. Example: `comply_test_controller` declares
1045
+ # `x-mutates-state: true` but omits `idempotency_key` from `required`
1046
+ # because the `scenario` enum is the dedup boundary — replaying
1047
+ # `force_media_buy_status=active` converges to the same observable
1048
+ # state without a key. `si_terminate_session` is the same shape
1049
+ # (session_id is the dedup boundary). `scripts/build-compliance.cjs`
1050
+ # reads `idempotency_key` for its own enforcement; do not try to share
1051
+ # the two reads.
1052
+ #
1053
+ # --- Validation ---
1054
+ #
1055
+ # CANONICAL CHECK ENUM
1056
+ # --------------------
1057
+ # The authored check kinds storyboards MAY declare in `step.validations[].check`
1058
+ # live in `runner-output-contract.yaml > authored_check_kinds` (structured
1059
+ # YAML; this file is comment-only documentation). The build-time lint
1060
+ # `scripts/lint-storyboard-check-enum.cjs` reads from there; runtime
1061
+ # forward-compat (unknown kinds → not_applicable) is documented in
1062
+ # runner-output-contract.yaml > validation_result. Add new kinds to that
1063
+ # enum AND document semantics here in the same PR.
1064
+ #
1065
+ # check: string (what to validate: "response_schema", "field_present",
1066
+ # "envelope_field_present" / "envelope_field_absent" (walks
1067
+ # protocol-envelope.json instead of response_schema_ref — use for
1068
+ # top-level envelope fields like `status`, `task_status`, and
1069
+ # `response_status`),
1070
+ # "field_value",
1071
+ # "field_value_or_absent", "status_code", "http_status", "http_status_in",
1072
+ # "error_code", "on_401_require_header", "resource_equals_agent_url",
1073
+ # "any_of", "refs_resolve", "a2a_submitted_artifact",
1074
+ # "field_less_than" / "field_equals_context" (compare a field on this
1075
+ # step's response against a value captured by a prior step — see
1076
+ # "Cross-step comparison" section below),
1077
+ # "upstream_traffic" (asserts side-effects via comply_test_controller's
1078
+ # query_upstream_traffic scenario — see "upstream_traffic" section below),
1079
+ # "canonical_format_satisfaction" (asserts create_media_buy package
1080
+ # selectors against prior get_products format_options[] context — see
1081
+ # "Canonical format satisfaction" section below))
1082
+ # severity: string (optional, default "required"): "required" | "advisory".
1083
+ # A "required" validation failure fails the step and contributes
1084
+ # to steps_failed; this is the default. An "advisory" failure
1085
+ # surfaces in the validation_result with passed: false but does
1086
+ # NOT fail the step — the step grades on its remaining required
1087
+ # validations. Advisory failures contribute to a distinct
1088
+ # `validations_advisory_failed` counter on run_summary so they
1089
+ # stay visible without polluting conformance verdicts.
1090
+ #
1091
+ # Use case: a storyboard declares a check whose runner support
1092
+ # is rolling out (e.g., upstream_traffic during the months
1093
+ # between the contract spec landing and the @adcp/sdk runner
1094
+ # shipping it). Authors flip severity: advisory while
1095
+ # instrumentation matures, then drop the field (or set
1096
+ # "required") once adoption is stable. Without this escape
1097
+ # valve, the choice was binary: declare the check and break
1098
+ # conformance for adopters who haven't caught up, or omit it
1099
+ # entirely and lose the validation surface during rollout.
1100
+ # Distinct from the runtime forward-compat default (unknown
1101
+ # check kinds → not_applicable): forward-compat handles
1102
+ # runner-spec version skew; severity handles author-managed
1103
+ # rollout gating.
1104
+ # expires_after_version: string (optional, only meaningful when severity:
1105
+ # advisory): semver gating runtime promotion of this advisory
1106
+ # to "required". Closes the advisory-drift problem:
1107
+ # storyboards declared with severity: advisory during an
1108
+ # adoption window get force-promoted once runners catch up,
1109
+ # so authors don't need to track every advisory entry across
1110
+ # every PR.
1111
+ #
1112
+ # Versioning anchor: the comparison is against the runner's
1113
+ # self-declared `runner_capability_version` (see
1114
+ # runner-output-contract.yaml > run_summary), NOT against
1115
+ # any specific implementation's package version. The runner
1116
+ # chooses what its capability version corresponds to —
1117
+ # @adcp/sdk semver, the AdCP spec version it implements, or
1118
+ # a runner-binary version — and declares it in run_summary.
1119
+ # Storyboard authors target the spec capability they need,
1120
+ # not a specific SDK package; runners self-report the
1121
+ # capability they offer.
1122
+ #
1123
+ # Runner behavior: at storyboard-load time the runner
1124
+ # compares its own runner_capability_version to
1125
+ # expires_after_version using semver.gte. If
1126
+ # runner_capability_version >= expires_after_version, the
1127
+ # runner MUST promote severity to "required" — advisory
1128
+ # failures then fail the step. The validation_result MUST
1129
+ # carry severity_promoted_from_advisory: true to surface the
1130
+ # promotion on rendered reports.
1131
+ #
1132
+ # Forward-compat ordering: when a storyboard declares both
1133
+ # an unknown check kind (graded `not_applicable` per #3816's
1134
+ # forward-compat clause) AND severity: advisory +
1135
+ # expires_after_version, forward-compat resolves FIRST. The
1136
+ # check grades not_applicable and severity_promoted_from_advisory
1137
+ # MUST be absent — no promotion was evaluated because no
1138
+ # grading occurred. Promotion only applies to checks the
1139
+ # runner actually grades.
1140
+ #
1141
+ # Pre-release tags: comparison uses standard semver including
1142
+ # pre-releases (RFC §11). `6.5.0-rc.3 < 6.5.0`. Authors
1143
+ # wanting "after 6.5.0 stable" write `6.5.0`; authors
1144
+ # wanting "after any 6.5.0 pre-release" write `6.5.0-0`.
1145
+ # The runner MUST use semver.gte semantics, not a custom
1146
+ # comparator. Authors targeting a specific minor without
1147
+ # regard to pre-release status SHOULD write the lowest
1148
+ # pre-release form (`X.Y.0-0`) when they want to include
1149
+ # early adopters running RC builds.
1150
+ #
1151
+ # Permanent advisories — see `permanent_advisory` field
1152
+ # below for declaring an advisory that's deliberately not
1153
+ # gated on runner version.
1154
+ #
1155
+ # Format: a semver-valid string. The build-time lint at
1156
+ # scripts/lint-storyboard-advisory-expiry.cjs MUST validate
1157
+ # the value via Node's `semver.valid()` and reject malformed
1158
+ # values; this prevents typos AND bounds the trust surface
1159
+ # on the value when it's interpolated into rendered reports.
1160
+ #
1161
+ # permanent_advisory: object (optional, only meaningful when severity:
1162
+ # advisory and expires_after_version is absent): structured
1163
+ # marker declaring this advisory is deliberately not gated
1164
+ # on runner version. Use case: experimental signals where
1165
+ # the spec authors explicitly want the advisory grade to
1166
+ # persist regardless of runner adoption.
1167
+ #
1168
+ # Shape:
1169
+ # permanent_advisory:
1170
+ # reason: "<text>"
1171
+ #
1172
+ # Replaces the earlier `# advisory-permanent: <reason>` YAML
1173
+ # comment marker (which broke under YAML round-tripping in
1174
+ # editor formatters and downstream tooling). Structured
1175
+ # field is greppable, schema-validatable, and the reason is
1176
+ # machine-readable for dashboards.
1177
+ #
1178
+ # Schema requirement: when severity: advisory is declared,
1179
+ # exactly one of `expires_after_version` or
1180
+ # `permanent_advisory` MUST be present. The build-time lint
1181
+ # surfaces violations (warning, not error — drift is a
1182
+ # judgment call).
1183
+ # path: string (JSON path to the field, e.g., "formats[0].format_id")
1184
+ # value: any (expected value — string, number, or boolean; required when check is "field_value";
1185
+ # accepted (optional) when check is "field_value_or_absent";
1186
+ # accepted (optional) on "field_less_than" as a literal comparand when
1187
+ # `context_key` is not set)
1188
+ # allowed_values: array (acceptable values for "field_value", "field_value_or_absent",
1189
+ # "http_status_in", "error_code", "any_of")
1190
+ # context_key: string (only meaningful for "field_less_than" / "field_equals_context"
1191
+ # cross-step comparison checks — names a key populated by a prior
1192
+ # step's `context_outputs`. Required on "field_equals_context";
1193
+ # optional on "field_less_than" (falls back to the literal `value`
1194
+ # when omitted). Ignored on every other check kind. When set and
1195
+ # the key is absent from the storyboard accumulator, the check
1196
+ # passes with a `context_key_absent` observation — see
1197
+ # "Cross-step comparison" section below.)
1198
+ # description: string (human-readable description of validation)
1199
+ #
1200
+ # field_value_or_absent check:
1201
+ # Passes when the field is absent OR present and equal to `value` / contained in
1202
+ # `allowed_values`; fails only when the field is present with a disallowed value.
1203
+ # Use for fields that the spec permits to be omitted but MUST NOT carry a wrong value
1204
+ # when present (e.g., a `replayed` flag that MAY be omitted on a fresh path but MUST
1205
+ # NOT be true). Accepts both `value` (single expected value) and `allowed_values` (set
1206
+ # membership); at least one of the two MUST be provided. A `field_value_or_absent` check
1207
+ # that declares neither `value` nor `allowed_values` is a storyboard-load error; runners
1208
+ # MUST reject the storyboard before execution begins.
1209
+ # For fields that are required by the response schema, pair this check with a separate
1210
+ # `check: field_present` to avoid silently accepting absent fields that are required.
1211
+ #
1212
+ # Error-shape validation — use `check: error_code`:
1213
+ # The `error_code` check is shape-agnostic. The runner resolves the code from
1214
+ # any of the transport-binding locations defined by the client detection
1215
+ # order (see docs/building/implementation/transport-errors#client-detection-order):
1216
+ # - `adcp_error.code` (MCP structuredContent, A2A artifact DataPart, JSON-RPC
1217
+ # error.data, MCP text-fallback content JSON)
1218
+ # - `errors[0].code` (task-payload errors array, top-level or under payload)
1219
+ # A storyboard SHOULD assert error shape via `check: error_code` rather than
1220
+ # `check: field_present, path: "errors"` or `path: "adcp_error"` — pinning to
1221
+ # a specific shape makes the validator flakey against conformant agents that
1222
+ # surface errors on the other layer.
1223
+ #
1224
+ # Error-code vocabulary:
1225
+ # Every code referenced in a `value:` or `allowed_values:` under `check: error_code`
1226
+ # MUST exist in the canonical enum at `static/schemas/source/enums/error-code.json`
1227
+ # (or be registered as a deprecation alias). The `lint:error-codes` script
1228
+ # enforces this at build time — references to undefined codes fail the build.
1229
+ # Sellers MAY emit vendor-specific codes outside the enum, but storyboards
1230
+ # MUST NOT assert on them (that couples the conformance suite to a specific
1231
+ # vendor's taxonomy). When a test needs to accept multiple possible codes,
1232
+ # use `allowed_values: [...]` rather than a single-code `value:` assertion.
1233
+ #
1234
+ # Canonical format satisfaction — use `check: canonical_format_satisfaction`:
1235
+ # Applies only to create_media_buy steps. The runner compares each package's
1236
+ # format selector against the target product captured from prior get_products
1237
+ # context. When multiple get_products responses exist, the runner MUST use
1238
+ # the most recent prior get_products response containing the package's
1239
+ # product_id; if no such response exists, the check fails as an authoring or
1240
+ # setup error. `value: true` means the request should be accepted; `value:
1241
+ # false` means the request should be rejected for a format-selector cause.
1242
+ #
1243
+ # The check normalizes legacy `format_ids[]` through `v1_format_ref[]` and the
1244
+ # v1-to-canonical registry, resolves `format_option_refs[]`, and applies
1245
+ # directional product gating for fixed dimensions, duration declarations,
1246
+ # canonical params, and range containment. Negative cases only pass when the
1247
+ # observed rejection is attributable to formats, so unrelated auth or tenant
1248
+ # failures do not mask selector bugs.
1249
+ #
1250
+ # Fields:
1251
+ # check: canonical_format_satisfaction
1252
+ # value: boolean # true = expected accepted; false = expected rejected.
1253
+ # path: string # optional; selects one package object or package array.
1254
+ # # default is the request payload's packages[] array.
1255
+ #
1256
+ # Example:
1257
+ # - check: canonical_format_satisfaction
1258
+ # value: false
1259
+ # description: "Bare image selector lacks the dimensions required by the fixed product"
1260
+ #
1261
+ # Cross-step integrity — use `check: refs_resolve`:
1262
+ # `refs_resolve` asserts every ref in a source set resolves to a member of a
1263
+ # target set, matched on declared keys. Use it when one step's response
1264
+ # contains references (e.g., `format_ids` on `products`) that MUST exist in
1265
+ # another step's response (e.g., `formats` from `list_creative_formats`).
1266
+ # Without this primitive, broken references are silent until a later call
1267
+ # (e.g., `sync_creatives`) fails at runtime, after commitments are already
1268
+ # made.
1269
+ #
1270
+ # Fields:
1271
+ # source:
1272
+ # from: current_step | context
1273
+ # # `current_step` reads the step's task-result data;
1274
+ # # `context` reads values captured by prior steps.
1275
+ # path: <string> # supports `[*]` wildcards — see below.
1276
+ # target:
1277
+ # from: current_step | context
1278
+ # path: <string>
1279
+ # match_keys: [<key>, ...] # keys compared on each ref — e.g. [agent_url, id].
1280
+ # # A ref missing any declared key is NEVER a match
1281
+ # # (agents that drop a key don't fuzzy-match others
1282
+ # # that also dropped it).
1283
+ # scope: # optional — restrict integrity to in-scope refs.
1284
+ # key: <string> # e.g. `agent_url`.
1285
+ # equals: <string> # literal value, or `$agent_url` for the
1286
+ # # runner target URL. Keys ending in `url`
1287
+ # # get trailing-slash / case normalization
1288
+ # # on both sides before compare. Transport
1289
+ # # path canonicalization (`/mcp`, well-known
1290
+ # # A2A card path) for `$agent_url` is tracked
1291
+ # # in adcp-client#710 — until it lands, MCP
1292
+ # # agents should expect refs to fall
1293
+ # # out-of-scope when format_ids name the
1294
+ # # bare agent URL.
1295
+ # on_out_of_scope: warn | ignore | fail
1296
+ # # how refs outside `scope` are graded.
1297
+ # # default `warn`: pass the check, attach
1298
+ # # observations naming the skipped refs.
1299
+ # # `ignore`: silent. `fail`: promote to
1300
+ # # missing so reports name them.
1301
+ #
1302
+ # Path wildcards:
1303
+ # `[*]` in a `source.path` or `target.path` flattens over arrays. So
1304
+ # `products[*].format_ids[*]` walks every product and flattens every
1305
+ # `format_ids` array into a single list of refs. Runners cap terminal
1306
+ # fan-out at 10,000 values to bound malicious agent responses; paths
1307
+ # realistic for any catalog size are well under the cap.
1308
+ #
1309
+ # Grading output:
1310
+ # A failing `refs_resolve` check names the specific unresolved ref tuples
1311
+ # in `actual.missing` (projected to `match_keys`). Duplicates are collapsed
1312
+ # on the projected tuple so one broken ref across 50 products shows up
1313
+ # once — compliance reports stay readable.
1314
+ #
1315
+ # Example — every `format_id` on products resolves to a format on this agent:
1316
+ # - check: refs_resolve
1317
+ # description: "Every format_id on products resolves to a format returned by list_creative_formats"
1318
+ # source:
1319
+ # from: context
1320
+ # path: "products[*].format_ids[*]"
1321
+ # target:
1322
+ # from: current_step
1323
+ # path: "formats[*].format_id"
1324
+ # match_keys: [agent_url, id]
1325
+ # scope:
1326
+ # key: agent_url
1327
+ # equals: $agent_url
1328
+ # on_out_of_scope: warn
1329
+ #
1330
+ # Cross-step comparison — use `check: field_less_than` / `field_equals_context`:
1331
+ # These two checks compare a field in THIS step's response against a value
1332
+ # captured by a PRIOR step via `context_outputs`. Without them, every existing
1333
+ # `check:` is single-step (`field_value` resolves the comparand at YAML
1334
+ # authoring time via `$context.<name>` substitution; the runtime value the
1335
+ # prior step actually returned is opaque to the validation).
1336
+ #
1337
+ # Use cases:
1338
+ # - Round-trip identity: assert that `media_buys[0].media_buy_id` on
1339
+ # `get_media_buys` equals the id returned by the earlier `create_media_buy`
1340
+ # step. Today's `field_value` + `$context.media_buy_id` substitution
1341
+ # resolves the placeholder against the request, not the response — a
1342
+ # seller could return a different id on the read and still pass.
1343
+ # - Filtered-vs-baseline: assert that a filtered `get_rights` returns a
1344
+ # strictly smaller `rights.length` than the unfiltered baseline call.
1345
+ # - Idempotency replay: assert that the replayed response's body hash
1346
+ # equals the originally captured hash (byte-for-byte cache hit).
1347
+ #
1348
+ # Fields:
1349
+ # check: field_less_than | field_equals_context
1350
+ # path: string # JSON path on this step's response.
1351
+ # context_key: string # name set by a prior step's
1352
+ # # `context_outputs[].name`. Required for
1353
+ # # `field_equals_context`. Optional for
1354
+ # # `field_less_than` — when omitted, the
1355
+ # # check falls back to the literal `value`
1356
+ # # (so authors can compare against a
1357
+ # # static threshold without context).
1358
+ # value: number # Literal comparand for `field_less_than`
1359
+ # # when `context_key` is not set. Ignored on
1360
+ # # `field_equals_context` (which always reads
1361
+ # # from context).
1362
+ # description: string
1363
+ #
1364
+ # Type semantics:
1365
+ # `field_less_than` requires both operands to be finite numbers; non-numeric
1366
+ # or absent values fail the check with a type error in the validation_result.
1367
+ # `field_equals_context` deep-equals the response value against the captured
1368
+ # comparand using the same equality semantics as `field_value`.
1369
+ #
1370
+ # Absent context_key — context_key_absent observation:
1371
+ # When `context_key` is set but the named entry is missing from the
1372
+ # storyboard accumulator (typically because the prior capturing step was
1373
+ # legitimately skipped on a branch-set path), the check PASSES with a
1374
+ # `context_key_absent` observation rather than failing. This preserves the
1375
+ # "skipped prerequisite cascades to skip, not fail" semantics that
1376
+ # `context_outputs` already establishes for `$context.<name>` substitution.
1377
+ # A genuinely missing capture (the prior step ran but its capture path did
1378
+ # not resolve) is graded `capture_path_not_resolvable` ON THE PRIOR STEP —
1379
+ # downstream cross-step checks on a never-populated key are not the place
1380
+ # to surface that failure.
1381
+ #
1382
+ # Examples:
1383
+ # # Round-trip: get_media_buys returns the id we just created.
1384
+ # # Pair with `context_outputs` on the earlier create_media_buy step:
1385
+ # # context_outputs:
1386
+ # # - name: created_media_buy_id
1387
+ # # path: "media_buy_id"
1388
+ # - check: field_equals_context
1389
+ # path: "media_buys[0].media_buy_id"
1390
+ # context_key: "created_media_buy_id"
1391
+ # description: "get_media_buys echoes the id returned by create_media_buy"
1392
+ #
1393
+ # # Filtered call returns fewer rights than baseline.
1394
+ # # Capture the baseline length on the unfiltered step:
1395
+ # # context_outputs:
1396
+ # # - name: baseline_rights_count
1397
+ # # path: "rights.length"
1398
+ # - check: field_less_than
1399
+ # path: "rights.length"
1400
+ # context_key: "baseline_rights_count"
1401
+ # description: "Filtered rights array is smaller than unfiltered baseline"
1402
+ #
1403
+ # # Idempotency replay: replayed body deep-equals the originally-captured one.
1404
+ # # Pair with `context_outputs` on the original call:
1405
+ # # context_outputs:
1406
+ # # - name: original_create_response
1407
+ # # path: "" # capture the entire response body
1408
+ # - check: field_equals_context
1409
+ # path: ""
1410
+ # context_key: "original_create_response"
1411
+ # description: "Replay returned the cached response body, deep-equal to the original"
1412
+ #
1413
+ # # Static threshold (single-step form — no context_key).
1414
+ # # NOTE: `field_less_than` is primarily a cross-step check; the literal-
1415
+ # # `value` form is a convenience for numeric thresholds. Authors who
1416
+ # # need same-step equality should reach for `field_value` instead.
1417
+ # - check: field_less_than
1418
+ # path: "delivery.spend"
1419
+ # value: 10000
1420
+ # description: "Spend stays under the buy's budget cap"
1421
+ #
1422
+ # A2A wire-shape — use `check: a2a_submitted_artifact`:
1423
+ # Asserts the A2A envelope invariants for AdCP `submitted` arms: `Task.id` /
1424
+ # `Task.contextId` non-empty, `Task.state` normalised to `completed` (A2A 0.3
1425
+ # wire value `"completed"`; A2A 1.0 enum `"TASK_STATE_COMPLETED"`), and the
1426
+ # last non-null, object-typed DataPart's `data.status === 'submitted'`
1427
+ # preserving the AdCP discriminator. Grades `not_applicable` on non-A2A
1428
+ # transports so storyboards can include it alongside MCP-shape assertions
1429
+ # without forking by transport.
1430
+ #
1431
+ # Upstream side-effects — use `check: upstream_traffic`:
1432
+ # Asserts that the agent caused observable upstream traffic with a payload
1433
+ # carrying the storyboard-supplied identifiers. The mechanism raises the bar
1434
+ # against UNINTENTIONAL façades — adapters (often LLM-generated) that satisfy
1435
+ # AdCP schema requirements with synthetic placeholders without forwarding the
1436
+ # payload upstream. It is NOT an adversarial integrity check: adopters
1437
+ # self-report their own traffic, and a determined façade could fabricate
1438
+ # recorded_calls. Spec consumers MUST NOT treat upstream_traffic passing as
1439
+ # cryptographic proof of adapter behavior; the value is "raises the bar
1440
+ # against accidental façades," not "stops malicious façades."
1441
+ #
1442
+ # The runner queries the adopter's comply_test_controller with scenario
1443
+ # `query_upstream_traffic`, scoped to traffic recorded since the storyboard
1444
+ # step's request timestamp, and applies the assertion declared on the
1445
+ # storyboard step. Adopters who do not advertise `query_upstream_traffic` in
1446
+ # `list_scenarios` grade the check `not_applicable` — the contract is opt-in
1447
+ # by adopter capability. Adopters who DO advertise it but return zero
1448
+ # recorded calls during the assertion window grade as failed (the
1449
+ # "controller present and observed nothing" path is the façade signal).
1450
+ #
1451
+ # Runner attestation-mode selection (normative):
1452
+ # Conforming runners MUST choose params.attestation_mode for
1453
+ # comply_test_controller query_upstream_traffic calls in a way that
1454
+ # preserves assertion coverage:
1455
+ # 1. If any upstream_traffic validation in the current step sets
1456
+ # attestation_mode_required: "raw", request "raw".
1457
+ # 2. Else if any upstream_traffic validation in the current step declares
1458
+ # payload_must_contain, request "raw"; arbitrary payload-path
1459
+ # assertions cannot be evaluated from digest attestations.
1460
+ # 3. Else if every upstream_traffic validation in the current step can
1461
+ # be evaluated from count/endpoint/purpose plus identifier_paths, and
1462
+ # the runner can supply params.identifier_value_digests for all
1463
+ # resolved identifier values, the runner SHOULD request "digest" when
1464
+ # it knows the matching upstream calls are JSON-shaped
1465
+ # (`application/json` or `*/*+json`). If the runner does not know the
1466
+ # matching calls are JSON-shaped, request "raw"; digest-mode
1467
+ # identifier_match_proofs are intentionally unavailable for non-JSON
1468
+ # payloads, so an unconditional digest request can turn an otherwise
1469
+ # evaluable identifier_paths assertion into not_applicable.
1470
+ # 4. Otherwise, request "raw".
1471
+ # Controllers MAY still return digest-mode attestations for a raw request
1472
+ # when local policy forbids raw payload disclosure; raw-required and
1473
+ # payload_must_contain assertions then grade not_applicable as described
1474
+ # below.
1475
+ # Storyboard authors SHOULD NOT invent non-schema attestation-mode hints.
1476
+ # Use attestation_mode_required: "raw" only for hard raw requirements;
1477
+ # otherwise rely on this runner selection rule.
1478
+ #
1479
+ # Fields:
1480
+ # min_count: integer (default: 1)
1481
+ # # Minimum number of recorded calls that match
1482
+ # # the optional `endpoint_pattern` filter. Use 0
1483
+ # # only for negative assertions (e.g., a step
1484
+ # # MUST NOT cause upstream traffic).
1485
+ # endpoint_pattern: string (optional)
1486
+ # # Glob pattern matched against
1487
+ # # recorded_calls[].endpoint. Examples:
1488
+ # # "POST */audience/upload"
1489
+ # # "POST *" (any POST, useful when
1490
+ # # the storyboard stays
1491
+ # # platform-agnostic)
1492
+ # # When omitted, all recorded calls in the window
1493
+ # # match. Storyboards in the shared corpus SHOULD
1494
+ # # stay platform-agnostic — host-specific patterns
1495
+ # # like "POST api.tiktok.com/*" couple the spec
1496
+ # # to specific upstream platforms and break for
1497
+ # # adopters whose upstream is something else.
1498
+ # # Reserve host-specific patterns for
1499
+ # # adapter-specific extensions, not shared
1500
+ # # storyboards.
1501
+ # payload_must_contain: array of MatchSpec (optional)
1502
+ # # Each entry asserts a path/value pair that MUST
1503
+ # # appear in at least one matching call's payload.
1504
+ # # Path syntax: JSONPath-lite positional path
1505
+ # # with `[*]` array wildcards. Shared storyboards
1506
+ # # SHOULD use the bare dotted form. Examples:
1507
+ # # "users[*].hashed_email"
1508
+ # # "data.audience.members[*].id"
1509
+ # # The literal `[*]` wildcard flattens over arrays
1510
+ # # and passes if any element matches. The runner
1511
+ # # also tolerates optional `$` / `$.` prefixes and
1512
+ # # standalone numeric index tokens for SDK
1513
+ # # compatibility (for example `items.[0].id`);
1514
+ # # shared storyboards should avoid indexes unless
1515
+ # # a position-specific assertion is intentional.
1516
+ # # RFC 9535 JSONPath descendant syntax (`$..foo`) is NOT
1517
+ # # supported — the runner uses
1518
+ # # JSONPath-lite matching from @adcp/sdk. Path matching only
1519
+ # # applies when recorded_calls[].content_type is
1520
+ # # JSON-shaped — defined as `application/json`
1521
+ # # or any structured-syntax-suffix `*/*+json`
1522
+ # # per RFC 6839 §3.1 (e.g., `application/vnd.api+json`,
1523
+ # # `application/scim+json`). All other content
1524
+ # # types (form-urlencoded, multipart, plain text,
1525
+ # # newline-delimited JSON like `application/json-seq`,
1526
+ # # JSON Lines `application/jsonl`) take the non-
1527
+ # # JSON path. Against non-JSON ALL match modes
1528
+ # # (`present` / `equals` / `contains_any`) grade
1529
+ # # `not_applicable`. Earlier sketches downgraded
1530
+ # # `match: present` to a terminal-key substring
1531
+ # # search of the raw payload string; that
1532
+ # # heuristic creates false positives (a payload
1533
+ # # mentioning the key name in any context — URL
1534
+ # # fragment, comment, unrelated metadata field —
1535
+ # # would pass), which is exactly the anti-façade
1536
+ # # contract this surface exists to protect.
1537
+ # # Storyboards that need a "the upstream call
1538
+ # # carried this value" signal against non-JSON
1539
+ # # payloads use `identifier_paths` instead — that
1540
+ # # surface substring-searches storyboard-supplied
1541
+ # # VALUES (not path-derived strings), which is
1542
+ # # encoding-agnostic and doesn't suffer the
1543
+ # # false-positive surface.
1544
+ # # Shape:
1545
+ # # - path: <dotted-with-[*] path>
1546
+ # # match: present | equals | contains_any
1547
+ # # value: <value> # for equals
1548
+ # # allowed_values: [...] # for contains_any
1549
+ # # Example:
1550
+ # # - path: "users[*].hashed_email"
1551
+ # # match: present
1552
+ # identifier_paths: array of string (optional)
1553
+ # # Paths into the storyboard's sample_request that
1554
+ # # name the load-bearing identifiers the adapter
1555
+ # # MUST forward upstream. The runner extracts the
1556
+ # # values at these paths from the storyboard's
1557
+ # # request and asserts each value appears in at
1558
+ # # least one matching recorded_call's payload at
1559
+ # # any depth. This is the strongest single
1560
+ # # anti-façade signal — adapters cannot satisfy
1561
+ # # it by fabricating a constant placeholder hash,
1562
+ # # because the placeholder will not match the
1563
+ # # storyboard's supplied value.
1564
+ # # Portable path grammar (normative):
1565
+ # # request-payload-relative dotted paths where
1566
+ # # each segment is an identifier key optionally
1567
+ # # followed by one `[*]` array wildcard. Example:
1568
+ # # "audiences[*].add[*].hashed_email"
1569
+ # # Supported segment form:
1570
+ # # [A-Za-z_][A-Za-z0-9_-]* or
1571
+ # # [A-Za-z_][A-Za-z0-9_-]*[*]
1572
+ # # Unsupported forms MUST be rejected at
1573
+ # # storyboard load/lint time rather than treated
1574
+ # # as paths that resolve to zero values:
1575
+ # # bracket-quoted keys (`foo["bar"]`), numeric
1576
+ # # indexes (`items[0]`), recursive descent
1577
+ # # (`$..foo`), absolute roots (`$.foo`), empty
1578
+ # # segments, and reserved roots (`request.*`,
1579
+ # # `response.*`, `context.*`). The path is
1580
+ # # already relative to the storyboard step's
1581
+ # # sample_request; adding an explicit root makes
1582
+ # # the same storyboard ambiguous across runners.
1583
+ # # Each path MAY resolve to a single value or an
1584
+ # # array; all resolved values MUST be present in
1585
+ # # the recorded payload. Replaces the earlier
1586
+ # # `buyer_identifier_echo: boolean` shorthand
1587
+ # # (which left "what counts as an identifier" to
1588
+ # # convention) with an explicit enumeration.
1589
+ # # Example:
1590
+ # # identifier_paths:
1591
+ # # - "audiences[*].add[*].hashed_email"
1592
+ # # - "audiences[*].add[*].external_id"
1593
+ # purpose_filter: array of string (optional)
1594
+ # # Restrict the assertion to recorded_calls whose
1595
+ # # `purpose` field matches one of the listed
1596
+ # # values. Allowed values:
1597
+ # # "platform_primary" | "measurement" |
1598
+ # # "attribution" | "creative_serving" |
1599
+ # # "identity" | "other"
1600
+ # # Use this when an adapter legitimately makes
1601
+ # # multiple kinds of upstream calls during a step
1602
+ # # — e.g., a sales-non-guaranteed buyer agent
1603
+ # # creating a campaign AND calling a measurement
1604
+ # # vendor AND posting an attribution event during
1605
+ # # the same create_media_buy. The storyboard
1606
+ # # scopes to `purpose_filter: [platform_primary]`
1607
+ # # so the other calls don't muddy the
1608
+ # # campaign-creation assertion.
1609
+ # # When omitted, the assertion considers all
1610
+ # # recorded_calls regardless of purpose. Calls
1611
+ # # without a `purpose` field (adopter didn't
1612
+ # # classify) are treated as `purpose: other` for
1613
+ # # filter matching — they're included by filters
1614
+ # # listing `other` and excluded by filters that
1615
+ # # don't. Runners MUST include a count of
1616
+ # # unclassified calls in the validation_result's
1617
+ # # `actual` when `purpose_filter` is set and
1618
+ # # zero recorded_calls match the filter, so a
1619
+ # # façade that misclassifies-into-`other` to
1620
+ # # dodge filtering produces a noisy zero rather
1621
+ # # than a silent one.
1622
+ # since: prior_step_id (optional, default: this step's request timestamp)
1623
+ # # Bound the lookup window. By default, the
1624
+ # # runner queries traffic recorded since the
1625
+ # # storyboard step issued its AdCP request, so
1626
+ # # only side-effects of THIS step are counted.
1627
+ # # Set to a prior step's id to include traffic
1628
+ # # caused by an earlier step (e.g., for a
1629
+ # # cumulative-effect assertion). The runner uses
1630
+ # # its own wall-clock timestamp from when it
1631
+ # # issued the AdCP request as the lower bound —
1632
+ # # the controller's `since_timestamp` echo is
1633
+ # # informational, not authoritative. See
1634
+ # # runner-output-contract.yaml > validation_result
1635
+ # # for the timestamp boundary semantics
1636
+ # # (inclusive lower bound, clock-skew tolerance).
1637
+ # attestation_mode_required: string (optional)
1638
+ # # When set to "raw", the storyboard requires
1639
+ # # raw payload introspection. Recorded calls
1640
+ # # returned in `attestation_mode: digest` grade
1641
+ # # the upstream_traffic check `not_applicable`
1642
+ # # rather than failing — the storyboard is
1643
+ # # asserting against payload contents the
1644
+ # # adopter's privacy policy doesn't permit
1645
+ # # disclosing.
1646
+ # # When omitted, the storyboard accepts either
1647
+ # # mode: assertions degrade gracefully in digest
1648
+ # # mode (`payload_must_contain` grades
1649
+ # # not_applicable per call; `identifier_paths`
1650
+ # # works via the digest-of-identifier echo
1651
+ # # mechanism, see "Digest-mode behavior" below).
1652
+ # # Use "raw" sparingly — the digest mode exists
1653
+ # # so EU/privacy-conscious adopters can support
1654
+ # # upstream_traffic conformance without returning
1655
+ # # hashed-PII payloads. Forcing raw excludes
1656
+ # # those adopters; only use it when the
1657
+ # # storyboard's assertion genuinely cannot be
1658
+ # # expressed in identifier-digest form.
1659
+ #
1660
+ # Digest-mode behavior:
1661
+ # `attestation_mode: digest` per recorded_call swaps payload introspection
1662
+ # for digest-based echo verification (see comply-test-controller-response.json
1663
+ # > UpstreamTrafficSuccess > recorded_calls > DigestAttestation):
1664
+ # - `payload_must_contain` arbitrary path assertions: NOT supported in
1665
+ # digest mode. Each entry grades `not_applicable` per affected call,
1666
+ # with a note explaining the mode constraint.
1667
+ # - JCS non-finite numbers: when a digest-mode upstream_traffic response
1668
+ # cannot be produced because the parsed JSON-like value tree contains
1669
+ # a non-finite numeric value (`NaN`, `+Infinity`, or `-Infinity`), the
1670
+ # controller MUST NOT coerce that value to `null`, a string, or any
1671
+ # other placeholder for digest computation; it returns ControllerError
1672
+ # with error code `JCS_NON_FINITE_NUMBER`; the runner grades the
1673
+ # affected validation `not_applicable`, not failed, with a note citing
1674
+ # the RFC 8785/JCS constraint.
1675
+ # - `payload_length`: MUST equal the exact byte length covered by
1676
+ # `payload_digest_sha256` — RFC 8785 (JCS) canonical bytes for
1677
+ # JSON-shaped content after redaction, or post-redaction raw body bytes
1678
+ # for non-JSON content. It is not the original outbound body length
1679
+ # before JSON parsing, redaction, or canonicalization.
1680
+ # - `identifier_paths` echo verification: supported via the controller's
1681
+ # `identifier_match_proofs[]`. The runner computes SHA-256 of each
1682
+ # resolved identifier value, sends the digests in
1683
+ # `params.identifier_value_digests`, and reads the per-digest
1684
+ # `found: true | false` proof from the response. Plaintext identifiers
1685
+ # never reach the controller; the controller never returns plaintext
1686
+ # payloads. The privacy boundary holds in both directions.
1687
+ # - `min_count` and `endpoint_pattern`: unchanged — count and URL
1688
+ # matching work identically in both modes.
1689
+ #
1690
+ # Grading output (see runner-output-contract.yaml > validation_result):
1691
+ # `expected` echoes the declared assertion fields. `actual` is a
1692
+ # diagnostic summary: { matched_count, total_calls, missing_payload_paths,
1693
+ # missing_identifier_values }. The full recorded_calls array lives in
1694
+ # `response.payload` so an implementor can see the exact upstream
1695
+ # payloads the runner observed.
1696
+ #
1697
+ # Adopter integration:
1698
+ # The adopter implements `query_upstream_traffic` in its
1699
+ # comply_test_controller. The controller records outbound HTTP calls in a
1700
+ # caller-scoped buffer (sandbox-only — production builds MUST 404 the
1701
+ # comply_test_controller tool entirely; multi-tenant sandboxes MUST key
1702
+ # the buffer on the calling principal so cross-caller traffic does not
1703
+ # leak). See comply-test-controller-request.json > query_upstream_traffic
1704
+ # for the controller-side query contract.
1705
+ #
1706
+ # Example — sync_audiences must POST hashed members upstream:
1707
+ # - check: upstream_traffic
1708
+ # description: "sync_audiences caused upstream traffic with hashed members"
1709
+ # endpoint_pattern: "POST *"
1710
+ # min_count: 1
1711
+ # payload_must_contain:
1712
+ # - path: "users[*].hashed_email"
1713
+ # match: present
1714
+ # identifier_paths:
1715
+ # - "audiences[*].add[*].hashed_email"
1716
+ # - "audiences[*].add[*].external_id"
1717
+ #
1718
+ # Cross-response assertions — `check: cross_response_field_equal` / `cross_response_count_distinct`:
1719
+ # These check kinds operate on the RESPONSE SET of a step that dispatches
1720
+ # multiple parallel requests under the `parallel_dispatch_runner` contract
1721
+ # (see test-kits/parallel-dispatch-runner.yaml). They are NOT applicable to
1722
+ # single-dispatch steps: a single response trivially has cardinality 1 on any
1723
+ # field. Runners MUST gate these checks on the step declaring
1724
+ # `requires_contract: parallel_dispatch_runner` and a `parallel_dispatch`
1725
+ # block; outside that gate the check MUST grade `not_applicable`. The
1726
+ # canonical use case is rule-9 testing (concurrent retries / first-insert-wins
1727
+ # from L1/security.mdx#idempotency) — fire N parallel calls with the same
1728
+ # `idempotency_key`, assert exactly one resource was created.
1729
+ #
1730
+ # `parallel_dispatch` is a step-level block that travels alongside `requires_contract`:
1731
+ #
1732
+ # - id: concurrent_create
1733
+ # task: create_media_buy
1734
+ # requires_contract: parallel_dispatch_runner
1735
+ # parallel_dispatch:
1736
+ # count: 2 # required, integer 2..10. The runner fires
1737
+ # # `count` logically-simultaneous requests
1738
+ # # using the SDK's batch primitive.
1739
+ # same_idempotency_key: true # required for rule-9 tests; future
1740
+ # # non-idempotency parallel tests MAY set
1741
+ # # this false.
1742
+ # barrier_timeout_ms: 5000 # optional; default 5000 — how long the
1743
+ # # runner waits at the dispatch barrier
1744
+ # # before firing requests anyway.
1745
+ # validations:
1746
+ # - check: cross_response_count_distinct
1747
+ # path: "media_buy_id"
1748
+ # allowed_values: [1]
1749
+ # description: "Exactly one media_buy_id across all dispatches"
1750
+ # - check: cross_response_field_equal
1751
+ # path: "media_buy_id"
1752
+ # description: "All dispatches converge on the same media_buy_id"
1753
+ #
1754
+ # `cross_response_field_equal`:
1755
+ # - path (string, required): JSON path checked on every resolved response.
1756
+ # All non-null observed values at `path` MUST be deeply equal. A missing
1757
+ # value on a response is treated as a non-equal observation and fails the
1758
+ # check.
1759
+ #
1760
+ # `cross_response_count_distinct`:
1761
+ # - path (string, required): JSON path scanned on every resolved response.
1762
+ # - allowed_values (array of integers, required): permitted cardinalities of
1763
+ # distinct values observed at `path` across the resolved response set.
1764
+ # For rule-9 tests, `allowed_values: [1]` — exactly one resource was
1765
+ # created across all dispatches.
1766
+ #
1767
+ # Resolution semantics. A seller adopting the reject-and-redirect policy under
1768
+ # rule 9 may return `IDEMPOTENCY_IN_FLIGHT` on the second (or later) dispatch
1769
+ # rather than block. The runner MUST resolve `IDEMPOTENCY_IN_FLIGHT` responses
1770
+ # to their eventual cached response before applying these checks: wait
1771
+ # `error.details.retry_after` seconds and retry with the SAME idempotency_key,
1772
+ # exactly as the buyer-side normative behavior specifies. The cross-response
1773
+ # check operates on the resolved response set, not the raw initial set. If
1774
+ # resolution exceeds the runner's step-timeout budget, the step grades
1775
+ # `step_timeout` (a runner-synthesized code), not a rule-9 violation.
1776
+ #
1777
+ # Grading output. `expected` carries the declared assertion fields
1778
+ # (`path`, plus `allowed_values` for `count_distinct`). `actual` summarizes
1779
+ # what was observed across the resolved response set:
1780
+ # cross_response_field_equal → { observed_values: [...] }
1781
+ # cross_response_count_distinct → { observed_cardinality, observed_values: [...] }
1782
+ # See runner-output-contract.yaml > validation_result > expected/actual for
1783
+ # the canonical shapes.
1784
+ #
1785
+ # --- Runner output ---
1786
+ #
1787
+ # How a runner MUST report validation results — including which fields a
1788
+ # failing validation carries (exact request, response, JSON Pointer, expected
1789
+ # vs. actual, schema $id, schema URL) and how skipped storyboards MUST
1790
+ # distinguish not_applicable vs. no_phases vs. missing_tool — is defined in
1791
+ # runner-output-contract.yaml. Storyboard authors SHOULD assume failures will
1792
+ # be rendered with that detail and write descriptions accordingly.
1793
+ #
1794
+ # Runner grading codes introduced by the context accumulator and fixture
1795
+ # seeding:
1796
+ #
1797
+ # capture_path_not_resolvable
1798
+ # Emitted on the capturing step when a `context_outputs:` entry's
1799
+ # `path` does not resolve to a usable value in the step's response body.
1800
+ # The step grades as failed — the capture declared a contract that the
1801
+ # response did not meet. Distinct from unresolved_substitution so tooling
1802
+ # can discriminate "producer contract breach" from "consumer missing
1803
+ # state."
1804
+ #
1805
+ # "Does not resolve to a usable value" covers all three cases:
1806
+ # 1. Path structurally absent (e.g. `signals[0]` when array is empty)
1807
+ # 2. Path present but value is null
1808
+ # 3. Path present but value is "" (empty string)
1809
+ # Capturing null or "" produces fabricated downstream state and is as
1810
+ # incorrect as a missing path; runners MUST treat all three as failures.
1811
+ #
1812
+ # Output shape: runner-output-contract.yaml > validation_result, with
1813
+ # check: capture_path_not_resolvable, expected: the declared path string,
1814
+ # actual: the resolved value (or null if absent), json_pointer: RFC 6901
1815
+ # form of the path.
1816
+ #
1817
+ # unresolved_substitution
1818
+ # Emitted on a consumer step when a referenced $context.<name> or
1819
+ # {{prior_step.<id>.<field>}} value is not populated at step-execution
1820
+ # time (either because the producer step failed, was skipped, or never
1821
+ # ran). The step grades as failed. Also used at preflight when a
1822
+ # substitution is statically unresolvable (e.g., {{runner.webhook_url:*}}
1823
+ # on a run with no webhook receiver) to grade the entire storyboard
1824
+ # not_applicable before execution.
1825
+ #
1826
+ # Output shape: runner-output-contract.yaml > validation_result, with
1827
+ # check: unresolved_substitution, expected: the unresolved token string,
1828
+ # actual: null, json_pointer: null (pre-wire failure — no response
1829
+ # payload available).
1830
+ #
1831
+ # fixture_seed_unsupported
1832
+ # Emitted when a storyboard's `prerequisites.controller_seeding` requires
1833
+ # a seed_* scenario the seller does not implement (returned as
1834
+ # UNKNOWN_SCENARIO). The storyboard grades not_applicable (coverage gap),
1835
+ # not failed.
1836
+ #
1837
+ # unresolved_scenario_reference
1838
+ # A detailed sub-reason under the canonical skip `reason:
1839
+ # not_applicable`. Emitted when a parent storyboard's
1840
+ # `requires_scenarios:` entry cannot be resolved against the source
1841
+ # tree (no file declares that `id:`). Per the
1842
+ # `detailed_reason_mapping` convention in runner-output-contract.yaml,
1843
+ # runners MUST populate the canonical `reason: not_applicable` and
1844
+ # encode `unresolved_scenario_reference` in `detail`. detail MUST
1845
+ # follow the shape
1846
+ # 'requires_scenarios reference "<scenario_id>" did not resolve
1847
+ # against the source tree' — and when multiple references are
1848
+ # unresolved, detail MUST enumerate every unresolved id with the
1849
+ # same shape (comma-separated or newline-delimited) so downstream
1850
+ # tooling can parse each. Runners MUST NOT silently pass the parent
1851
+ # as if the missing scenario had contributed.
1852
+ #
1853
+ # Distinct from `fixture_seed_unsupported`: that reason indicates an
1854
+ # agent-coverage gap (seller doesn't implement a seed scenario);
1855
+ # `unresolved_scenario_reference` indicates a source-tree authoring
1856
+ # bug that the build-time lint SHOULD have caught. Dashboards and
1857
+ # summary reports SHOULD NOT conflate the two when aggregating
1858
+ # `not_applicable` counts — they are categorically different
1859
+ # signals.
1860
+ #
1861
+ # The `lint:storyboard-branch-sets` script surfaces this as a
1862
+ # build-time error via the `unresolved_scenario_reference` rule so
1863
+ # unresolved references in a well-linted corpus never reach a
1864
+ # runner. The runner-side grading is defensive for corpora that
1865
+ # bypass the lint or come from pre-lint CI states.
1866
+ #
1867
+ # --- Webhook receiver (optional, for outbound-webhook conformance) ---
1868
+ #
1869
+ # Storyboards that verify outbound webhook conformance (signing, idempotency_key
1870
+ # presence, idempotency_key stability across retries) require the runner to host
1871
+ # a webhook receiver during test execution. The receiver URL is injected into
1872
+ # push_notification_config on steps that trigger webhooks, and a subsequent
1873
+ # expect_webhook* step asserts on what arrived.
1874
+ #
1875
+ # Receiver behavior is specified by a test-kit contract referenced from the
1876
+ # storyboard's prerequisites.test_kit field — see test-kits/webhook-receiver-runner.yaml
1877
+ # for the reference contract (endpoint modes, retry-replay shape, client-primitive
1878
+ # hooks). Storyboard authors MUST declare the contract in prerequisites; the
1879
+ # webhook-emission universal (universal/webhook-emission.yaml) is the reference
1880
+ # consumer, and universal/idempotency.yaml uses it for the "no duplicate webhooks
1881
+ # on replay" assertion.
1882
+ #
1883
+ # --- Substitution variables (webhook-receiver enabled) ---
1884
+ #
1885
+ # When a storyboard's test-kit contract declares a webhook_receiver, the runner
1886
+ # exposes these substitutions for use in sample_request payloads and expectations:
1887
+ #
1888
+ # {{runner.webhook_base}} → HTTPS base of the runner's receiver for this run
1889
+ # {{runner.webhook_url:<step_id>}} → per-step URL; unique per step
1890
+ #
1891
+ # Per-step receivers are the only supported shape in v1 so each step's
1892
+ # expect_webhook matches only its own deliveries. Fan-in scenarios are deferred
1893
+ # (see "shared_receiver" note below).
1894
+ #
1895
+ # --- Webhook-assertion steps ---
1896
+ #
1897
+ # The following step `task` values are only valid when the storyboard's test-kit
1898
+ # declares a webhook_receiver. They are scheduled alongside regular task steps
1899
+ # and reference earlier steps by id.
1900
+ #
1901
+ # task: expect_webhook
1902
+ # Wait up to `timeout_seconds` (default 30) for a webhook matching the filter,
1903
+ # then validate. Runner delegates to the `@adcp/client` AsyncHandler; the webhook
1904
+ # is observed via onActivity with type webhook_received, and WebhookMetadata
1905
+ # (including idempotency_key) is provided by the client.
1906
+ #
1907
+ # Fields:
1908
+ # triggered_by: <step_id> # earlier step whose task triggered this webhook
1909
+ # filter: # match by payload fields
1910
+ # operation_id: "{{prior_step.<id>.operation_id}}"
1911
+ # status: "completed" # optional; match any status if omitted
1912
+ # timeout_seconds: 30 # optional; fail if no match within window
1913
+ # expect_idempotency_key: true # default true; assert the key is present and pattern-valid
1914
+ # webhook_payload_schema_ref: <path>
1915
+ # # optional — validate payload against the webhook schema
1916
+ # # (distinct from step-level `schema_ref` and
1917
+ # # `response_schema_ref`, which apply to the caller→agent
1918
+ # # request/response. Webhook steps have no caller request,
1919
+ # # so the payload schema is named explicitly to avoid
1920
+ # # overload of schema_ref.)
1921
+ # expect_max_deliveries_per_logical_event: 1
1922
+ # # optional — if set, runner asserts at most N distinct
1923
+ # # logical webhook events (grouped by idempotency_key)
1924
+ # # arrive within the window. Used by idempotency storyboards
1925
+ # # to catch duplicate-side-effect bugs where a seller
1926
+ # # re-executes on replay with a fresh idempotency_key.
1927
+ # requires_contract: <contract_id>
1928
+ # # optional — step grades as not_applicable (not fail)
1929
+ # # when the named test-kit contract is not in scope.
1930
+ # # Used on cross-specialism assertions that depend on
1931
+ # # webhook_receiver_runner being active.
1932
+ #
1933
+ # Error modes: no_webhook_received, schema_violation, missing_idempotency_key,
1934
+ # invalid_idempotency_key_format, signature_invalid (when signature assertion on),
1935
+ # duplicate_webhook_on_replay (when expect_max_deliveries_per_logical_event exceeded).
1936
+ #
1937
+ # task: expect_webhook_retry_keys_stable
1938
+ # The runner's webhook receiver deterministically returns 5xx for the first
1939
+ # `retry_trigger.count` deliveries of the matching webhook, then 2xx. The
1940
+ # runner asserts that every delivery within the retry window carries the
1941
+ # byte-identical idempotency_key (the sender regenerating a key on retry is a
1942
+ # conformance failure).
1943
+ #
1944
+ # Signature verification on every delivery in the retry loop: when 9421 is in
1945
+ # effect (i.e., push_notification_config was registered without `authentication`),
1946
+ # the runner MUST verify the 9421 signature on EVERY delivery including retries
1947
+ # and MUST reuse the run-scoped (keyid, nonce) replay store (see test-kit
1948
+ # client_primitives.signature_replay_store). A publisher that stably reuses
1949
+ # idempotency_key (correct) but also reuses the 9421 `nonce` sig-param
1950
+ # (incorrect — nonce MUST be fresh per delivery) would pass retry-stability
1951
+ # but MUST fail signature-replay dedup on the second delivery. Running the two
1952
+ # checks together catches this class of bug; a retry-stability step that skips
1953
+ # signature verification hides it.
1954
+ #
1955
+ # Fields:
1956
+ # triggered_by: <step_id>
1957
+ # filter: { ... } # same shape as expect_webhook
1958
+ # retry_trigger:
1959
+ # count: 3 # runner returns 5xx for the first N deliveries.
1960
+ # # MUST be 1 ≤ count ≤ 10 (test-kit
1961
+ # # retry_replay_contract.max_count). Counts outside
1962
+ # # the range fail storyboard validation; sellers
1963
+ # # legitimately exercise N=3 for back-off sanity,
1964
+ # # higher counts risk becoming DoS amplifiers in
1965
+ # # proxy_url mode.
1966
+ # http_status: 503 # optional; default 503. MUST be in
1967
+ # # {429, 500, 502, 503, 504} (test-kit
1968
+ # # retry_replay_contract.allowed_statuses). Other
1969
+ # # statuses are not retryable signals for
1970
+ # # at-least-once senders and fail validation.
1971
+ # timeout_seconds: 90 # longer default — must cover sender's retry back-off
1972
+ # expect_min_deliveries: 2 # fail if fewer deliveries observed within window
1973
+ # verify_signature_on_every_delivery: true
1974
+ # # default true when 9421 is in effect. Set false
1975
+ # # ONLY when the storyboard is explicitly registered
1976
+ # # for legacy HMAC mode; in 9421 mode, silently
1977
+ # # disabling retry-delivery signature verification
1978
+ # # hides nonce-replay bugs.
1979
+ #
1980
+ # Error modes: insufficient_retries, idempotency_key_rotated,
1981
+ # idempotency_key_format_changed, signature_replayed (nonce reused across
1982
+ # retries), signature_invalid (any retry delivery fails 9421).
1983
+ #
1984
+ # task: expect_webhook_signature_valid
1985
+ # Assert that the inbound webhook passed the 9421 webhook-signing verifier
1986
+ # checklist (see docs/building/implementation/security.mdx#verifier-checklist-for-webhooks).
1987
+ # Runner delegates verification to the `@adcp/client` 9421 webhook verifier;
1988
+ # this step is gated on the client library having that verifier available
1989
+ # (see test-kit contract). Negative conformance — signatures that MUST be
1990
+ # rejected with a specific webhook_signature_* code — is tested via static
1991
+ # conformance vectors under /compliance/{version}/test-vectors/webhook-signing/
1992
+ # when those land, not by this step type.
1993
+ #
1994
+ # Cross-step replay dedup: the runner MUST share a single (keyid, nonce) replay
1995
+ # store across ALL expect_webhook_signature_valid invocations (and all retry
1996
+ # deliveries under expect_webhook_retry_keys_stable) in a storyboard run — see
1997
+ # test-kit client_primitives.signature_replay_store. A per-step store would let
1998
+ # a publisher reuse the same (keyid, nonce) bytes at two expect_webhook_signature_valid
1999
+ # steps and pass both in isolation, silently missing cross-step replay.
2000
+ #
2001
+ # Fields:
2002
+ # triggered_by: <step_id>
2003
+ # filter: { ... } # same shape as expect_webhook
2004
+ # timeout_seconds: 30
2005
+ # require_tag: "adcp/webhook-signing/v1" # default; sanity check
2006
+ #
2007
+ # Error modes: signature_invalid, signature_expired, signature_key_unknown,
2008
+ # signature_key_purpose_invalid, signature_digest_mismatch, signature_tag_invalid,
2009
+ # signature_replayed.
2010
+ #
2011
+ # task: expect_substitution_safe
2012
+ # Assert that a creative preview (preview_html or preview_url) contains
2013
+ # substituted catalog-item macro values that are percent-encoded per RFC 3986
2014
+ # (unreserved-whitelist), per docs/creative/universal-macros#substitution-safety-catalog-item-macros.
2015
+ # Runner delegates HTML parsing, URL extraction, and the encoding check to
2016
+ # the `@adcp/client` SubstitutionObserver primitive (see test-kit
2017
+ # substitution-observer-runner.yaml). Catches implementations that pass raw
2018
+ # attacker bytes through substitution, including CRLF injection, bidi-override
2019
+ # spoofing, reserved-character URL breakout, and javascript:-scheme injection.
2020
+ #
2021
+ # Fields:
2022
+ # source: html_inline | url_fetch # default html_inline
2023
+ # source_path: "<JSON pointer into previous step's response>"
2024
+ # macro_template: "<URL template with {MACRO} placeholders>"
2025
+ # catalog_bindings:
2026
+ # - macro: "{SKU}"
2027
+ # catalog_item_id: "<item_id in the synced catalog>"
2028
+ # vector_name: "<fixture entry name, e.g. reserved-character-breakout>"
2029
+ # # Optional overrides for custom (non-canonical-fixture) vectors:
2030
+ # # raw_value: "<attacker-shaped input>"
2031
+ # # expected_encoded: "<RFC 3986 unreserved-encoded form>"
2032
+ # require_every_binding_observed: true # default true. Opt to false only
2033
+ # # when preview shows a partial
2034
+ # # catalog and partial observation
2035
+ # # is legitimate (document why in
2036
+ # # the narrative). The old field
2037
+ # # name require_all_bindings_observed
2038
+ # # is deprecated.
2039
+ # requires_contract: substitution_observer_runner
2040
+ #
2041
+ # Error modes: substitution_encoding_violation, nested_macro_re_expansion,
2042
+ # substitution_scheme_injection, substitution_binding_missing,
2043
+ # preview_source_unavailable, preview_url_unusable (with sub-reason:
2044
+ # http_status | content_type | size_exceeded | redirect_returned |
2045
+ # ssrf_blocked | fetch_timeout).
2046
+ #
2047
+ # task: expect_rate_limit_not_replayed
2048
+ # Assert that a `RATE_LIMITED` response on idempotency-cache insert MUST NOT
2049
+ # be cached as the canonical replay for that idempotency_key, per
2050
+ # L1/security.mdx#idempotency rule 3 ("Only successful responses are cached")
2051
+ # and bullet 8 (insert-rate ceiling). The runner drives a sequential fresh-
2052
+ # key burst against the named mutating-op target until a RATE_LIMITED
2053
+ # response appears, then re-submits the same idempotency_key after waiting
2054
+ # `error.details.retry_after` seconds. The cross-step assertion fails if the
2055
+ # replay returned the cached RATE_LIMITED response shape.
2056
+ #
2057
+ # When `max_attempts` requests complete without producing a RATE_LIMITED
2058
+ # response, the step grades as `not_applicable` with reason
2059
+ # `rate_limit_not_triggered` — a seller's sandbox limiter may legitimately
2060
+ # sit above the contract's burst ceiling. Runner delegates the burst,
2061
+ # wait, and replay mechanics to the `@adcp/client` RateLimitTripObserver
2062
+ # primitive (see test-kit rate-limit-trip-runner.yaml).
2063
+ #
2064
+ # Fields:
2065
+ # rate_limit_trip:
2066
+ # trip_target_task: "<task name, e.g. create_media_buy>"
2067
+ # # the mutating-op the burst targets
2068
+ # trip_target_sample_request: { ... }
2069
+ # # full canonical request body; the runner
2070
+ # # rewrites `idempotency_key` per attempt
2071
+ # # and on the replay reuses the captured key
2072
+ # max_attempts: 200 # 50–500; see test-kit max_attempts bounds
2073
+ # replay_max_wait_seconds: 30 # optional; caps the post-trip wait. Sellers
2074
+ # # returning retry_after > this fail with
2075
+ # # `replay_wait_exceeded` rather than the
2076
+ # # invariant itself.
2077
+ # requires_contract: rate_limit_trip_runner
2078
+ #
2079
+ # Error modes: rate_limit_response_cached_as_replay,
2080
+ # missing_retry_after, replay_wait_exceeded, rate_limit_not_triggered
2081
+ # (the not_applicable case, surfaced for runner output transparency).
2082
+ #
2083
+ # --- shared_receiver (deferred / out of scope v1) ---
2084
+ #
2085
+ # Storyboards with multiple webhook-triggering steps MUST use per-step receiver
2086
+ # URLs; the default substitution `{{runner.webhook_url:<step_id>}}` gives each
2087
+ # step its own receiver so expect_webhook scopes cleanly. A `shared_receiver: true`
2088
+ # flag on an emitting step would route all emissions from that step to a shared
2089
+ # bucket (for fan-in dedup tests), but the mechanics — filter matching across
2090
+ # multiple emitters, retry-replay policy precedence, shared replay store scoping —
2091
+ # are underspecified. shared_receiver is OUT OF SCOPE for the first
2092
+ # webhook-emission universal release; storyboard authors MUST NOT set it. Future
2093
+ # fan-in storyboards will either flesh out the semantics or adopt a different
2094
+ # idiom (e.g., per-step receivers with cross-step assertions on aggregate
2095
+ # observations).
2096
+ #
2097
+ # --- Context accumulator and substitution ---
2098
+ #
2099
+ # The runner maintains a storyboard-run-scoped context accumulator populated from
2100
+ # three sources (evaluated in this precedence, high to low):
2101
+ #
2102
+ # 1. Step `context_outputs:` captures (populated after a step's validations pass)
2103
+ # 2. Storyboard-root `context:` block (literal values fixed at run start)
2104
+ # 3. Test-kit substitutions where explicitly scoped into context
2105
+ #
2106
+ # Two substitution syntaxes are supported in `sample_request` payloads:
2107
+ #
2108
+ # $context.<name> — single-token substitution. The runner replaces
2109
+ # the literal string "$context.<name>" with the
2110
+ # captured value. Example:
2111
+ # account_id: '$context.account_id'
2112
+ # resolves to the account_id captured from an
2113
+ # earlier step. Works inside scalar values and
2114
+ # inside array elements.
2115
+ #
2116
+ # {{prior_step.<id>.<field>}} — Mustache-style interpolation for webhook
2117
+ # filters, operation_id matching, and any other
2118
+ # cross-step reference. Resolves against the
2119
+ # named prior step's captured response fields.
2120
+ # Used by expect_webhook and expect_*_webhook_*
2121
+ # assertion steps (see Webhook-assertion steps
2122
+ # above) and by storyboards correlating an
2123
+ # async operation_id across steps.
2124
+ #
2125
+ # Runner requirements:
2126
+ # - Substitution runs at step-execution time, after resolving the test-kit
2127
+ # overlay and before applying auth. The agent under test receives the
2128
+ # substituted value — it MUST NOT see the literal `$context.foo` or `{{...}}`
2129
+ # token on the wire (see Unresolved substitution behavior below for the one
2130
+ # legitimate exception: preflight not_applicable grading when a substitution
2131
+ # is statically unresolvable).
2132
+ # - $context.<name> referencing an unpopulated name (the capturing step failed
2133
+ # or did not run) grades the referencing step as failed with
2134
+ # `unresolved_substitution`. The storyboard MUST NOT proceed with a fabricated
2135
+ # default — test outcomes on synthesized state are not signal.
2136
+ # - Captured values are typed as the JSON path resolved against the response;
2137
+ # numeric and object captures are preserved (they do not round-trip through
2138
+ # string coercion).
2139
+ # - When the per-task request builder discards body fields and
2140
+ # `$context.<name>` inside `sample_request` never reaches the wire (the
2141
+ # observable symptom is the agent answering with an unfiltered result
2142
+ # and the round-trip assertion grading on fabricated state), use
2143
+ # `context_inputs:` as the post-builder injection path. See the
2144
+ # `context_inputs:` field definition above for the full semantics and
2145
+ # when it is acceptable to reach for.
2146
+ #
2147
+ # --- Context echo contract (agent-facing) ---
2148
+ #
2149
+ # Agents under test MUST obey the following when a storyboard sends a `context:`
2150
+ # block on its sample_request:
2151
+ #
2152
+ # 1. Successful responses MUST echo the `context` object verbatim. The agent
2153
+ # MUST NOT mutate, filter, or reorder fields. The echo applies whether the
2154
+ # response is synchronous (`completed`) or asynchronous (`submitted` /
2155
+ # `working`).
2156
+ # 2. Error responses MUST also echo the `context` object verbatim. Context is
2157
+ # used as a correlation surface; dropping it on failure prevents buyers from
2158
+ # correlating errors with the originating call.
2159
+ # 3. Agents MUST NOT synthesize a `context` object when the caller did not
2160
+ # send one. A storyboard whose `sample_request` lacks `context:` MUST see
2161
+ # the response lack `context:` — fabricating `{ correlation_id: "..." }` on
2162
+ # the agent side is a conformance failure because the storyboard validator
2163
+ # is asserting on what-the-caller-sent, not what-the-agent-invented.
2164
+ # 4. Agents MUST NOT parse or act on `context` contents — see
2165
+ # docs/building/integration/context-sessions.mdx for the normative
2166
+ # agent-facing rules. The storyboard runner's validators rely on verbatim
2167
+ # echo and will fail any mutation.
2168
+ #
2169
+ # Runners MUST NOT auto-inject an implicit `context:` block on sample_request
2170
+ # payloads the storyboard did not declare. Storyboards that want to assert on
2171
+ # `context.correlation_id` MUST declare the context block explicitly on each
2172
+ # sample_request. This keeps the sample_request an honest shape of what the
2173
+ # agent will receive, and prevents an agent from passing only because the
2174
+ # runner compensated for its missing contract.
2175
+ #
2176
+ # --- Unresolved substitution behavior ---
2177
+ #
2178
+ # When a storyboard references a substitution variable the runner cannot
2179
+ # resolve — most commonly {{runner.webhook_base}} or {{runner.webhook_url:<id>}}
2180
+ # on a run with no webhook receiver configured — the runner MUST NOT ship the
2181
+ # literal `{{...}}` token to the agent under test. Shipping unresolved
2182
+ # substitutions is a conformance bug on the runner side, not a grading signal
2183
+ # about the agent.
2184
+ #
2185
+ # Runners MUST resolve substitutions at one of two preflight points:
2186
+ # 1. Before the storyboard run starts — if any step references an unresolvable
2187
+ # substitution, grade the storyboard as not_applicable with
2188
+ # `unresolved_substitution` as the reason, and skip execution entirely.
2189
+ # This is the preferred behavior for absent webhook receivers.
2190
+ # 2. Before individual step execution — if a later-computed substitution
2191
+ # (e.g., {{prior_step.<id>.operation_id}}) fails to resolve, grade the
2192
+ # step as failed with `unresolved_substitution`.
2193
+ #
2194
+ # Neither path may ship a literal `{{...}}` token on the wire.