> ## Documentation Index
> Fetch the complete documentation index at: https://adcp-docs-ja.pier1.co.jp/llms.txt
> Use this file to discover all available pages before exploring further.

# ストーリーボードの作成

> AdCP コンプライアンスストーリーボードの作成方法: 正準アカウント形状、セッションスコープ lint、sync_plans のプランレベルアイデンティティ、クロステナントプローブのオプトアウト。

# ストーリーボードの作成 — スコープルール

コンプライアンスストーリーボードは、コンプライアンスバンドルの正準な作成ソースである `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` を要求します — 仕様レベルに「ブランドだけ」の形状はありません。

```yaml theme={null}
sample_request:
  account:
    brand:
      domain: "acmeoutdoor.example"
    operator: "pinnacle-agency.example"
  # ...
```

明示的アカウント形式（セラーが `list_accounts` 経由で `account_id` を発行したとき）:

```yaml theme={null}
sample_request:
  account:
    account_id: "acc_acme_001"
  # ...
```

`sync_plans` では、アイデンティティは各プランエントリー内に存在します。`sync-plans-request` スキーマは各プランアイテムに `brand` を定義し、そこでの `account` を禁止します — `plans[]` 内でラッパー形式を使わないでください:

```yaml theme={null}
sample_request:
  plans:
    - plan_id: "plan-001"
      brand:
        domain: "acmeoutdoor.example"
      # ...
```

## トップレベル `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）からテナントを解決します — [テナント解決](/docs/building/integration/authentication#tenant-resolution) を参照。彼らは ID スコープタスクでエンベロープアイデンティティを必要とせず、存在しても依存しません。アイデンティティをワイヤーから外すためだけにトレーニングエージェントにクロスセッション逆インデックスを構築することは、仕様意味のないサンドボックス配管でしょう。

## 意図的なクロステナントプローブ

ステップがテナントアイデンティティなしでセッションスコープタスクをプローブすることが *意図されている* 場合 — 例: セラーがむき出しのリクエストを拒否することを検証するネガティブテスト、またはケイパビリティディスカバリープローブ — ステップをアノテーションします:

```yaml theme={null}
- id: probe_without_brand
  task: get_media_buys
  scoping: global
  sample_request:
    # ... no brand/account here by design
```

控えめに使ってください。疑わしいときはブランドアイデンティティを運びます — ほぼすべての実世界の呼び出しがそうします。

## フィクスチャとクロスステップキャプチャ

前提条件状態（特定の `product_id` を持つ製品、既に `approved` ステータスのクリエイティブ、ガバナンスフローが参照できるプラン）を必要とするストーリーボードは、それを設定する 2 つの方法があります: テストが実行される *前* に存在する状態のための **ストーリーボードルートの宣言的 `fixtures:`** と、実行 *中に生成される* ID のための **ステップ `context_outputs:` キャプチャ**。

### どちらをいつ使うか

| Fixture origin         | Pattern                                              | Authored as                                             |
| ---------------------- | ---------------------------------------------------- | ------------------------------------------------------- |
| ストーリーボードの前に存在（シードが必要）  | ストーリーボードルートの `fixtures:`                             | 宣言的ブロック; ランナーが `comply_test_controller` `seed_*` 経由でシード |
| この実行の以前のステップで生成        | 生成ステップの `context_outputs:`、後のステップの `$context.<name>` | ランタイムでキャプチャ; この実行内に留まる                                  |
| ランナー供給（webhook URL など） | `{{runner.webhook_url:<step_id>}}`                   | 置換変数                                                    |

**避けられるなら `sample_request` にリテラル ID をハードコードしないでください。** `media_buy_id: "mb_acme_q2_2026_auction"` のようなリテラルは、エージェントがたまたまその正確な ID を生成（または受け入れ）する場合のみ機能します。仕様準拠のエージェントは ID を自動生成します — リテラルは一致せず、何も間違っていない実装者に対してストーリーボードが失敗します。

### パターン A — `fixtures:` + `comply_test_controller` 経由の前提条件フィクスチャ

ストーリーボードルートでフィクスチャを宣言します。`prerequisites.controller_seeding: true` を設定して、ランナーにメインフェーズの前にフィクスチャフェーズを自動注入するよう伝えます。

```yaml theme={null}
id: sales_non_guaranteed
prerequisites:
  controller_seeding: true
  description: "Requires a seeded product and approved creative."

fixtures:
  products:
    - product_id: "test-product"
      delivery_type: "non_guaranteed"
      pricing_options:
        - pricing_option_id: "test-pricing"
          pricing_model: "cpm"
          currency: "USD"
  creatives:
    - creative_id: "campaign_hero_video"
      status: "approved"
      format_id: { id: "video_30s" }

phases:
  - id: place_buy
    steps:
      - id: create_buy
        task: create_media_buy
        sample_request:
          packages:
            - product_id: "test-product"           # ← seeded above
              pricing_option_id: "test-pricing"   # ← seeded above
```

ランナーは、`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[]` は、同じストーリーボードがセラーの製品レベルケイパビリティフィールドも運ぶ必要があるときの互換性フォールバックとしてのみ使ってください。

