> ## 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.

# コンプライアンステストコントローラー

> セラー側の遷移を決定的にトリガーすることで、ストーリーボードランナーが完全なライフサイクルステートマシンを歩けるようにするオプションのサンドボックスツール。

# コンプライアンステストコントローラー

<Note>
  **コンプライアンステストコントローラーは開発/ステージング専用のアフォーダンスであり、本番時の概念ではありません。** AAO グレーディングはそれを要求も使用もしません。AAO コンプライアンスハートビートは、すべてのリクエストに `account.sandbox: true` を付けてセラーの登録された本番 URL に対してストーリーボードを駆動し、セラーの本番スタックがフラグを尊重する責任を負います — コントローラーエンドポイントは不要です。

  セラーは、自身の統合テストをサポートするため開発またはステージング環境でコントローラーを実装してもよい（MAY） — ライフサイクルステートマシンを決定的に歩く、フィクスチャをシードする、そうでなければ実時間を待つ必要がある遷移を強制する。それがその目的です。本番デプロイで公開してはなりません（MUST NOT）（下の [Sandbox gating](#sandbox-gating) を参照）。

  コントローラーが AAO Verified (Sandbox) にどう関係するか混乱していますか? フレーミング決定については [#4379](https://github.com/adcontextprotocol/adcp/issues/4379) を参照してください: (Sandbox) は「実本番エンドポイントが完全なストーリーボードスイート全体でサンドボックスフラグ付きトラフィックを正しく処理する」ことを証明します。コントローラーは *あなたの* テストのための開発者側のアフォーダンスであり、AAO 側のグレーディングメカニズムではありません。
</Note>

AdCP は、アカウント、クリエイティブ、メディアバイ、SI セッション、配信レポートのライフサイクルステートマシンを定義します。これらのステートマシンの多くの遷移はセラー開始です — クリエイティブ承認、アカウント停止、予算枯渇、配信計上。ストーリーボードランナーはバイヤー開始フローのみを行使でき、セラー開始遷移を未テストのままにします。

**コンプライアンステストコントローラー** は、決定的ローカルテストをサポートするためセラーが開発/ステージング環境で公開するオプションのツールです。ランナーがセラー側の状態遷移をオンデマンドでトリガーでき、開発中にエンドツーエンドのライフサイクル検証を可能にします。

## 動機

テストコントローラーなしでは、コンプライアンステストは観測的です: アクションを発火し、存在する状態を読み返し、進む。これはスキーマ違反を捕捉しますが動作違反は捕捉しません。

| Track           | Observational (today)                        | Deterministic (with controller)                                 |
| --------------- | -------------------------------------------- | --------------------------------------------------------------- |
| **Creative**    | Sync → 初期ステータスを観測                            | `processing` → `approved` → `archived` を歩く。理由付きで `rejected` を強制 |
| **Account**     | 既存ステータスを読む                                   | `suspended` を強制 → 操作ゲートを検証 → 再アクティブ化                            |
| **SI sessions** | Initiate → message → terminate               | タイムアウト理由で `terminated` を強制 → 次の呼び出しで `SESSION_NOT_FOUND` を検証    |
| **Reporting**   | `get_media_buy_delivery` を呼ぶ → データが存在することを望む | 配信をシミュレート → ロールアップを検証                                           |
| **Budgeting**   | 予算付きでバイを作成 → 読み返す                            | しきい値まで支出をシミュレート → アラートと `payment_required` を検証                  |
| **Media buy**   | Create → pause → resume                      | セラー開始 `rejected` を強制 → 終端状態を検証                                  |

## Sandbox gating

セラーは本番デプロイで `comply_test_controller` を公開してはなりません（MUST NOT） — 誰にも、どの表面でも。ツールは `tools/list`（MCP）とエージェントカードの `skills[]`（A2A）から不在でなければならず（MUST）、`compliance_testing` ブロックは `get_adcp_capabilities` から不在でなければならず（MUST）、ディスパッチはトランスポートの標準未知ツールエラー（例: MCP の JSON-RPC `-32601 Method not found`、A2A の未知スキル拒否）を返さなければなりません（MUST） — ツールを実装しないセラーの同一トランスポートレスポンスと区別できない。これらの表面のいずれかでツールを公開する本番デプロイは、ディスパッチがゲートされているかどうかにかかわらず非適合です。

正準パターンは 2 つのデプロイです: 1 つは本番（コントローラー未配線）、1 つはサンドボックス/ステージング（すべての来訪者向けにコントローラー配線）。セラーはサンドボックス/ステージングデプロイでのみ `comply_test_controller` を公開します。そのようなデプロイに認証できる任意のプリンシパルがそれを呼べます。

セラーは代わりに、混合サンドボックス/ライブプリンシパルを持つ単一デプロイを実行し、解決されたアカウントのモードでゲートしてプリンシパルごとにツールを投影してもよい（MAY）。これは実装パターンであり、正準モデルではありません。このパターンを選ぶセラーは 3 つの表面すべてを一貫してゲートしなければなりません（MUST）: `tools/list`（または `skills[]`）、`compliance_testing` ケイパビリティブロック、ディスパッチ。部分的な投影 — 例: `tools/list` をゲートするが `compliance_testing` ブロックをライブプリンシパルに可視のまま残す、または名前でプローブするライブプリンシパルに（未知ツールではなく）`FORBIDDEN` を返す — は非適合です。それはデプロイスコーピングが閉じるディスカバリーサイドチャネルを再開きます。

`FORBIDDEN` は、呼び出し元がコントローラーを呼ぶ権限があるが `params` が非サンドボックスアカウントを参照するサンドボックス内ケースのために予約されています。サンドボックスゲートは、ツール登録時だけでなく、アカウント参照についてリクエストごとに強制されます。

サンドボックス認証情報のプロビジョニングと、本番をサンドボックス/ステージングデプロイから分離するメカニズムはセラー固有で、この仕様の範囲外です。セラーは、ストーリーボードランナーが適切に接続できるよう、サンドボックスアクセスメカニズムを文書化しなければなりません（MUST）。

ストーリーボードランナーは、本番と信じる接続上で `tools/list`（または `skills[]`）内の `comply_test_controller` の存在、または `get_adcp_capabilities` 内の `compliance_testing` ブロックの存在を、ハードな適合性失敗として扱わなければなりません（MUST）。

## ツール定義

**Schemas**: [`comply-test-controller-request.json`](https://adcontextprotocol.org/schemas/v3/compliance/comply-test-controller-request.json) | [`comply-test-controller-response.json`](https://adcontextprotocol.org/schemas/v3/compliance/comply-test-controller-response.json)

コンプライアンステストコントローラーを実装するセラーは以下をしなければなりません（MUST）:

* サンドボックスモードでのみツールを公開（上のサンドボックスゲートを参照）
* 本番と同じ状態遷移ルールを強制 — 無効な遷移はエラーを返さなければならない（MUST）
* 強制された状態変更を後続の読み取り（`list_creatives`、`get_media_buys` など）に反映

```json theme={null}
{
  "name": "comply_test_controller",
  "description": "Triggers seller-side state transitions for compliance testing. Sandbox only.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "scenario": {
        "type": "string",
        "enum": [
          "list_scenarios",
          "force_creative_status",
          "force_creative_purge",
          "force_account_status",
          "force_media_buy_status",
          "force_create_media_buy_arm",
          "force_get_products_arm",
          "force_get_signals_arm",
          "force_task_completion",
          "force_session_status",
          "simulate_delivery",
          "simulate_budget_spend",
          "seed_account",
          "seed_product",
          "seed_pricing_option",
          "seed_creative",
          "seed_plan",
          "seed_media_buy",
          "seed_creative_format",
          "seed_measurement_catalog",
          "query_upstream_traffic",
          "query_provenance_audit_observations",
          "force_upstream_unavailable"
        ],
        "description": "The seller-side transition or fixture-seed to trigger."
      },
      "params": {
        "type": "object",
        "description": "Scenario-specific parameters. Omit for list_scenarios. force_creative_status: {creative_id, status, rejection_reason?}. force_creative_purge: {creative_id, purge_kind?, reason_code?, reason_detail?}. force_account_status: {account_id, status}. force_media_buy_status: {media_buy_id, status, rejection_reason?}. force_create_media_buy_arm: {arm, task_id?, message?} - task_id required when arm = submitted. force_get_products_arm: {arm, task_id?, message?} - task_id required when arm = submitted. force_get_signals_arm: {arm, task_id?, message?} - task_id required when arm = submitted. force_task_completion: {task_id, result}. force_session_status: {session_id, status, termination_reason?}. simulate_delivery: {media_buy_id, impressions?, clicks?, reported_spend?, conversions?, reach?, frequency?, reach_window?, viewability?}. simulate_budget_spend: {account_id|media_buy_id, spend_percentage}. seed_account: {account_id, fixture?}. seed_product: {product_id, fixture?}. seed_pricing_option: {product_id, pricing_option_id, fixture?}. seed_creative: {creative_id, fixture?}. seed_plan: {plan_id, fixture?}. seed_media_buy: {media_buy_id, fixture?}. seed_creative_format: {format_id, fixture?}. seed_measurement_catalog: {vendor, metrics[]}. query_upstream_traffic: {since_timestamp?, endpoint_pattern?, limit?, attestation_mode?, identifier_value_digests?}. query_provenance_audit_observations: {creative_id}. force_upstream_unavailable: {tool, upstream_name?}."
      }
    },
    "required": ["scenario"]
  }
}
```

<Note>
  `params` の description は、MCP クライアント（LLM を含む）が条件付きスキーマ分岐ではなく description を読むため、各シナリオの param 形状をインラインします。SDK コード生成に適した形式的検証スキーマについては、下のシナリオごとの定義を参照してください。
</Note>

## Scenarios

### `force_creative_status`

クリエイティブを指定されたステータスに遷移させます。セラーは [クリエイティブライフサイクルステートマシン](/docs/creative/specification#creative-status-lifecycle) に従い有効な遷移を強制しなければなりません（MUST）。

**Params:**

| Field              | Type                                                                                      | Required                  | Description       |
| ------------------ | ----------------------------------------------------------------------------------------- | ------------------------- | ----------------- |
| `creative_id`      | string                                                                                    | Yes                       | 遷移するクリエイティブ       |
| `status`           | `processing` \| `pending_review` \| `approved` \| `suspended` \| `rejected` \| `archived` | Yes                       | ターゲットステータス        |
| `rejection_reason` | string                                                                                    | `status` = `rejected` のとき | 拒否の理由             |
| `reason_code`      | CreativeEventReasonCode                                                                   | No                        | ライフサイクル遷移の理由コード   |
| `reason_detail`    | string                                                                                    | No                        | ライフサイクル遷移の人間可読な詳細 |

**Example:**

```json theme={null}
{
  "scenario": "force_creative_status",
  "params": {
    "creative_id": "cr-123",
    "status": "rejected",
    "reason_code": "policy_revocation",
    "rejection_reason": "Brand safety policy violation"
  }
}
```

### `force_account_status`

アカウントを指定されたステータスに遷移させます。セラーは [アカウントライフサイクルルール](/docs/accounts/overview#account-status-lifecycle) を強制しなければなりません（MUST） — 終端状態（`rejected`、`closed`）は退出できません。

**Params:**

| Field        | Type                                                                                          | Required | Description |
| ------------ | --------------------------------------------------------------------------------------------- | -------- | ----------- |
| `account_id` | string                                                                                        | Yes      | 遷移するアカウント   |
| `status`     | `active` \| `pending_approval` \| `rejected` \| `payment_required` \| `suspended` \| `closed` | Yes      | ターゲットステータス  |

**Example:**

```json theme={null}
{
  "scenario": "force_account_status",
  "params": {
    "account_id": "acct-456",
    "status": "payment_required"
  }
}
```

### `force_media_buy_status`

メディアバイを指定されたステータスに遷移させます。セラーはメディアバイライフサイクルを強制しなければなりません（MUST） — `rejected` は `pending_creatives` または `pending_start` からのみ有効です。

**Params:**

| Field              | Type                                                                                                      | Required                  | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------- | ----------- |
| `media_buy_id`     | string                                                                                                    | Yes                       | 遷移するメディアバイ  |
| `status`           | `pending_creatives` \| `pending_start` \| `active` \| `paused` \| `completed` \| `rejected` \| `canceled` | Yes                       | ターゲットステータス  |
| `rejection_reason` | string                                                                                                    | `status` = `rejected` のとき | 拒否の理由       |

**Example:**

```json theme={null}
{
  "scenario": "force_media_buy_status",
  "params": {
    "media_buy_id": "mb-789",
    "status": "rejected",
    "rejection_reason": "Policy violation"
  }
}
```

### `force_create_media_buy_arm`

呼び出し元の認証済みサンドボックスアカウントからの次の [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy) 呼び出しを特定のレスポンスアームに形作ります。v1 は 2 つのアームをサポートします: `submitted`（非同期タスクエンベロープ、まだ `media_buy_id` なし）と `input-required`（errors 分岐）。`force_media_buy_status` と異なり、エンティティは遷移しません — まだメディアバイがありません — したがってレスポンスは `previous_state`/`current_state` ではなく `forced.arm` を運びます。

submitted アームのワイヤー形状はそれ以外は実装依存です: ほとんどのセラーはほとんどのバイを同期的にルーティングし、どのバイヤー側リクエスト形状も確実に非同期をトリガーしません。このシナリオはストーリーボードがアームをピン留めできるようにし、退行したセラー（例: `status: submitted` の下で `media_buy_id` を発行）が黙って適合性を通過できないようにします。

**Params:**

| Field     | Type                            | Required                | Description                                                                                                                                                                                                                                                                                               |
| --------- | ------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `arm`     | `submitted` \| `input-required` | Yes                     | 次の `create_media_buy` 呼び出しのターゲットレスポンスアーム                                                                                                                                                                                                                                                                  |
| `task_id` | string                          | `arm` = `submitted` のとき | セラーが submitted エンベロープ上でそのまま発行しなければならず（MUST）、後続の `tasks/get` ポーリングで受理しなければならない（MUST）決定的タスクハンドル（最大 128 文字）。サンドボックス task\_id は呼び出し元不透明な文字列。本番 task-id 形式ルールは適用されない。                                                                                                                                          |
| `message` | string                          | No                      | セラーの `create_media_buy` レスポンス上にそのまま表示される人間可読な説明。プレーンテキスト、最大 2000 文字。結果のレスポンスを消費するバイヤーは、[submitted エンベロープの `message`](https://adcontextprotocol.org/schemas/v3/media-buy/create-media-buy-response.json) について文書化されたプロンプトインジェクションサニタイズを適用しなければならない（MUST） — このシナリオは、ランナーがバイヤー側サニタイズをテストするため敵対的文字列を注入する自然な場所。 |

**Example:**

```json theme={null}
{
  "scenario": "force_create_media_buy_arm",
  "params": {
    "arm": "submitted",
    "task_id": "task_async_signed_io_q2",
    "message": "Awaiting IO signature from sales team; typical turnaround 2–4 hours"
  }
}
```

**Response.** 登録されたディレクティブを運ぶ `ForcedDirectiveSuccess` 形状:

```json theme={null}
{
  "success": true,
  "forced": {
    "arm": "submitted",
    "task_id": "task_async_signed_io_q2"
  },
  "message": "Next create_media_buy call will return the submitted arm with task_id task_async_signed_io_q2"
}
```

`forced.task_id` は `arm: submitted` のときのみ存在します。

**Consumption and idempotency.** ディレクティブは呼び出し元の認証済みサンドボックスアカウント（アカウント + プリンシパルペア）にキーされ、そのアカウントからの次の `create_media_buy` 呼び出しで消費されます。新しいディレクティブなしの後続呼び出しはセラーのデフォルトアームを返します。バイヤー側 `idempotency_key` セマンティクスは変わりません: 呼び出し元が既にディレクティブを消費した `create_media_buy` リクエストをリプレイする場合、セラーはキャッシュされたレスポンスをリプレイしなければならず（MUST）（リクエスト冪等性キャッシュが勝つ）、今や空のディレクティブスロットに対して再評価してはなりません（MUST NOT）。セラーは、同じトランスポート接続内でも、異なるアカウントまたはプリンシパルからの `create_media_buy` 呼び出しに対してディレクティブをマッチしてはなりません（MUST NOT）。ディレクティブが消費される前の 2 つ目の `force_create_media_buy_arm` 呼び出しは前のものを上書きします。

### `force_get_products_arm` / `force_get_signals_arm`

呼び出し元の認証済みサンドボックスアカウント（アカウント + プリンシパルペア）からの次のキュレートディスカバリー呼び出しを submitted タスクエンベロープに形作ります。`force_get_products_arm` は `buying_mode: "brief"` または `"refine"` の `get_products` にのみ適用されます。`force_get_signals_arm` は `discovery_mode: "brief"`（または省略、brief がデフォルト）の `get_signals` にのみ適用されます。ホールセールフィード読み取りは同期フィードアクセスであり、これらのディレクティブを消費してはならず（MUST NOT）、ディレクティブが存在するというだけで Submitted アームを返してはなりません（MUST NOT）。

ディレクティブは `force_create_media_buy_arm` と同じ理由で存在します: バイヤーはリクエスト形状だけから確実に非同期ディスカバリーをトリガーできませんが、適合性はクライアントとセラーがタスク結果パスを尊重することを証明する決定的な方法を必要とします。submitted エンベロープは `status` と `task_id`（プラス `message` のようなオプションの助言フィールド）のみを運びます。終端の `products[]`、`proposals[]`、または `signals[]` は、`get_task_status`（レガシー `tasks/get`）と任意の登録されたプッシュ通知を通じてタスク完了時に着地します。

**Params:**

| Field     | Type        | Required                | Description                                                                                  |
| --------- | ----------- | ----------------------- | -------------------------------------------------------------------------------------------- |
| `arm`     | `submitted` | Yes                     | 次の一致するディスカバリー呼び出しのターゲットレスポンスアーム。                                                             |
| `task_id` | string      | `arm` = `submitted` のとき | セラーが submitted エンベロープ上でそのまま発行しなければならず（MUST）、後続のポーリングで受理しなければならない（MUST）決定的タスクハンドル（最大 128 文字）。 |
| `message` | string      | No                      | submitted ディスカバリーレスポンス上にそのまま表示される人間可読な説明。プレーンテキスト、最大 2000 文字。                                |

**Examples:**

```json theme={null}
{
  "scenario": "force_get_products_arm",
  "account": {
    "brand": {
      "domain": "acmeoutdoor.example"
    },
    "operator": "pinnacle-agency.example",
    "sandbox": true
  },
  "params": {
    "arm": "submitted",
    "task_id": "task_async_products_acme_q3",
    "message": "Custom product curation queued; typical turnaround 10 minutes"
  }
}
```

```json theme={null}
{
  "scenario": "force_get_signals_arm",
  "account": {
    "brand": {
      "domain": "novamotors.example"
    },
    "operator": "pinnacle-agency.example",
    "sandbox": true
  },
  "params": {
    "arm": "submitted",
    "task_id": "task_async_signals_nova_ev",
    "message": "Signal discovery queued; typical turnaround 10 minutes"
  }
}
```

**Response.** 両シナリオとも `force_create_media_buy_arm` と同じ `ForcedDirectiveSuccess` 形状を返し、`forced.arm` と `forced.task_id` を運びます。

**Consumption and idempotency.** ディレクティブは呼び出し元の認証済みサンドボックスアカウント（アカウント + プリンシパルペア）にキーされ、その同じアカウントからの次の一致するディスカバリー呼び出しで消費されます。セラーは、製品ディレクティブを `get_signals` に、シグナルディレクティブを `get_products` に、brief/refine ディレクティブをホールセールモードに、または任意のディレクティブを異なるアカウントまたはプリンシパルにマッチしてはなりません（MUST NOT）。消費前の同じ操作に対する 2 つ目のディレクティブは前のディレクティブを上書きします。リクエスト冪等性リプレイセマンティクスは変わりません: ディレクティブを消費したディスカバリーリクエストがリプレイされる場合、セラーはキャッシュされた submitted エンベロープを返し、新しいディレクティブを消費しません。

### `force_task_completion`

以前に submitted された非同期タスクを、バイヤー供給の結果ペイロードで `completed` に解決します。`force_*_arm` シナリオの相棒: それらのシナリオはセラーを submitted エンベロープに駆動します。これはタスクストアエントリーを `completed` に遷移させ登録された結果をスタンプすることでループを閉じます。バイヤーは、`push_notification_config.url` へのセラーのプッシュ通知と、`status: "completed"` をレポートする後続の `get_task_status` 呼び出しを通じて完了を観測します。呼び出し元が `include_result: true` を要求するとき、`get_task_status` は元の非同期操作に一致する型付き終端結果ペイロードを返します。

submitted → completed ライフサイクルはそれ以外は非決定的です — 実タスク完了は帯域外シグナル（IO 副署名、バッチプロセッサー cron、ガバナンス人間レビュー）に乗ります。ストーリーボードは待てません。このシナリオは、ランナーがディレクティブ登録直後に完了を決定的にピン留めできるようにし、バイヤー側ポーリングアサーションがバイヤーが本番で観測するのと同じワイヤー形状で発火するようにします。

**Params:**

| Field     | Type                                                                                            | Required | Description                                                                                                                                                                                                                                                                                                                                              |
| --------- | ----------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `task_id` | string                                                                                          | Yes      | 解決するタスク。呼び出し元の認証済みサンドボックスアカウント内で解決しなければならない（MUST）。セラーは他のアカウントに属する `task_id` に対して `NOT_FOUND` を返さなければならない（MUST）（上のマルチテナント規約に従い `FORBIDDEN` ではない）。通常は先の `create_media_buy` submitted エンベロープレスポンスから捕捉（または `force_create_media_buy_arm` 経由で登録）。                                                                                                              |
| `result`  | [`async-response-data`](https://adcontextprotocol.org/schemas/v3/core/async-response-data.json) | Yes      | 記録する完了ペイロード。プッシュ通知 webhook と `tasks/get` ポーリングレスポンスが使う同じ `anyOf` union に対して検証される。`create_media_buy` については、これは `media_buy_id` と `packages` を持つ `CreateMediaBuyResponse`。`result` がタスクの元のメソッドのレスポンス分岐に対して検証されない場合、セラーは `INVALID_PARAMS` を発行しなければならない（MUST）。セラーは 256 KB を超える `result` ペイロードを `INVALID_PARAMS` で拒否してもよい（MAY）。ストーリーボードはこの下に留まらなければならない（MUST）。 |

**Example:**

```json theme={null}
{
  "scenario": "force_task_completion",
  "params": {
    "task_id": "task_async_signed_io_q2",
    "result": {
      "media_buy_id": "mb_async_signed_io_q2",
      "status": "active",
      "packages": [
        { "package_id": "pkg-0", "product_id": "async_signed_io_q2", "budget": 30000 }
      ]
    }
  }
}
```

**Response.** 状態遷移成功形状を返します:

```json theme={null}
{
  "success": true,
  "previous_state": "submitted",
  "current_state": "completed",
  "message": "Task task_async_signed_io_q2 transitioned from submitted to completed"
}
```

ソース状態は `submitted`、`working`、または `input-required` でなければならない（MUST）。他のソースは `INVALID_TRANSITION` を返します。`task_id` が呼び出し元のアカウントに未知なら、セラーは `NOT_FOUND` を発行しなければならず（MUST）、タスクが既に終端（`completed` / `failed` / `canceled`）なら `INVALID_TRANSITION` を発行しなければなりません（MUST）。タスクを `failed` に強制することはこのシナリオの範囲外です。`force_create_media_buy_arm` の input-required アームがバイヤー入力必要失敗パスをカバーします。

**Replay semantics.** タスクが終端になる前の同一 params でのリプレイは冪等な no-op です。タスクが終端になる前の分岐する params でのリプレイは登録された結果を上書きしなければなりません（MUST）（last-write-wins） — `force_create_media_buy_arm` の「2 つ目の呼び出しが上書き」と同じ前例。タスクが終端になった後、すべてのリプレイは params にかかわらず `INVALID_TRANSITION` を返します。

**Cross-protocol obligations.**

* **プッシュ通知。** バイヤーが元の `create_media_buy` で `push_notification_config.url` を登録した場合、完了強制は登録された `result` ペイロードで webhook を発火しなければなりません（MUST）（完了データの正準 3.0 配信パス）。そうでなければストーリーボードは終端ステータスのポーリングのみをテストでき、結果のプッシュ配信はテストできません。
* **`simulate_delivery` / `simulate_budget_spend`。** `media_buy_id` を運ぶ有効な `CreateMediaBuyResponse` で completed に強制されると、結果のメディアバイはそれらのシナリオでアドレス可能でなければなりません（MUST）。`force_task_completion` を通じたラウンドトリップは、同期フローを通らずにメディアバイを必要とするストーリーボードのサポートされたパスです。

**Buyer-side observation.** このシナリオが実行された後、登録された `result` はすべての呼び出し元供給フィールドを保持してバイヤーの `push_notification_config.url`（3.0 正準パス）に配信されます。セラーはセラー制御フィールド（例: `created_at`、`dsp_*` ID、正規化された通貨ケーシング）で拡張してもよい（MAY）が、呼び出し元供給値を上書きしてはなりません（MUST NOT）。後続の `tasks/get(task_id)` は `status: "completed"` を返さなければなりません（MUST）。`result` ペイロードはサンドボックスでバイヤー制御でセラーのストアを通じてラウンドトリップします — webhook 経由でそれを受け取るバイヤーは、バイト自体を起源としたという事実にかかわらず、ペイロードを信頼できないセラー出力として扱わなければなりません（MUST）（AdCP 規約に従い）。これは `force_task_completion` を、webhook 配信パスでバイヤー側サニタイズをテストするときランナーが敵対的ペイロードを注入する自然な場所にします。

### `force_session_status`

SI セッションを終端ステータスに遷移させます。そうでなければ実タイムアウトを待つ必要があるタイムアウトと終了シナリオのテストを可能にします。`termination_reason` param は原因をシミュレートし、ストーリーボードランナーがセラーが後続レスポンスで正しい理由をレポートすることを検証できます。

**Params:**

| Field                | Type                       | Required                    | Description                                                      |
| -------------------- | -------------------------- | --------------------------- | ---------------------------------------------------------------- |
| `session_id`         | string                     | Yes                         | 遷移するセッション                                                        |
| `status`             | `complete` \| `terminated` | Yes                         | ターゲット終端ステータス                                                     |
| `termination_reason` | string                     | `status` = `terminated` のとき | 終了の理由（例: `session_timeout`、`host_terminated`、`policy_violation`） |

**Example:**

```json theme={null}
{
  "scenario": "force_session_status",
  "params": {
    "session_id": "sess-abc",
    "status": "terminated",
    "termination_reason": "session_timeout"
  }
}
```

### `simulate_delivery`

メディアバイの合成配信データを注入します。`get_media_buy_delivery` への後続呼び出しはこのデータを反映しなければなりません（MUST）。配信シミュレーションは加算的です — 各呼び出しが既存の配信合計に加算します。

**配信と予算は独立したシステムです。** `simulate_delivery` は広告サーバーがレポートするものを記録します。`simulate_budget_spend` は課金システムが追跡するものを記録します。セラーの本番システムはこれらを結合してもしなくてもよい — テストコントローラーは結合を仮定しません。

**Params:**

| Field            | Type    | Required | Description                                                                                                                                                                               |
| ---------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `media_buy_id`   | string  | Yes      | 配信を加えるメディアバイ                                                                                                                                                                              |
| `impressions`    | integer | No       | シミュレートするインプレッション                                                                                                                                                                          |
| `clicks`         | integer | No       | シミュレートするクリック                                                                                                                                                                              |
| `reported_spend` | object  | No       | `{ amount: number, currency: string }` — 配信データでレポートされる支出、予算に影響しない                                                                                                                         |
| `conversions`    | integer | No       | シミュレートするコンバージョン                                                                                                                                                                           |
| `reach`          | number  | No       | `totals.reach` に表示するユニークリーチ数                                                                                                                                                              |
| `frequency`      | number  | No       | `totals.frequency` に表示するリーチ単位あたり平均フリークエンシー                                                                                                                                                |
| `reach_window`   | object  | No       | シミュレートされたリーチ/フリークエンシーの測定ウィンドウ。形状: `{ kind: "cumulative" }`、`{ kind: "period", period: Duration }`、または `{ kind: "rolling", period: Duration }`                                             |
| `viewability`    | object  | No       | `totals.viewability` に表示するビューアビリティブロック。`measurable_impressions`、`viewable_impressions`、`viewable_rate`、`viewed_seconds`、`standard` を含む。測定されたビューアビリティ値が存在するときは常に `standard` を供給すべき（SHOULD） |

**Example:**

```json theme={null}
{
  "scenario": "simulate_delivery",
  "params": {
    "media_buy_id": "mb-789",
    "impressions": 10000,
    "clicks": 150,
    "reach": 4000,
    "frequency": 2.5,
    "reach_window": { "kind": "rolling", "period": { "interval": 7, "unit": "days" } },
    "viewability": {
      "measurable_impressions": 9000,
      "viewable_impressions": 7200,
      "viewable_rate": 0.8,
      "viewed_seconds": 4.3,
      "standard": "mrc"
    },
    "reported_spend": { "amount": 150.00, "currency": "USD" }
  }
}
```

### `simulate_budget_spend`

指定されたパーセンテージまでの予算消費をシミュレートします。実支出を待たずに予算しきい値アラートと `payment_required` 遷移のテストを可能にします。これはアカウントレベルの財務状態に影響する唯一のシナリオです。

`simulate_budget_spend` を呼んだ後、セラーはシミュレートされた消費を `get_account_financials` に反映しなければなりません（MUST）。具体的には:

* `total_spend`（または同等）はシミュレートされた金額を反映しなければならない（MUST）
* `remaining_budget`（または同等）はそれに応じて減らされなければならない（MUST）
* 予算利用率パーセンテージは `spend_percentage` に一致しなければならない（MUST）

**Params:**

| Field              | Type   | Required | Description         |
| ------------------ | ------ | -------- | ------------------- |
| `account_id`       | string | No       | アカウント（アカウントレベル予算用）  |
| `media_buy_id`     | string | No       | メディアバイ（バイレベル予算用）    |
| `spend_percentage` | number | Yes      | 予算のこの % まで支出（0–100） |

`account_id` または `media_buy_id` の少なくとも 1 つが必要です。ターゲットエンティティは非ゼロ予算が構成されていなければならず（MUST）、そうでない場合コントローラーは `INVALID_PARAMS` を返すべきです（SHOULD）。

**Example:**

```json theme={null}
{
  "scenario": "simulate_budget_spend",
  "params": {
    "media_buy_id": "mb-789",
    "spend_percentage": 95
  }
}
```

### `seed_product`

後続のストーリーボードステップが安定した ID で製品を参照できるよう、呼び出し元供給の `product_id` を持つ製品フィクスチャを作成（またはアップサート）します。フィクスチャが明示的に hidden とマークしない限り、コントローラーはシードされた製品を認証済みアカウントの下で `get_products` 経由で発見可能にしなければなりません（MUST）。

**なぜこのシナリオが存在するか。** ストーリーボードは `"test-product"` のようなフィクスチャ ID をハードコードし、セラーが一致する製品を持つことを期待します。シードシナリオなしでは、すべての実装者が適合性スイートがどの ID を期待するかを再発見し手動でエイリアスしなければなりません。`seed_product` はその発見を明示的でストーリーボード作成のコントラクトに置き換えます。

**Params:**

| Field        | Type   | Required | Description                                                                                                       |
| ------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `product_id` | string | Yes      | ストーリーボードが参照する安定した識別子                                                                                              |
| `fixture`    | object | No       | 製品形状。最小限有用なフィールド: `delivery_type`、`channels`、`pricing_options[]`、`format_ids[]`。セラーは省略されたフィールドのデフォルトを埋めてもよい（MAY）。 |

ベンダーメトリック前提条件テストには、外部ベンダーカタログには `seed_measurement_catalog` を優先してください。製品フィクスチャは、製品コントラクトと参照される測定スナップショットを 1 つのフィクスチャで必要とするローカルハーネスの互換性フォールバックとして、`{ vendor, metrics[] }` として形作られた `measurement_catalogs[]` エントリーも運べます。同じベンダーに両方が供給される場合、明示的な `seed_measurement_catalog` スナップショットがそのコンプライアンスセッションで優先されます。

**Example:**

```json theme={null}
{
  "scenario": "seed_product",
  "params": {
    "product_id": "test-product",
    "fixture": {
      "delivery_type": "non_guaranteed",
      "channels": ["display"],
      "pricing_options": [
        { "pricing_option_id": "test-pricing", "pricing_model": "cpm", "currency": "USD", "floor_price": 1.0 }
      ],
      "format_ids": [{ "id": "display_300x250" }]
    }
  }
}
```

### `seed_pricing_option`

既存のシードされた製品に価格オプションを追加（またはアップサート）します。ストーリーボードが最初の `seed_product` 呼び出しに含まれなかった特定の価格オプションを必要とするとき、またはオプションの属性がセラーのデフォルトから分岐する必要があるときに使います。

**Params:**

| Field               | Type   | Required | Description                                                                                                                                                                     |
| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `product_id`        | string | Yes      | 親製品（既に存在していなければならない — 先にシードする）                                                                                                                                                  |
| `pricing_option_id` | string | Yes      | 価格オプションの安定した識別子                                                                                                                                                                 |
| `fixture`           | object | No       | [`PricingOption`](https://adcontextprotocol.org/schemas/v3/core/pricing-option.json) スキーマに従う価格オプション形状（`pricing_model`、`currency`、オークションベースの `floor_price`、固定の `fixed_price` など） |

**Example:**

```json theme={null}
{
  "scenario": "seed_pricing_option",
  "params": {
    "product_id": "test-product",
    "pricing_option_id": "default",
    "fixture": {
      "pricing_model": "cpm",
      "floor_price": 5.0,
      "currency": "USD"
    }
  }
}
```

### `seed_creative`

特定のライフサイクルステータスでクリエイティブフィクスチャを作成します。ガバナンスと配信ストーリーボードが最初に `sync_creatives` をラウンドトリップせずに事前承認されたクリエイティブを参照できるようにします。

**Params:**

| Field         | Type   | Required | Description                                                             |
| ------------- | ------ | -------- | ----------------------------------------------------------------------- |
| `creative_id` | string | Yes      | 安定した識別子                                                                 |
| `fixture`     | object | No       | クリエイティブ形状。典型的なフィールド: `status`、`format_id`、`assets`、`click_through_url`。 |

**Example:**

```json theme={null}
{
  "scenario": "seed_creative",
  "params": {
    "creative_id": "campaign_hero_video",
    "fixture": {
      "status": "approved",
      "format_id": { "id": "video_30s" },
      "assets": [{ "type": "video", "url": "https://example.com/hero.mp4" }]
    }
  }
}
```

### `seed_plan`

メディアプランフィクスチャを作成します。最初に完全なブリーフィング + プロポーザルフローを実行せずに特定のプランに対してアサートするガバナンスストーリーボードで使われます。

**Params:**

| Field     | Type   | Required | Description                                                |
| --------- | ------ | -------- | ---------------------------------------------------------- |
| `plan_id` | string | Yes      | 安定した識別子                                                    |
| `fixture` | object | No       | プラン形状。典型的なフィールド: `budget`、`brand`、`flight`、`line_items[]`。 |

**Example:**

```json theme={null}
{
  "scenario": "seed_plan",
  "params": {
    "plan_id": "gov_acme_q2_2027",
    "fixture": {
      "budget": { "total": 30000, "currency": "USD" },
      "brand": { "domain": "acmeoutdoor.example" },
      "flight": { "start": "2027-04-01", "end": "2027-06-30" }
    }
  }
}
```

### `seed_media_buy`

`create_media_buy` フローをバイパスして、指定されたライフサイクル状態でメディアバイフィクスチャを作成します。既存のバイに対してガバナンスまたは配信動作をアサートする必要があるストーリーボードで使われます。

**Params:**

| Field          | Type   | Required | Description                                                  |
| -------------- | ------ | -------- | ------------------------------------------------------------ |
| `media_buy_id` | string | Yes      | 安定した識別子                                                      |
| `fixture`      | object | No       | メディアバイ形状。典型的なフィールド: `status`、`packages[]`、`budget`、`flight`。 |

**Example:**

```json theme={null}
{
  "scenario": "seed_media_buy",
  "params": {
    "media_buy_id": "mb_acme_q2_2026_auction",
    "fixture": {
      "status": "active",
      "packages": [{ "package_id": "pkg_001", "product_id": "test-product" }]
    }
  }
}
```

### `seed_measurement_catalog`

セラーのコントローラーがこのシナリオをアドバタイズするとき、コンプライアンスセッションのため測定ベンダーの `get_adcp_capabilities.measurement.metrics[]` スナップショットをシードします。メディアバイストーリーボードは、製品レベルのベンダーメトリックケイパビリティを外部ベンダーカタログディスカバリー前提条件から区別するため、このシナリオを伴う明示的な `comply_test_controller` ステップを使います。ストーリーボードは、SDK アダプターセットがまだこのシナリオを採用していないコントローラーの互換性フォールバックとして、このスナップショットを `seed_product.fixture.measurement_catalogs[]` に運ぶこともできます。同じベンダーに両方のソースが存在するとき、明示的な `seed_measurement_catalog` シードが権威的です。

**Params:**

| Field     | Type      | Required | Description                                                                         |
| --------- | --------- | -------- | ----------------------------------------------------------------------------------- |
| `vendor`  | BrandRef  | Yes      | カタログがシードされる測定ベンダー                                                                   |
| `metrics` | object\[] | Yes      | カタログエントリー。各エントリーは `metric_id` を含まなければならず、`measurement.metrics[]` のオプションフィールドを含んでもよい |

**Example:**

```json theme={null}
{
  "scenario": "seed_measurement_catalog",
  "params": {
    "vendor": { "domain": "attentionvendor.example" },
    "metrics": [
      {
        "metric_id": "attention_catalog_baseline",
        "unit": "score",
        "description": "Baseline attention metric present in this vendor catalog."
      }
    ]
  }
}
```

### シードのセマンティクスと順序

* **フィクスチャ形状。** `fixture` は許容的（`additionalProperties: true`）に保たれ、ストーリーボード作成者が各テストが必要とする最小限の形状を宣言できます。フィクスチャは対応するドメインスキーマ（`seed_product` には `core/product.json`、`seed_pricing_option` には `core/pricing-option.json`、`seed_creative` には `media-buy/sync-creatives-request.json` の creative-item 形状、`seed_media_buy` には `core/media-buy.json`、`seed_plan` にはプランスキーマ）に適合すべきです（SHOULD）。`seed_measurement_catalog.metrics[]` は `get_adcp_capabilities.measurement.metrics[]` をミラーします。セラーは明らかに不正な形式のフィクスチャを `INVALID_PARAMS` で拒否してもよい（MAY）。
* **再シード時の冪等性。** 同じ主 ID と最初と等価な `fixture` を持つ 2 つ目の呼び出しは成功し `previous_state: "existing"` で `success: true` を返すべきです（SHOULD）。**分岐する** フィクスチャを持つ 2 つ目の呼び出しは、どのフィールドが分岐したかを説明する `error_detail` を伴う `INVALID_PARAMS` を返さなければなりません（MUST） — セラーは黙ってマージまたは更新してはなりません（MUST NOT）。実行中にフィクスチャ状態を変える必要があるストーリーボードは、再シードではなく `force_*` シナリオを使わなければなりません（MUST）。これは同じストーリーボードをセラー全体で決定的に保ちます。
* **外部キー順序。** ランナーは、セラーが子の前に参照される親を受け取るよう、依存関係順にフィクスチャをシードします。依存関係 DAG:

  ```
  product ──┬─→ pricing_option
            ├─→ plan
            └─→ media_buy
  creative ────→ media_buy
  plan ────────→ media_buy
  ```

  具体的には: `seed_pricing_option` の前に `seed_product`。フィクスチャがそれらを参照するとき `seed_media_buy` の前に `seed_product`、`seed_creative`、`seed_plan` すべて。`fixtures:` ブロックを宣言するストーリーボードは、ランナーがトポロジカルソートできる順序でエントリーをリストしなければならない（MUST） — 存在しない製品の `seed_pricing_option`、または最初にシードされなかったクリエイティブ/製品/プランを参照する `seed_media_buy` を受け取るセラーは、親を自動作成するのではなく `INVALID_PARAMS` を返さなければならない（MUST）。
* **サンドボックススコープ。** シードされたフィクスチャは認証済みサンドボックスアカウントにのみ存在します。`NOT_FOUND` は `force_*` と同じように適用されます — 呼び出し元のアカウントの親製品を見られないセラーは、黙って別のテナントにフォールバックするのではなく `NOT_FOUND` を返さなければなりません（MUST）。
* **ケイパビリティアドバタイズ。** 特定のシードシナリオを実装しないセラーは、そのシナリオ名に対して `UNKNOWN_SCENARIO` を返さなければなりません（MUST）。ランナーは、`prerequisites.controller_seeding` がそのシナリオを要求するストーリーボードの `seed_*` 上の `UNKNOWN_SCENARIO` をカバレッジギャップとして扱います — それらのストーリーボードは failed ではなく `not_applicable` としてグレードされます。これは **馴染みのない** `seed_*` 名にも適用されます: enum は拡張のためオープン（下記参照）なので、ランナーはセラーが決して見たことのないシナリオを発行するかもしれません。セラーとランナーは、認識されないシナリオ値をスキーマ拒否するのではなく `UNKNOWN_SCENARIO` で応答しなければなりません（MUST）。
* **拡張のためオープンな enum。** `scenario` enum は時間とともに新しい値を追加します（専門分野が要求するにつれ新しいシードシナリオが着地）。ランナーとセラーは、認識しないシナリオ文字列を受け入れ、ハードにスキーマ検証を失敗させるのではなく `UNKNOWN_SCENARIO` で応答しなければなりません（MUST） — そうでなければすべての新しい enum 値が古い実装の破壊的変更になります。

## レスポンス形状

### 状態遷移レスポンス（`force_*`）

**Success:**

```json theme={null}
{
  "success": true,
  "previous_state": "processing",
  "current_state": "approved",
  "message": "Creative cr-123 transitioned from processing to approved"
}
```

**Failure (invalid transition):**

```json theme={null}
{
  "success": false,
  "error": "INVALID_TRANSITION",
  "error_detail": "Cannot transition from archived to processing — archived is terminal",
  "current_state": "archived"
}
```

**Failure (unknown entity):**

```json theme={null}
{
  "success": false,
  "error": "NOT_FOUND",
  "error_detail": "Creative cr-unknown not found",
  "current_state": null
}
```

### シミュレーションレスポンス（`simulate_*`）

**`simulate_delivery` response:**

```json theme={null}
{
  "success": true,
  "simulated": {
    "impressions": 10000,
    "clicks": 150,
    "reach": 4000,
    "frequency": 2.5,
    "reach_window": { "kind": "rolling", "period": { "interval": 7, "unit": "days" } },
    "viewability": {
      "measurable_impressions": 9000,
      "viewable_impressions": 7200,
      "viewable_rate": 0.8,
      "viewed_seconds": 4.3,
      "standard": "mrc"
    },
    "reported_spend": { "amount": 150.00, "currency": "USD" }
  },
  "cumulative": {
    "impressions": 25000,
    "clicks": 380,
    "reach": 4000,
    "frequency": 2.5,
    "reach_window": { "kind": "rolling", "period": { "interval": 7, "unit": "days" } },
    "viewability": {
      "measurable_impressions": 9000,
      "viewable_impressions": 7200,
      "viewable_rate": 0.8,
      "viewed_seconds": 4.3,
      "standard": "mrc"
    },
    "reported_spend": { "amount": 375.00, "currency": "USD" }
  },
  "message": "Delivery simulated for mb-789: 10000 impressions, 150 clicks, $150.00 spend"
}
```

`simulated` フィールドはこの呼び出しで注入された値をエコーバックします。`cumulative` フィールドは、このメディアバイの加算カウンターと支出の実行合計、プラス最新の非加算リーチウィンドウとビューアビリティ状態を返し、呼び出し元が `get_media_buy_delivery` をチェックする前に期待される状態を検証できます。

**`simulate_budget_spend` response:**

```json theme={null}
{
  "success": true,
  "simulated": {
    "spend_percentage": 95,
    "computed_spend": { "amount": 950.00, "currency": "USD" },
    "budget": { "amount": 1000.00, "currency": "USD" }
  },
  "message": "Budget for mb-789 set to 95% consumed ($950.00 of $1000.00)"
}
```

### エラーコード

コントローラーは、ストーリーボードランナーが特定の失敗モードをアサートできるよう構造化エラーコードを使わなければなりません（MUST）:

| Error code              | When                                                                                                                                                              |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_TRANSITION`    | 要求されたステートマシン遷移が有効でない（例: `archived → processing`、`canceled → paused`）                                                                                              |
| `INVALID_STATE`         | 操作がリソースの現在ステータスに許可されていない（例: 分岐する形状で既に存在するフィクスチャを再シード）                                                                                                             |
| `NOT_FOUND`             | エンティティが存在しないか呼び出し元がアクセス権を持たない（マルチテナントサンドボックスは「あなたのものでない」を「見つからない」として扱うべき（SHOULD））                                                                                 |
| `UNKNOWN_SCENARIO`      | このセラーが実装しないシナリオ                                                                                                                                                   |
| `INVALID_PARAMS`        | 欠けているまたは不正な形式の params、または前提条件が満たされない（例: 予算未構成のエンティティでの `simulate_budget_spend`）                                                                                   |
| `FORBIDDEN`             | サンドボックス接続から参照された本番アカウント                                                                                                                                           |
| `JCS_NON_FINITE_NUMBER` | Digest モード `query_upstream_traffic` が `NaN`、`+Infinity`、`-Infinity` を含む解析済み JSON 様値ツリーを正準化できない。コントローラーはこれらの値を強制変換してはならず、ランナーは影響を受けた検証を `not_applicable` としてグレードする |
| `INTERNAL_ERROR`        | 一時的なセラー側の失敗（例: サンドボックスデータベース利用不可）。ランナーは失敗として扱う前に一度リトライすべき（SHOULD）。                                                                                                |

<Note>
  **コントローラー固有 enum。** コントローラーレスポンスの `error` フィールドは、[`comply-test-controller-response.json`](https://adcontextprotocol.org/schemas/v3/compliance/comply-test-controller-response.json) で定義されたコントローラー固有の語彙を使い、タスクレベルエラーを統制する正準セラーレスポンス [`error-code.json`](https://adcontextprotocol.org/schemas/v3/enums/error-code.json) enum とは別です。`INVALID_TRANSITION` はコントローラー固有です（ステートマシンプリミティブは、セラーレベルエラーコードが `INVALID_STATE` に折りたたむ遷移対状態の区別を公開する）。コントローラーレスポンスのストーリーボードアサーションは、`check: error_code` ではなく `path: "error"` または直接 `field_value` チェックを使います — 形状非依存の `error_code` チェックは、コントローラー自身のレスポンススキーマではなく、タスクレスポンスエラー（`adcp_error` / ペイロード `errors[]`）用です。
</Note>

### 冪等性

状態遷移シナリオ（`force_*`）は冪等です: 現在状態に一致するステータスを強制すると、`previous_state` が `current_state` に等しい成功を返します。これは、ランナーが一時的失敗後にリトライするときのフレーキーなテストを避けます。

シミュレーションシナリオ（`simulate_*`）は冪等では **ありません** — `simulate_delivery` は既存合計に加算し、`simulate_budget_spend` は現在の支出レベルを置き換えます。

## テスト表面

セラーの状態の記録がどこに存在するかが、ストーリーボードテストループがどう閉じるかを決定します。状態ローカルセラー（典型的には SSP、クリエイティブエージェント）は上の `seed_*` シナリオ経由でセラーの DB に書き込みます。セラーの読み取りハンドラーは同じストアを消費し、seed→read ループが自然に閉じます。アップストリームプロキシセラー（プラットフォームにプロキシする DSP、リテーラーカタログを読むリテールメディアネットワーク、シグナルブローカー）は、読み取りハンドラーがセラーの制御しないシステムに到達するためその方法でループを閉じられません。TypeScript SDK は、まず実アダプター呼び出しを実行し、次にシードされたフィクスチャをレスポンスにマージする `TestControllerBridge` を出荷します。どちらのパスも `AAO Verified (Spec)` が証明するワイヤー形式通過を獲得します。どちらのパスも `(Sandbox)` が証明するものではありません — それはセラーの本番スタックが実世界の副作用なしに `account.sandbox: true` を尊重するかどうかをカバーする別の軸です。

このパターンの両実装のクロスページフレーミング、SDK の `_bridge` 助言マーカー、ランタイムシグナル曖昧性解消テーブルはすべて、適合性仕様 → [Test surfaces and the storyboard loop](/docs/building/verification/conformance#test-surfaces-and-the-storyboard-loop) に存在します。

## コンプライアンステストモード

セラーのツールリストに `comply_test_controller` が存在するかどうかが、コンプライアンステスターがどのモードを使うかを決定します:

### ケイパビリティディスカバリー

セラーはすべてのシナリオをサポートせずにテストコントローラーを実装してもよい。ストーリーボードランナーは、最初のインタラクションとして `scenario: "list_scenarios"` で `comply_test_controller` を呼ぶべきです（SHOULD）。これをサポートするセラーは実装されたシナリオのリストを返します:

```json theme={null}
{
  "success": true,
  "scenarios": [
    "force_creative_status",
    "force_account_status",
    "force_media_buy_status"
  ]
}
```

`list_scenarios` を実装するセラーは、[`comply-test-controller-request.json`](https://adcontextprotocol.org/schemas/v3/compliance/comply-test-controller-request.json) の `scenario` enum にそのまま現れるシナリオ名で応答しなければなりません（MUST）。カスタムセラー固有シナリオ名はコンプライアンスコントラクトの一部ではありません。ストーリーボードランナーは正準 enum 外のシナリオにディスパッチしないため、それらをリストしても目的はありません。`seed_product` をサポートするセラーは文字列 `"seed_product"` で応答しなければなりません（MUST） — `"create_test_product"` や他のバリアントではなく。

`list_scenarios` を実装しないセラーは `UNKNOWN_SCENARIO` を伴うエラーを返すべきです（SHOULD）。これが起こると、ランナーは各シナリオを個別に試み、`UNKNOWN_SCENARIO` レスポンスをカバレッジギャップ（失敗ではない）として扱います。これは、`list_scenarios` をスキップする早期実装者がペナルティを受けないことを意味します — ランナーは試行を通じてサポートされたシナリオを発見します。

### 観測モード（デフォルト）

`comply_test_controller` が利用できないとき:

* ランナーはバイヤー開始フローを実行しレスポンススキーマを検証
* セラーアクションを要求するステートマシン遷移はスキップ
* 助言観測が何をテストできなかったかを記録

### 決定的モード

`comply_test_controller` が利用可能なとき:

* ランナーは各ライフサイクルのすべての到達可能な状態を歩く
* エッジケースを強制: 終端状態、無効な遷移、エラーコード
* 強制された状態変更が後続の読み取りに反映されることを検証
* 操作ゲートをテスト（例: アカウントが `suspended` のとき `create_media_buy` がブロックされる）

ランナーは決定的モードで 3 つの結果カテゴリーを区別します:

* **Scenario not supported** — `list_scenarios` または `UNKNOWN_SCENARIO` エラーで返される。失敗ではなくカバレッジギャップとしてレポート。
* **Transition correctly rejected** — コントローラーが無効な状態変更に `INVALID_TRANSITION` を返した。これは pass。
* **Unexpected failure** — コントローラーが有効であるべき遷移にエラーを返した、または失敗すべき遷移に成功した。これはコンプライアンス失敗。

### 例: 決定的モードでのクリエイティブライフサイクル

```
1. sync_creatives(creative)
2. list_creatives() → verify status = "processing"
3. force_creative_status(creative_id, "pending_review")
4. force_creative_status(creative_id, "approved")
5. list_creatives() → verify status = "approved"
6. force_creative_status(creative_id, "archived")
7. list_creatives() → verify status = "archived"
8. sync_creatives(same creative) → verify unarchive (→ approved or pending_review)
9. force_creative_status(creative_id, "rejected", reason)
10. list_creatives() → verify rejection_reason persisted
11. sync_creatives(same creative) → verify resubmission (rejected → processing)
12. force_creative_status(creative_id, "approved") → expect INVALID_TRANSITION (must go through pending_review)
```

### 例: 決定的モードでのアカウント操作ゲート

```
1. sync_accounts(account) → active
2. force_account_status(account_id, "suspended")
3. create_media_buy() → expect ACCOUNT_SUSPENDED
4. get_media_buys() → expect existing buys still readable
5. force_account_status(account_id, "active")
6. create_media_buy() → expect success
7. force_account_status(account_id, "payment_required")
8. update_media_buy(add packages) → expect ACCOUNT_PAYMENT_REQUIRED
9. get_media_buys() → existing buys still readable
```

### 例: 決定的モードでのメディアバイライフサイクル

```
1. create_media_buy() → status = "pending_creatives"
2. force_media_buy_status(media_buy_id, "rejected", reason) → expect success
3. get_media_buys() → verify status = "rejected", rejection_reason persisted
4. force_media_buy_status(media_buy_id, "active") → expect INVALID_TRANSITION (rejected is terminal)
5. create_media_buy() → new buy, status = "pending_creatives"
6. force_media_buy_status(media_buy_id, "pending_start")
7. force_media_buy_status(media_buy_id, "active")
8. force_media_buy_status(media_buy_id, "rejected") → expect INVALID_TRANSITION (rejected only valid from pending_creatives or pending_start)
```

### 例: 配信と予算の検証

```
1. create_media_buy(budget: $1000)
2. simulate_delivery(impressions: 10000, reported_spend: $500)
3. get_media_buy_delivery() → verify delivery reflects simulated data
   (reported_spend is delivery-only; does not affect account budget)
4. simulate_budget_spend(spend_percentage: 95)
5. get_account_financials() → verify total_spend reflects 95% ($950, not $500 from delivery)
6. simulate_budget_spend(spend_percentage: 100)
7. force_account_status("payment_required")
8. create_media_buy() → expect ACCOUNT_PAYMENT_REQUIRED
```

## 認定階層

| Tier                      | Requirement            | What it proves                                  |
| ------------------------- | ---------------------- | ----------------------------------------------- |
| **Functional compliance** | 観測モードですべてのストーリーボードを通過  | ツールが存在し、正しく応答し、バイヤー開始フローを完了する                   |
| **Stateful compliance**   | 決定的モードですべてのストーリーボードを通過 | ステートマシンが正しい遷移を強制し、エラーコードが仕様に一致し、操作ゲートが正しくブロックする |

**専門分野スコープのシード要件。** Stateful compliance はまた、セラーが認定する専門分野をカバーする `seed_*` シナリオを実装することを要求します。`UNKNOWN_SCENARIO` → `not_applicable` グレーディングは、欠けている表面積の正直なカバレッジレポート用であり、適合性からの一括オプトアウトではありません — `sales-non-guaranteed` を認定するセラーは少なくとも `seed_product` と `seed_pricing_option` を実装しなければならず（MUST）、`creative-ad-server` を認定するセラーは `seed_creative` を実装しなければならず（MUST）、`governance-delivery-monitor` を認定するセラーは `seed_plan`（とストーリーボードが要求する場合 `seed_media_buy`）を実装しなければなりません（MUST）。`static/compliance/source/specialisms/` のストーリーボード作成者はストーリーボードが必要とするフィクスチャを宣言します。セラーはそのリストを認定上の専門分野に一致させます。

## 実装ガイダンス

### セラー向け

1. `comply_test_controller` をデプロイレベルでゲートする — `tools/list`（または A2A `skills[]`）に現れてはならず（MUST NOT）、`compliance_testing` ケイパビリティブロック経由でアドバタイズされてはならず（MUST NOT）、本番デプロイで未知ツールにディスパッチしなければならない（MUST）。完全なルールについては [Sandbox gating](#sandbox-gating) を参照。
2. 本番ステートマシンロジックを再利用する — コントローラーは同じ内部遷移関数を呼ぶべきで、バイパスしない
3. 遷移ルールを強制する — `rejected` が本番で終端なら、`force_media_buy_status(rejected → active)` はコントローラー経由でも失敗しなければならない
4. 変更を即座に反映する — 強制された遷移の後、次の `list_*` または `get_*` 呼び出しは更新された状態を返さなければならない

### コンプライアンステスター向け

1. `tools/list` 経由のプロファイルディスカバリー中にツールを検出
2. `list_scenarios` を呼びどのシナリオがサポートされるかを発見
3. ベースラインとして観測モードを実行 — どこでも動く
4. コントローラーが利用可能なとき決定的シナリオを上に重ねる
5. どのモードが使われたかをレポートしカバレッジギャップを失敗から区別
6. コントローラーの遷移検証自体をテスト — 無効な遷移は黙って成功するのではなく `INVALID_TRANSITION` を返すべき

## 設計決定

1. **セラーは遷移順序を検証する。** コントローラーは本番と同じステートマシンルールを強制する。決して `processing` でなかったクリエイティブに `force_creative_status(approved)` を呼ぶことはエラー — コントローラーは本番と同様にそれを拒否する。ここで参照されるライフサイクルステートマシンはそれぞれのプロトコル仕様で定義される（[クリエイティブライフサイクル](/docs/creative/specification#creative-status-lifecycle)、[アカウントライフサイクル](/docs/accounts/overview#account-status-lifecycle)、[メディアバイライフサイクル](/docs/media-buy/specification)、[SI セッションライフサイクル](/docs/sponsored-intelligence/specification#session-states) を参照）。

2. **テストは自己完結的。** 各テストは既存のものを再利用するのではなく専用エンティティ（メディアバイ、クリエイティブ、アカウント）を作成すべき（SHOULD）。これは加算シミュレーション呼び出し（`simulate_delivery`）がリセットメカニズムを必要とせずに既知のゼロ状態から始まることを保証する。`reset` シナリオは不要。コンプライアンステスターは、複数のストーリーボードランナーインスタンスが同じサンドボックスに対して並行実行するときの衝突を避けるため、テストエンティティに一意の識別子（例: UUID）を使うべき（SHOULD）。サンドボックスエンティティのクリーンアップ（例: TTL ベースの期限切れ）はセラーの責任。

3. **配信シミュレーションは合成マーカーを使う。** `simulate_delivery` レコードは、セラーが内部的に簿記に使える `synthetic: true` フィールドを含んでもよい（MAY）。ランナーはこのマーカーを無視する — にかかわらず同じスキーマに対して `get_media_buy_delivery` レスポンスを検証する。これはテストの正しさに影響せずにセラーの実装ハードルを下げる。

4. **1 ツール、多シナリオ。** 単一ツール設計は、7 つの別々のツールの約 1,400 トークンに対しコンテキストウィンドウコストを約 500 トークンに保つ。セラーは 1 つのサンドボックスゲートを実装する。ランナーは 1 つのツールを検出する。`list_scenarios` イントロスペクションは、ツールごとの存在検出を要求せずに部分実装を処理する。
