ストーリーボードの作成 — スコープルール
コンプライアンスストーリーボードは、コンプライアンスバンドルの正準な作成ソースであるstatic/compliance/source/ の下に存在します。domains/ や index.json のような生成キャッシュアーティファクトをそこに追加しないでください。scripts/build-compliance.cjs が開発中に dist/compliance/latest/ にそれらを作成し、リリース時に dist/compliance/{version}/ にスタンプします。
セッション状態をテナントでスコープするトレーニングエージェントタスクを呼び出す各ステップは、sample_request にブランドまたはアカウントアイデンティティを運ば なければなりません。さもなければ呼び出しは open:default に着地し、アイデンティティを 運ぶ 後続ステップが open:<brand> に書き込みます — あなた自身の今作成したメディアバイに対して MEDIA_BUY_NOT_FOUND を与えます。
このルールは scripts/lint-storyboard-scoping.cjs によってビルド時に強制され、npm run build:compliance の一部として実行されます。
正準アイデンティティ形状
account { brand, operator } を使います。AccountRef スキーマは、自然キー形式(brand)が使われるときは常に operator を要求します — 仕様レベルに「ブランドだけ」の形状はありません。
list_accounts 経由で account_id を発行したとき):
sync_plans では、アイデンティティは各プランエントリー内に存在します。sync-plans-request スキーマは各プランアイテムに brand を定義し、そこでの account を禁止します — plans[] 内でラッパー形式を使わないでください:
トップレベル brand はどうか?
一部の AdCP リクエスト(create_media_buy、get_products、build_creative)はトップレベル brand フィールドを持ちます。それは キャンペーンのブランド で、別のスキーマフィールドです — アイデンティティの略記ではありません。create_media_buy は account と brand の両方を要求します。一方が他方を代替しません。
lint は依然として、トレーニングエージェントの sessionKeyFromArgs がそれを読むため、むき出しのトップレベル brand.domain をフォールバックとして受け入れます — が、それはトレーニングエージェントのルーティング詳細で、仕様正準な形状ではありません。新しいストーリーボードは account { brand, operator } を使うべきです。
どのタスクがセッションスコープか?
権威的なリストはscripts/lint-storyboard-scoping.cjs に TENANT_SCOPED_TASKS として存在します。パリティテスト(tests/lint-storyboard-scoping.test.cjs)が、トレーニングエージェントの HANDLER_MAP に登録されたすべてのタスクが TENANT_SCOPED_TASKS または EXEMPT_FROM_LINT のいずれかに現れることをアサートします。ディスパッチテーブルに新しいツールを追加して分類を忘れると、パリティテストが失敗します — 静かなドリフトは起こりません。
経験則: タスクの リクエストスキーマがグローバルに一意なスコープ ID を要求 するなら(plan_id、rights_id、standards_id、list_id、event_source_id)、セラーはその ID だけからテナントを解決できます — エンベロープアイデンティティは冗長で、lint はそれを要求しません(EXEMPT_FROM_LINT バケット (c) を参照)。
それ以外すべては TENANT_SCOPED_TASKS に該当します: スコープ ID のない create/update ミューテーション、単一リソース ID を運ばない list/get 操作、スキーマに standards_id のないリソース標準呼び出し、など。これらは エンベロープ account { brand, operator } を運ばなければなりません。
$context を通じて流れるアイデンティティフィールド
ステップが context_outputs 経由で値を $context にキャプチャし、後のステップがそれを $context.<name> として消費するとき、両端の エンティティタイプ は一致しなければなりません。advertiser_brand とアノテーションされたフィールドからキャプチャされた値が rights_holder_brand とアノテーションされたフィールドとして消費されると、lint がそれをフラグします(それが #2627 バグ: 同じフィールド名、異なるエンティティ)。エンティティタイプのリストとスキーマ作成者がフィールドをどうアノテーションするかについては docs/contributing/x-entity-annotation.md を参照。
その他の免除カテゴリー: ペイロード配列キー付き sync タスク(sync_accounts、sync_governance、sync_catalogs、sync_event_sources)、グローバルディスカバリー(list_creative_formats、get_adcp_capabilities)、グローバルカタログ読み取り(get_brand_identity、get_rights、update_rights)、および comply_test_controller サンドボックスプリミティブ。
なぜ ID スコープタスクは免除だがストーリーボードは依然としてアイデンティティを運ぶか
check_governance、report_plan_outcome、acquire_rights、log_event、calibrate_content、validate_content_delivery、validate_property_delivery はすべて、以前にブランドコンテキストでプロビジョニングされたグローバルに一意な ID(plan_id、rights_id、standards_id など)を要求します。仕様レベルでは、実際のセラーは ID → テナントを自身のルックアップ経由で解決します。エンベロープはアイデンティティを繰り返す必要がありません。
トレーニングエージェントの sessionKeyFromArgs はエンベロープアイデンティティでルーティングします。ID スコープタスクでアイデンティティを 落とす ストーリーボードは open:default に着地し、plan/rights/standards を見つけられません — だからストーリーボードはとにかくエンベロープアイデンティティを運び、lint はそれを強制しないだけです。
これはサンドボックスルーティング規約で、仕様の主張ではありません。本番セラーは、エンベロープペイロードからではなく認証済みプリンシパル(bearer/OAuth/HMAC)からテナントを解決します — テナント解決 を参照。彼らは ID スコープタスクでエンベロープアイデンティティを必要とせず、存在しても依存しません。アイデンティティをワイヤーから外すためだけにトレーニングエージェントにクロスセッション逆インデックスを構築することは、仕様意味のないサンドボックス配管でしょう。
意図的なクロステナントプローブ
ステップがテナントアイデンティティなしでセッションスコープタスクをプローブすることが 意図されている 場合 — 例: セラーがむき出しのリクエストを拒否することを検証するネガティブテスト、またはケイパビリティディスカバリープローブ — ステップをアノテーションします:フィクスチャとクロスステップキャプチャ
前提条件状態(特定のproduct_id を持つ製品、既に approved ステータスのクリエイティブ、ガバナンスフローが参照できるプラン)を必要とするストーリーボードは、それを設定する 2 つの方法があります: テストが実行される 前 に存在する状態のための ストーリーボードルートの宣言的 fixtures: と、実行 中に生成される ID のための ステップ context_outputs: キャプチャ。
どちらをいつ使うか
避けられるなら
sample_request にリテラル ID をハードコードしないでください。 media_buy_id: "mb_acme_q2_2026_auction" のようなリテラルは、エージェントがたまたまその正確な ID を生成(または受け入れ)する場合のみ機能します。仕様準拠のエージェントは ID を自動生成します — リテラルは一致せず、何も間違っていない実装者に対してストーリーボードが失敗します。
パターン A — fixtures: + comply_test_controller 経由の前提条件フィクスチャ
ストーリーボードルートでフィクスチャを宣言します。prerequisites.controller_seeding: true を設定して、ランナーにメインフェーズの前にフィクスチャフェーズを自動注入するよう伝えます。
place_buy を実行する前に(外部キー順で)scenario: seed_product、scenario: seed_pricing_option、scenario: seed_creative で comply_test_controller を呼ぶフィクスチャフェーズを注入します。シードシナリオを実装するエージェントは箱から出してすぐ通過します。シードで UNKNOWN_SCENARIO を返すエージェントは、ストーリーボードを failed ではなく not_applicable としてグレードさせます — 実装者はサンドボックス専用の表面が欠けていることでペナルティを受けません。
ベンダーメトリックストーリーボードが決定論的な外部 measurement.metrics[] スナップショットを必要とするとき、scenario: seed_measurement_catalog を持つ明示的な comply_test_controller ステップを追加します。製品フィクスチャ内の measurement_catalogs[] は、同じストーリーボードがセラーの製品レベルケイパビリティフィールドも運ぶ必要があるときの互換性フォールバックとしてのみ使ってください。
シードシナリオとそのパラメーターの完全なリストは コンプライアンステストコントローラー — シナリオ を参照。
パターン B — context_outputs: + $context.<name> 経由のフロー由来キャプチャ
生成ステップが返した ID をキャプチャし、下流ステップで $context.<name> で参照します。
create_buy のレスポンスから media_buy_id をキャプチャし、実行スコープのコンテキストアキュムレーターに格納し、次に送信前に check_buy.sample_request のリテラル文字列 $context.media_buy_id を置換します。エージェントは実際の ID を見ます — リテラル $context.foo トークンを決して見ません。
キャプチャ失敗はリーダーではなく 生成 ステップをグレードします: レスポンスが宣言されたパスに media_buy_id を含まない場合、create_buy が capture_path_not_resolvable で失敗します。これは意図的です — ストーリーボードが宣言したコントラクト(「このステップは media_buy_id を生成する」)が失敗したもので、それを使おうとしたステップではありません。
コンテキストブロックとエコーコントラクト
レスポンスのcontext をアサートするストーリーボードは、sample_request に context: ブロックを送らなければなりません(MUST):
context: を自動注入 しません。バリデーターがレスポンスに context.correlation_id を期待するがその sample_request に context: が欠けているストーリーボードは、作成バグです — 呼び出し元が何も送らなかったとき、エージェントはコンテキストを省略することが許可されて(かつ要求されて)います。
エージェント側のルールについては コンテキストとセッション — 規範的エコーコントラクト を参照。
Asserting on errors
AdCP はエラーを 2 つの層で表面化します(エラー処理 — エンベロープ対ペイロード を参照)。ストーリーボードは、準拠エージェントがどの層でエラーを表面化したかにかかわらず機能する方法で、エラー形状をアサートしなければなりません(MUST)。check: error_code を使う — check: field_present, path: "errors" ではなく。
value: または allowed_values: で使われるすべてのコードは、static/schemas/source/enums/error-code.json の正準エラーコード enum に存在しなければなりません(MUST)。lint:error-codes スクリプト(npm run test に組み込まれている)はすべてのストーリーボードを歩き、enum にないコードへの参照を拒否します — 任意のテストが実行される前のビルド失敗です。
リネームが必要なとき、古いコードを scripts/error-code-aliases.json に登録します。ファイルは純粋なデータで(それを読む lint スクリプトの隣に存在し、スキーマツリーにはない)、デフォルトで空の aliases マップとともに出荷されます:
分岐可能な動作のアサート
一部の仕様要件は複数の準拠エージェント動作を許可します — 例: 操作がセラーポリシーに応じて即座の成功 ORpending_review を返しうる。1 つの分岐のみをアサートする単一アサーションバリデーターは、他の分岐を選んだ準拠エージェントを静かに失敗させます。
仕様が分岐可能な結果を許可するとき、ストーリーボードを並行のオプションフェーズに分割し、assert_contribution 経由で解決します:
optional: true フェーズ内の失敗はストーリーボードを失敗させません — 最終フェーズの合成 assert_contribution のみが、かつどの分岐も貢献しなかったときのみ失敗させます。準拠エージェントは正確に 1 つの分岐を通過し、設計上他方を失敗させます。
選ばれなかった分岐の失敗ステップは、failed ではなくスキップ理由 peer_branch_taken でランナーによってレポートされなければなりません(MUST)。これは準拠エージェントのランナーサマリーを正確に保ち(他分岐の失敗は本物の失敗ではなかった)、ダッシュボードカバレッジシグナルをクリーンに保ちます(peer_branch_taken はランタイムルーティング; not_applicable はプロトコルカバレッジギャップ用)。規範的ルールについては universal/storyboard-schema.yaml §「Per-step grading in any_of branch patterns」と universal/runner-output-contract.yaml > skip_result.reasons.peer_branch_taken を参照。
観測可能な結果が分岐全体で異なる任意の仕様 MAY / any_of にこの形状を使ってください。過去の create_media_buy.start_time にはこれを使わないでください。そのケースは現在 INVALID_REQUEST で拒否のみです。
単一コード check: error_code は、仕様がシナリオに正準コードを義務付けるとき(例: ガバナンス拒否結果での GOVERNANCE_DENIED、再キャンセルでの NOT_CANCELLABLE)は依然として正しいです。分割フェーズパターンは、仕様自体が結果を分岐可能に残すときのみ適用されます。
このパターンを使わないとき
並行オプションフェーズ +assert_contribution 形状は、仕様テキスト自体 が複数の観測可能な結果を許可するとき(規範的散文の明示的な MAY/OR、または受け入れ可能なステータスの enum を探す)のみ適切です。エージェントの動作が仕様からドリフトしたからベクターを軟化させるツールでは ありません。以下にこのパターンを適用しないでください:
- 冪等性セマンティクス。 ミューテーションタスクで欠けているとき
idempotency_keyは拒否されなければならない; リプレイはキャッシュされたレスポンスを返さなければならない; コンフリクトはIDEMPOTENCY_CONFLICTを表面化しなければならない。仕様は単一の動作を義務付けます — 他のどの結果も準拠せず、有効な分岐ではありません。 - コンテキストエコー。 レスポンスは、呼び出し元が送ったとき
context:を逐語的にエコーしなければなりません(MUST)。エコーを省略する準拠分岐はありません。 - エラーコード語彙。
static/schemas/source/enums/error-code.jsonに列挙された正準コードは、シナリオごとに単一値です。ストーリーボードがガバナンス拒否結果でGOVERNANCE_DENIEDをアサートするなら、それがコードです — いくつかの中の 1 つのオプションではありません。 - Webhook 署名の正しさ。 AdCP の covered-components プロファイルによる RFC 9421 署名は単一の検証形状です; 代替分岐はありません。
新しい専門分野へのカタログ置換安全性フェーズの追加
カタログアイテムマクロを URL にレンダーする専門分野(カタログ駆動セールス、生成セラー、リテールメディアなど)を追加する場合、ストーリーボードはdocs/creative/universal-macros.mdx#substitution-safety-catalog-item-macros
のルールセットをカバーする置換安全性フェーズを含めるべきです(SHOULD)。
テンプレートから始めてください。兄弟専門分野からコピペしないでください。 正準な 3 ステップフェーズ(sync_*_probe_catalog → build_*_probe_creative
→ expect_substitution_safe)は、
static/compliance/source/test-kits/substitution-observer-runner.yaml
に phase_template: コメントブロックとして存在します。
ブロックは専門分野固有のビット(ブランドドメイン、catalog_id プレフィックス、冪等性プレフィックス)に <<PLACEHOLDER>> トークンを使うので、それらのトークンに対する単純なテキスト置換を行うことで新しいフェーズを具体化できます。
sales-catalog-driven または creative-generative からほぼクローンをコピーすることは原理的には機能しますが、#2654 の DX レビュアーは、3 つのコンシューマーが些細なドリフト(item_id のミススペル、require_every_binding_observed: true の欠落)が始まる変曲点であることをフラグしました。テンプレートはドリフト回避表面で、lint:substitution-vector-names
スクリプト(#2655)が vector_name 参照のタイポを捕捉します。