シードシナリオとそのパラメーターの完全なリストは [コンプライアンステストコントローラー — シナリオ](/docs/building/implementation/comply-test-controller#scenarios) を参照。

### パターン B — `context_outputs:` + `$context.<name>` 経由のフロー由来キャプチャ

生成ステップが返した ID をキャプチャし、下流ステップで `$context.<name>` で参照します。

```yaml theme={null}
steps:
  - id: create_buy
    task: create_media_buy
    sample_request:
      packages: [...]
    context_outputs:
      - name: media_buy_id
        path: "media_buy_id"        # JSON path against this step's response

  - id: check_buy
    task: get_media_buys
    sample_request:
      media_buy_ids: ["$context.media_buy_id"]   # ← resolved at run time
```

ランナーは（バリデーションが通過した後）`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）:

```yaml theme={null}
sample_request:
  packages: [...]
  context:
    correlation_id: "sales_non_guaranteed--create_buy"
validations:
  - check: field_value
    path: "context.correlation_id"
    value: "sales_non_guaranteed--create_buy"
    description: "Agent echoes context verbatim"
```

ランナーは、それを省略する sample\_request に `context:` を自動注入 **しません**。バリデーターがレスポンスに `context.correlation_id` を期待するがその sample\_request に `context:` が欠けているストーリーボードは、作成バグです — 呼び出し元が何も送らなかったとき、エージェントはコンテキストを省略することが許可されて（かつ要求されて）います。

エージェント側のルールについては [コンテキストとセッション — 規範的エコーコントラクト](/docs/building/integration/context-sessions#normative-echo-contract) を参照。

## Asserting on errors

AdCP はエラーを 2 つの層で表面化します（[エラー処理 — エンベロープ対ペイロード](/docs/building/implementation/error-handling#envelope-vs-payload-errors-the-two-layer-model) を参照）。ストーリーボードは、準拠エージェントがどの層でエラーを表面化したかにかかわらず機能する方法で、エラー形状をアサートしなければなりません（MUST）。

**`check: error_code` を使う — `check: field_present, path: "errors"` ではなく。**

```yaml theme={null}
# ✅ Shape-agnostic — resolves from either adcp_error (envelope) or errors[] (payload)
validations:
  - check: error_code
    value: "BUDGET_TOO_LOW"
    description: "Budget validation rejected with BUDGET_TOO_LOW"

# ✅ Multiple acceptable codes
validations:
  - check: error_code
    allowed_values: ["VALIDATION_ERROR", "INVALID_REQUEST", "BUDGET_TOO_LOW"]

# ❌ Pins to the payload `errors[]` shape — fails against agents that surface
#    errors only via the transport envelope (MCP `adcp_error`, A2A DataPart)
validations:
  - 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` マップとともに出荷されます:

```json theme={null}
{
  "aliases": {
    "OLD_CODE": "NEW_CODE"
  }
}
```

エイリアスされたコードは、非推奨ウィンドウの間 **警告** として lint を通過し、作成者にストーリーボードを移行する時間を与えます。エイリアスがファイルから削除されると、古いコードへの参照は lint エラーになります。これがバージョン全体でストーリーボード作成を壊さずにリネームが着地する方法です。

## 分岐可能な動作のアサート

一部の仕様要件は複数の準拠エージェント動作を許可します — 例: 操作がセラーポリシーに応じて即座の成功 OR `pending_review` を返しうる。1 つの分岐のみをアサートする単一アサーションバリデーターは、他の分岐を選んだ準拠エージェントを静かに失敗させます。

仕様が分岐可能な結果を許可するとき、ストーリーボードを並行のオプションフェーズに分割し、`assert_contribution` 経由で解決します:

```yaml theme={null}
phases:
  - id: reject_path
    optional: true
    steps:
      - id: probe_reject
        expect_error: true
        contributes_to: behavior_handled
        validations:
          - check: error_code
            value: "INVALID_REQUEST"

  - id: adjust_path
    optional: true
    steps:
      - id: probe_adjust
        contributes_to: behavior_handled
        validations:
          - check: response_schema
          - check: field_present
            path: "media_buy_id"

  - id: enforcement
    steps:
      - id: require_either
        task: assert_contribution
        validations:
          - check: any_of
            allowed_values: ["behavior_handled"]
            description: "Agent must exhibit one of the conformant branches."
```

`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`](../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`](https://github.com/adcontextprotocol/adcp/blob/main/static/compliance/source/test-kits/substitution-observer-runner.yaml)
に `phase_template:` コメントブロックとして存在します。
ブロックは専門分野固有のビット（ブランドドメイン、catalog\_id プレフィックス、冪等性プレフィックス）に `<<PLACEHOLDER>>` トークンを使うので、それらのトークンに対する単純なテキスト置換を行うことで新しいフェーズを具体化できます。

`sales-catalog-driven` または `creative-generative` からほぼクローンをコピーすることは原理的には機能しますが、[#2654](https://github.com/adcontextprotocol/adcp/issues/2654) の DX レビュアーは、3 つのコンシューマーが些細なドリフト（`item_id` のミススペル、`require_every_binding_observed: true` の欠落）が始まる変曲点であることをフラグしました。テンプレートはドリフト回避表面で、`lint:substitution-vector-names`
スクリプト（[#2655](https://github.com/adcontextprotocol/adcp/issues/2655)）が vector\_name 参照のタイポを捕捉します。

## lint をローカルで実行する

```bash theme={null}
npm run build:compliance    # includes the lint
node scripts/lint-storyboard-scoping.cjs    # lint only
npm run test:storyboard-scoping    # parity test
```

典型的な失敗出力:

```
✗ storyboard scoping lint: 1 violation(s)

  protocols/media-buy/scenarios/invalid_transitions.yaml:setup/create_buy (create_media_buy) — sample_request missing brand/account

Fix: add `account { brand, operator }` to sample_request, e.g.
  sample_request:
    account:
      brand:
        domain: "acmeoutdoor.example"
      operator: "pinnacle-agency.example"
```
