> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kanaeru.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# ブループリントの例

> Nadiaの人狼ゲームで使われた全種類のブループリント。実際のエディターのスクリーンショット、読みやすい表示、Kanaが書くソースを紹介します。

ここで紹介するブループリントは、Kanaeruのエピソードに登場する例のプロダクト、**Nadia** のオンライン人狼ゲームのものです。8〜15人の友人がスマホで遊ぶ、チャット専用のゲームで、プライベートルーム、秘密の役職、時間制限付きのラウンドがあります。ブループリントが作られる様子は、Nadiaの [Vision](https://www.kanaeru.ai/ja/blog/2026-08-31-vision-session-from-idea-to-plan)、[Planning](https://www.kanaeru.ai/ja/blog/2026-09-01-planning-session-from-outcome-to-reviewable-work)、[Review](https://www.kanaeru.ai/ja/blog/2026-09-02-review-session-caught-the-bug-before-code) の各セッションで見られます。

例のほとんどは、Nadiaの **ラウンドの自動進行** アウトカムのものです。ゲームが役職を配り、共有のタイマーで夜と昼を進め、票を数えるので、誰も進行役として抜けなくて済みます。スクリーンショットは、エピソードで紹介した **プライベートルーム** アウトカムのものです。

<Frame caption="計画とレビューに表示されたジャーニー。受け入れ条件と、それを支える7つのブループリント。">
  <img src="https://mintcdn.com/kanaerulabs/_FsoVBjQWp9cMeEf/images/nadia/ja/journey-scenarios-blueprints.jpg?fit=max&auto=format&n=_FsoVBjQWp9cMeEf&q=85&s=838fac6b0ea8f5620b13ee936441c3ae" alt="計画とレビューで、ジャーニーの受け入れ条件と、7つのブループリントを含むブループリント行を表示した画面" width="1600" height="900" data-path="images/nadia/ja/journey-scenarios-blueprints.jpg" />
</Frame>

各例には最大3つのタブがあります。**Kanaeruでの表示** は実際のエディター、**表示例** はここで読めるように簡略化した表示、**ソース** はKanaが書いた内容そのものです。どのブループリントも、ソースを [エクスポート](/ja/guides/blueprint-editor) できます。

## インテント仕様

Kanaは最初にインテント仕様を書きます。ほかのすべてのブループリントは、これに沿っている必要があります。

<Tabs>
  <Tab title="表示例">
    **意図:** 夜と昼のラウンドを、全員に見えるタイマーでゲーム自身が進行し、誰もGM役で抜けなくて済むようにする。

    **理由:** いまは誰か1人がゲームから抜けて、役職を配り、夜に起こし、票を数えています。その人は一度も遊べません。このアウトカムのすべては、その役目を交代制にするのではなく消すために存在します。

    | 成功条件 | 検証可能 |
    | - | - |
    | 最初の夜から決着まで、人間の進行役なしに1ゲームを完走できる | はい |
    | フェーズの変化が2秒以内に全プレイヤーの画面へ届く | はい |
    | フェーズが閉じる前に送られた行動は必ず集計される | はい |

    * **絶対に譲れないこと:** プレイヤーが他人の役職を受け取ることはない。時計はサーバーが持ち、クライアントは表示するだけ
    * **許容できるトレードオフ:** フェーズが閉じる時点でオフラインのプレイヤーは、そのフェーズの行動を失う。ラウンド履歴はそのゲーム内のみで保持し、保管はしない

    **エスカレーション**

    * **条件:** 同じゲームで、2人が別のフェーズを見ていると報告された。ラウンドを凍結してオーナーに上げる — 時計が割れると以降の投票がすべて静かに無効になる
    * **条件:** その役職を持たないプレイヤーへの応答に、役職が含まれていた。ゲームを停止して配り直す — 勝てないゲームになっており、誰もそれに気づけない
  </Tab>

  <Tab title="ソース">
    ```json intent-spec.json theme={null}
    {
      "intent": "夜と昼のラウンドを、全員に見えるタイマーでゲーム自身が進行し、誰もGM役で抜けなくて済むようにする。",
      "why": "いまは誰か1人がゲームから抜けて、役職を配り、夜に起こし、票を数えています。その人は一度も遊べません。このアウトカムのすべては、その役目を交代制にするのではなく消すために存在します。",
      "success": [
        {
          "statement": "最初の夜から決着まで、人間の進行役なしに1ゲームを完走できる",
          "verifiable": true
        },
        {
          "statement": "フェーズの変化が2秒以内に全プレイヤーの画面へ届く",
          "verifiable": true,
          "changeType": "modified"
        },
        {
          "statement": "フェーズが閉じる前に送られた行動は必ず集計される",
          "verifiable": true
        }
      ],
      "stakes": {
        "nonNegotiable": [
          "プレイヤーが他人の役職を受け取ることはない",
          "時計はサーバーが持ち、クライアントは表示するだけ"
        ],
        "acceptableTradeoffs": [
          "フェーズが閉じる時点でオフラインのプレイヤーは、そのフェーズの行動を失う",
          "ラウンド履歴はそのゲーム内のみで保持し、保管はしない"
        ]
      },
      "constraints": [
        {
          "rule": "片手で操作できるモバイルブラウザで動くこと",
          "hard": true
        },
        {
          "rule": "アカウントなし。席は参加リンクの使い捨てトークン",
          "hard": true
        }
      ],
      "escalate": [
        {
          "when": "同じゲームで、2人が別のフェーズを見ていると報告された",
          "action": "ラウンドを凍結してオーナーに上げる — 時計が割れると以降の投票がすべて静かに無効になる"
        },
        {
          "when": "その役職を持たないプレイヤーへの応答に、役職が含まれていた",
          "action": "ゲームを停止して配り直す — 勝てないゲームになっており、誰もそれに気づけない"
        }
      ]
    }
    ```
  </Tab>
</Tabs>

## UXモックアップ

絵ではなく、実際のルートを操作できるライブのプロトタイプです。Nadiaのプライベートルームのモックアップには21の画面があります。正常系、失敗、復帰、プライバシー、満員のルームです。変更点にはそれぞれ印が付いているので、その場でレビューできます。

<Tabs>
  <Tab title="Kanaeruでの表示">
    <Frame caption="左に画面の一覧、右にライブのプロトタイプ。Review 38 は、レビューが残っている変更の数です。">
      <img src="https://mintcdn.com/kanaerulabs/_FsoVBjQWp9cMeEf/images/nadia/ja/review-ux-scope.jpg?fit=max&auto=format&n=_FsoVBjQWp9cMeEf&q=85&s=4da82a725ffd9acf8c0367c9ac180d69" alt="21の画面の一覧と、人狼のルームのライブプロトタイプを並べたUXモックアップのエディター" width="1600" height="900" data-path="images/nadia/ja/review-ux-scope.jpg" />
    </Frame>

    <Frame caption="招待の差し替え方法をNadiaが決めると、Kanaがプロトタイプを更新してそれを示します。">
      <img src="https://mintcdn.com/kanaerulabs/_FsoVBjQWp9cMeEf/images/nadia/ja/review-invite-rotation.jpg?fit=max&auto=format&n=_FsoVBjQWp9cMeEf&q=85&s=087c77fd4196c66b5eb7792c8fa59a1b" alt="プロトタイプの画面。新しい招待カードと、維持された3つの席" width="1600" height="900" data-path="images/nadia/ja/review-invite-rotation.jpg" />
    </Frame>
  </Tab>
</Tabs>

## ユーザージャーニー

人がたどる手順をフェーズごとにまとめ、各手順を誰が行うか、どう感じるかを示します。数値が高いほど良い体験です。

<Tabs>
  <Tab title="表示例">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    journey
      title ホストがプライベートルームを開き、招待を共有する
      section 準備
        8〜15の席数を選ぶ: 4: ホスト
        プライベートルームを作る: 4: ホスト
      section 招待
        使い捨ての招待をコピーする: 5: ホスト
        グループチャットで共有する: 4: ホスト
      section 集合
        友人がロビーに参加する: 5: 友人
        漏れた招待を差し替える: 3: ホスト
    ```
  </Tab>
</Tabs>

## シーケンス図

システムの各部分がどの順番でやり取りするかを示します。この例は、夜の行動が締め切りの前後に届く場合です。

<Tabs>
  <Tab title="表示例">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    sequenceDiagram
      autonumber
      actor P as プレイヤー
      participant R as ラウンドサービス
      participant D as Postgres
      participant C as フェーズタイマー
      actor O as 他のプレイヤー
      P->>R: 夜の行動を送信
      alt フェーズが開いている間
        R->>R: 役職がその行動を許すか検証
        R->>D: 行動を保存（フェーズ番号つき）
        R-->>P: 受理
      else 締め切り後
        R-->>P: 拒否。すでに解決済みのフェーズ
      end
      C->>R: 夜のタイマー満了
      R->>D: フェーズを閉じる
      R->>R: この夜の行動をまとめて解決
      R->>O: 結果を全員へ配信
      R->>P: 結果を全員へ配信
      P-->>P: 夜明けの結果を見る
    ```
  </Tab>

  <Tab title="ソース">
    ```text sequence-diagram.mmd theme={null}
    sequenceDiagram
      autonumber
      actor P as プレイヤー
      participant R as ラウンドサービス
      participant D as Postgres
      participant C as フェーズタイマー
      actor O as 他のプレイヤー
      P->>R: 夜の行動を送信
      alt フェーズが開いている間
        R->>R: 役職がその行動を許すか検証
        R->>D: 行動を保存（フェーズ番号つき）
        R-->>P: 受理
      else 締め切り後
        R-->>P: 拒否。すでに解決済みのフェーズ
      end
      C->>R: 夜のタイマー満了
      R->>D: フェーズを閉じる
      R->>R: この夜の行動をまとめて解決
      R->>O: 結果を全員へ配信
      R->>P: 結果を全員へ配信
      P-->>P: 夜明けの結果を見る
    ```
  </Tab>
</Tabs>

## ステートマシン

ゲームが取りうるすべての状態と、状態を次に進めるきっかけを示します。

<Tabs>
  <Tab title="表示例">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    stateDiagram-v2
      [*] --> Lobby
      Lobby --> Dealing: 全席が埋まる
      Dealing --> Night: 役職を配り終える
      Night --> Dawn: 夜のタイマー満了
      Dawn --> Day: 夜の行動を解決
      Day --> Voting: 議論のタイマー満了
      Voting --> Tally: 開票
      Tally --> Day: 同数
      Tally --> Resolution: 追放が確定
      Resolution --> Night: 決着せず
      Resolution --> MafiaWin: 人狼の勝ち
      Resolution --> TownWin: 村人の勝ち
      MafiaWin --> [*]
      TownWin --> [*]
    ```
  </Tab>

  <Tab title="ソース">
    ```text state-machine.mmd theme={null}
    stateDiagram-v2
      [*] --> Lobby
      Lobby --> Dealing: 全席が埋まる
      Dealing --> Night: 役職を配り終える
      Night --> Dawn: 夜のタイマー満了
      Dawn --> Day: 夜の行動を解決
      Day --> Voting: 議論のタイマー満了
      Voting --> Tally: 開票
      Tally --> Day: 同数
      Tally --> Resolution: 追放が確定
      Resolution --> Night: 決着せず
      Resolution --> MafiaWin: 人狼の勝ち
      Resolution --> TownWin: 村人の勝ち
      MafiaWin --> [*]
      TownWin --> [*]
    ```
  </Tab>
</Tabs>

## データモデル

テーブル、カラム、データベースが強制するルールです。KanaはDBMLで書きます。一意インデックスに注目してください。1フェーズにつき1席1行動、1席1票というルールは、アプリのコードではなくデータベースのルールです。

<Tabs>
  <Tab title="表示例">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    erDiagram
      games ||--o{ seats : "持つ"
      seats ||--|| role_assignments : "配られる"
      games ||--o{ night_actions : "記録する"
      games ||--o{ votes : "記録する"
      seats ||--o{ night_actions : "行動する"
      seats ||--o{ votes : "投じる"
      games {
        uuid id PK
        varchar join_code UK
        varchar phase
        int phase_number
        timestamptz phase_ends_at
      }
      seats {
        uuid id PK
        uuid game_id FK
        varchar display_name
        varchar seat_token UK
        boolean alive
      }
      role_assignments {
        uuid seat_id PK
        varchar role
      }
      night_actions {
        uuid id PK
        uuid game_id FK
        int phase_number
        uuid actor_seat_id FK
        uuid target_seat_id FK
      }
      votes {
        uuid game_id FK
        int phase_number
        uuid voter_seat_id FK
        uuid target_seat_id FK
      }
    ```
  </Tab>

  <Tab title="ソース">
    ```text data-model.dbml theme={null}
    Table games {
      id uuid [pk]
      join_code varchar [not null, unique]
      phase varchar [not null]
      phase_number int [not null, default: 0]
      phase_ends_at timestamptz
      note: '唯一の時計がこの phase_ends_at。フェーズの終了をクライアントが判断することはない。'
    }

    Table seats {
      id uuid [pk]
      game_id uuid [not null, ref: > games.id]
      display_name varchar [not null]
      seat_token varchar [not null, unique]
      alive boolean [not null, default: true]

      indexes {
        (game_id, display_name) [unique, name: 'one_seat_per_name_per_game']
      }
      note: '席の証明に使うのは seat_token。名前は公開だが、トークンは公開しない。'
    }

    Table role_assignments {
      seat_id uuid [pk, ref: > seats.id]
      role varchar [not null]
      note: '読み出すのはサーバーのみ。所有する席にしか返さない。'
    }

    Table night_actions {
      id uuid [pk]
      game_id uuid [not null, ref: > games.id]
      phase_number int [not null]
      actor_seat_id uuid [not null, ref: > seats.id]
      target_seat_id uuid [ref: > seats.id]

      indexes {
        (game_id, phase_number, actor_seat_id) [unique, name: 'one_action_per_seat_per_phase']
      }
      note: '送信時のフェーズ番号を刻む。遅れて届いた行動は次の夜に適用されず、拒否される。'
    }

    Table votes {
      game_id uuid [not null, ref: > games.id]
      phase_number int [not null]
      voter_seat_id uuid [not null, ref: > seats.id]
      target_seat_id uuid [not null, ref: > seats.id]

      indexes {
        (game_id, phase_number, voter_seat_id) [unique, name: 'one_vote_per_seat_per_phase']
      }
    }
    ```
  </Tab>
</Tabs>

## ドメインモデル

ビジネス上の概念、そのうちどれが一貫した単位（**集約**）をなすか、そしてそれらが守るルールです。KanaはD2で書き、コア、サポーティング、汎用の3つの階層に分けます。`@change` は、このアウトカムで追加または変更されるものを示します。

<Tabs>
  <Tab title="Kanaeruでの表示">
    <Frame caption="ドメインモデルの変更を1つずつレビューしているところ。Roomは新しく、3つの属性を持ちます。それぞれを Approve または Flag します。">
      <img src="https://mintcdn.com/kanaerulabs/_FsoVBjQWp9cMeEf/images/nadia/ja/review-domain-progressive.jpg?fit=max&auto=format&n=_FsoVBjQWp9cMeEf&q=85&s=237548e86404f41f039a4908cf11e4ac" alt="ADDEDと表示されたSession、Room、Inviteのエンティティと、レビューパネルが表示されたドメインモデルのキャンバス" width="1600" height="900" data-path="images/nadia/ja/review-domain-progressive.jpg" />
    </Frame>
  </Tab>

  <Tab title="ソース">
    ```text domain-model.d2 theme={null}
    # ──── Core Tier ────

    core: {
      round: {
        Round: {
          shape: class
          \# Aggregate Root
          gameId: uuid
          phase: Phase
          phaseNumber: int {constraint: monotonic}
          phaseEndsAt: timestamptz

          open()
          close()
          resolve()
        }

        # @change: added
        NightAction: {
          shape: class
          actorSeatId: uuid {constraint: unique with phaseNumber}
          targetSeatId: uuid
          phaseNumber: int
        }

        Vote: {
          shape: class
          voterSeatId: uuid {constraint: unique with phaseNumber}
          targetSeatId: uuid
          phaseNumber: int
        }

        Round -> NightAction: 保持する
        Round -> Vote: 保持する
      }

      roster: {
        Game: {
          shape: class
          \# Aggregate Root
          id: uuid
          joinCode: string {constraint: unique}
        }

        Seat: {
          shape: class
          id: uuid
          displayName: string
          seatToken: string {constraint: secret}
          alive: boolean
        }

        RoleAssignment: {
          shape: class
          seatId: uuid
          role: Role {constraint: server-only}
        }

        Game -> Seat: 保持する
        Game -> RoleAssignment: 配る
      }
    }

    # ──── Supporting Tier ────

    supporting: {
      presence: {
        Connection: {
          shape: class
          seatId: uuid
          lastSeenAt: timestamptz
        }
      }
    }

    # ──── Generic Tier ────

    generic: {
      delivery: {
        # @change: modified
        PhaseBroadcast: {
          shape: class
          phase: Phase
          sentAt: timestamptz
        }
      }
    }

    # ──── Cross-Context Relationships ────

    core.roster.Seat -> core.round.NightAction: 対象にする
    core.round.Round -> core.roster.Game: 進める
    supporting.presence.Connection -> core.roster.Seat: 着席する
    core.round.Round -> generic.delivery.PhaseBroadcast: 配信する
    ```
  </Tab>
</Tabs>

## アーキテクチャ

コンポーネント、それぞれの責務、コンポーネント間のデータの流れです。KanaはLikeC4で書きます。LikeC4には、コンポーネント、デプロイ、連携、データの各ビューがあります。

<Tabs>
  <Tab title="表示例">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    flowchart TD
      player([プレイヤー])
      subgraph mafia[人狼ゲーム]
        web[ゲーム画面<br/>Next.js、モバイルブラウザ]
        round[ラウンドサービス<br/>フェーズの時計を持つ]
        timer[フェーズタイマー]
        db[(Postgres)]
      end
      player -->|遊ぶ| web
      web -->|行動と投票| round
      round -->|フェーズ番号つきで保存| db
      timer -->|フェーズを終了| round
      round -->|フェーズの変化、役職は含まない| web
    ```
  </Tab>

  <Tab title="ソース">
    ```text architecture.c4 theme={null}
    specification {
      element actor
      element system
      element container
      element database
    }

    model {
      player = actor "プレイヤー"
      mafia = system "人狼ゲーム" {
        web = container "ゲーム画面" "Next.js、モバイルブラウザで動作"
        round = container "ラウンドサービス" "フェーズの時計を持ち、行動を解決する"
        timer = container "フェーズタイマー" "各フェーズを時間どおりに閉じる"
        db = database "Postgres" "ゲーム、席、役職、行動、投票"
      }
      player -> mafia.web "遊ぶ"
      mafia.web -> mafia.round "行動と投票を送る"
      mafia.round -> mafia.db "フェーズ番号つきで保存"
      mafia.timer -> mafia.round "フェーズを終了"
      mafia.round -> mafia.web "フェーズの変化を配信（役職は含まない）"
    }

    views {
      view rounds { include * }
    }
    ```
  </Tab>
</Tabs>

## API契約

アウトカムが追加・変更するエンドポイントを、OpenAPIドキュメントとして示します。ラウンドに必要な最小限の範囲です。存在しないものに注目してください。他人の役職を返すエンドポイントはありません。

<Tabs>
  <Tab title="表示例">
    | メソッド | パス | 内容 | レスポンス |
    | - | - | - | - |
    | `GET` | `/games/{id}/state` | 自分から見た現在の状態 | `200` フェーズ、残り時間、生存者、そして自分の役職のみ |
    | `POST` | `/games/{id}/actions` | 夜の行動を送る | `202` 受理。フェーズ番号を刻んで保存、`403` この役職にその行動は許されていない、`409` そのフェーズはすでに解決済み |
    | `POST` | `/games/{id}/votes` | 投票する | `202` 受理、`409` この席はこのフェーズですでに投票済み |
    | `GET` | `/games/{id}/events` | フェーズ変化のストリーム | `200` サーバー送出のフェーズ変化。役職は含まない |
  </Tab>

  <Tab title="ソース">
    ```json api-contract.json theme={null}
    {
      "openapi": "3.0.3",
      "info": {
        "title": "人狼 ラウンドAPI",
        "version": "0.1.0",
        "description": "1ラウンドを進めるために必要な最小の面。役職を返す経路は存在しない。"
      },
      "paths": {
        "/games/{id}/state": {
          "get": {
            "summary": "自分から見た現在の状態",
            "parameters": [
              {
                "name": "id",
                "in": "path",
                "required": true,
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            ],
            "responses": {
              "200": {
                "description": "フェーズ、残り時間、生存者、そして自分の役職のみ"
              }
            }
          }
        },
        "/games/{id}/actions": {
          "post": {
            "summary": "夜の行動を送る",
            "responses": {
              "202": {
                "description": "受理。フェーズ番号を刻んで保存"
              },
              "403": {
                "description": "この役職にその行動は許されていない"
              },
              "409": {
                "description": "そのフェーズはすでに解決済み"
              }
            }
          }
        },
        "/games/{id}/votes": {
          "post": {
            "summary": "投票する",
            "responses": {
              "202": {
                "description": "受理"
              },
              "409": {
                "description": "この席はこのフェーズですでに投票済み"
              }
            }
          }
        },
        "/games/{id}/events": {
          "get": {
            "summary": "フェーズ変化のストリーム",
            "responses": {
              "200": {
                "description": "サーバー送出のフェーズ変化。役職は含まない"
              }
            }
          }
        }
      }
    }
    ```
  </Tab>
</Tabs>

## 技術スタック

各決定には、選んだもの、選んだ理由、ほかに検討したもの、トレードオフを記します。Kanaeruでは、各カードに技術のロゴが表示されます。

<Tabs>
  <Tab title="表示例">
    <CardGroup cols={2}>
      <Card title="言語: TypeScript" icon="code">
        フェーズとロールの型をサーバーと画面で共有し、片側だけ古くなることを防ぐ。**代替案:** Python、Go。
      </Card>

      <Card title="フレームワーク: Next.js" icon="layer-group">
        参加画面とラウンド画面を同じ土台で出し、サーバー送出イベントもそのまま扱える。**代替案:** Remix、SvelteKit。**トレードオフ:** 同時接続が数百を超えると、専用のソケット層が必要になる。
      </Card>

      <Card title="データベース: PostgreSQL（変更）" icon="database">
        1フェーズ1席1行動という制約が、アプリのコードではなく一意インデックスそのもの。**代替案:** MySQL、DynamoDB。
      </Card>

      <Card title="ホスティング: Vercel" icon="cloud">
        デプロイ単位が小さく、参加リンクの配信が素直。**代替案:** Fly.io、Render。
      </Card>

      <Card title="認証: 席トークン（アカウントなし）（新規）" icon="key">
        名前は通話にいる全員に見えるため、席の証明には使えない。参加時に配る使い捨てトークンで代替する。**代替案:** Auth0、Clerk。
      </Card>

      <Card title="モニタリング: 構造化ログ + エラートラッキング" icon="chart-line">
        ワークスペースの既定を継承。このアウトカムのために選び直していない。
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="ソース">
    ```json tech-stack.json theme={null}
    {
      "schemaVersion": 1,
      "decisions": [
        {
          "category": "language",
          "choice": "TypeScript",
          "icon": "typescript",
          "rationale": "フェーズとロールの型をサーバーと画面で共有し、片側だけ古くなることを防ぐ。",
          "alternatives": [
            "Python",
            "Go"
          ]
        },
        {
          "category": "framework",
          "choice": "Next.js",
          "icon": "nextdotjs",
          "rationale": "参加画面とラウンド画面を同じ土台で出し、サーバー送出イベントもそのまま扱える。",
          "alternatives": [
            "Remix",
            "SvelteKit"
          ],
          "tradeoffs": "同時接続が数百を超えると、専用のソケット層が必要になる。"
        },
        {
          "category": "database",
          "choice": "PostgreSQL",
          "icon": "postgresql",
          "rationale": "1フェーズ1席1行動という制約が、アプリのコードではなく一意インデックスそのもの。",
          "alternatives": [
            "MySQL",
            "DynamoDB"
          ],
          "changeType": "modified"
        },
        {
          "category": "hosting",
          "choice": "Vercel",
          "icon": "vercel",
          "rationale": "デプロイ単位が小さく、参加リンクの配信が素直。",
          "alternatives": [
            "Fly.io",
            "Render"
          ]
        },
        {
          "category": "auth",
          "choice": "席トークン（アカウントなし）",
          "rationale": "名前は通話にいる全員に見えるため、席の証明には使えない。参加時に配る使い捨てトークンで代替する。",
          "alternatives": [
            "Auth0",
            "Clerk"
          ],
          "changeType": "added"
        },
        {
          "category": "monitoring",
          "choice": "構造化ログ + エラートラッキング",
          "rationale": "ワークスペースの既定を継承。このアウトカムのために選び直していない。"
        }
      ]
    }
    ```
  </Tab>
</Tabs>

## 品質プロファイル

作業が満たすべき品質特性について、プロダクトの現在の状態と目標を示します。Kanaeruではレーダーチャートで表示します。

<Tabs>
  <Tab title="表示例">
    | ディメンション | 現在 | 目標 | 意味 |
    | - | - | - | - |
    | 役職の秘匿 | 100 | 100 | 自分のセッションが他人の役職を受け取ることはない。交渉の余地がない。 |
    | フェーズの一致 | 74 | 95 | 全プレイヤーが同じ瞬間に同じフェーズを見ていること。 |
    | 行動の取りこぼしのなさ | 91 | 99 | 締め切り前に送られた行動が、必ず集計されること。 |
    | 復帰の正しさ | 未計測 | 80 | 切断から戻ったプレイヤーが、正しいフェーズと自分の役職に戻ること。未計測 — 別アウトカム。 |
    | モバイルでの操作性 | 83 | 90 | ルームでチャットしながら片手で操作できること。 |
  </Tab>

  <Tab title="ソース">
    ```json quality-profile.json theme={null}
    {
      "dimensions": [
        {
          "name": "役職の秘匿",
          "target": 100,
          "current": 100,
          "weight": 1,
          "description": "自分のセッションが他人の役職を受け取ることはない。交渉の余地がない。",
          "slaTarget": "漏洩 0件",
          "linkedRequirements": [
            "J2-S1",
            "J2-S2"
          ]
        },
        {
          "name": "フェーズの一致",
          "target": 95,
          "current": 74,
          "weight": 1,
          "description": "全プレイヤーが同じ瞬間に同じフェーズを見ていること。",
          "slaTarget": "ずれ 2秒以内",
          "changeType": "modified"
        },
        {
          "name": "行動の取りこぼしのなさ",
          "target": 99,
          "current": 91,
          "weight": 0.9,
          "description": "締め切り前に送られた行動が、必ず集計されること。",
          "slaTarget": "取りこぼし 0件"
        },
        {
          "name": "復帰の正しさ",
          "target": 80,
          "weight": 0.6,
          "description": "切断から戻ったプレイヤーが、正しいフェーズと自分の役職に戻ること。未計測 — 別アウトカム。",
          "changeType": "added"
        },
        {
          "name": "モバイルでの操作性",
          "target": 90,
          "current": 83,
          "weight": 0.8,
          "description": "ルームでチャットしながら片手で操作できること。",
          "slaTarget": "主要操作は1タップ"
        }
      ]
    }
    ```
  </Tab>
</Tabs>


## Related topics

- [ブループリントエディターとツール](/ja/guides/blueprint-editor.md)
- [ブループリントと理解のギャップ](/ja/concepts/blueprints.md)
- [Kanaeruのしくみ](/ja/concepts/how-kanaeru-works.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.