アーキテクチャ提案
アプリの性質から出発して、それに合う様式(層の切り方)とディレクトリ構造を提案書として書く。 流行や好みから様式を選ぶのではなく、「このアプリはこういう性質だから、この形が合う」という 筋道を残すことが目的。提案書は読んだ人がそのまま移行に着手できる粒度まで落とす。
現状の様式が正しいことも多い。 その場合の提案は「様式は変えず、中の割り方や守り方を直す」に なる。「新しい様式を選ぶ」ことがこのスキルの成果ではない。
出すもの / 出さないもの
出す: 提案書1つ(references/proposal-template.md の形)。性質の分析、候補の比較と選んだ理由、
目標のディレクトリ構造、許す依存の辺、現状との差分、移行の段階、採らなかった案、未決事項。
出さない: コードの移動・リネーム・書き換え。提案は読んだ人(ユーザー)が採否を決めるものであり、 提案書を書いた同じターンで適用まで進めると、判断の機会を奪う。適用はユーザーの指示で別に行う。
手順
1. 性質を読む
様式を決めるのは性質であって規模ではない。だから最初にコードとドキュメントから次の軸を読み取り、 根拠となるファイルや行を添えてメモする(根拠が無い性質は、後の手順で「聞く」対象になる)。
| 軸 | 見るもの | 何が決まるか |
|---|---|---|
| 何を中心に回るか | エントリポイント、メインループ、リクエストの入口。入力から出力までの流れを1本、手で追う | 要求応答型か、イベント流入型か、常駐プロセスか、バッチか、ライブラリか。中心の形が層の切り方をほぼ決める |
| 外の世界との境界 | 子プロセス、ネットワーク、ファイル、環境変数、ホスト(端末・ブラウザ・IDE)への依存 | 境界の数と種類。多いほど「差し替え口を1箇所に集める」形(ポート/アダプタ)の価値が上がる |
| 状態の持ち方 | メモリ上のセッション、永続化、復元の要否 | 状態を持つ層をどこに閉じ込めるか |
| 実行環境の数 | サーバとブラウザ、CLI とデーモン、同一言語で複数の実行環境か | 「共有する契約」を独立した層にするかどうか |
| 変わりやすい場所 | git の変更頻度(下のコマンド)、TODO、ドキュメントの「未決事項」、残っているタスク | 変更が集中する場所を、他から切り離して薄い依存にする |
| 守られている制約 | 依存の向きを落とすテスト、lint の境界ルール、設計書の「採らなかった案」、CONTEXT.md / ADR |
すでに決まっている辺を壊さない。既存の判断を尊重したうえで直す |
| 利用者と開発体制 | README、CLAUDE.md、対象ユーザーの記述、サブエージェントに委譲する運用か | 個人開発なら儀式(層の数・抽象の数)を減らす方向に倒す。委譲する体制ではディレクトリ名が指示書の代わりになる |
| テストできる範囲 | テストの配置、目視で確かめている領域 | 自動で守れる境界と目視でしか守れない境界を分けて設計する |
読む順番は、ドキュメント(README・CLAUDE.md・CONTEXT.md・ADR・docs/ の要件と設計書)→
マニフェスト(依存とスクリプト)→ エントリポイント → ディレクトリ木 → 依存の辺 → 変更頻度。
読む量が多いリポジトリでは、この手順だけを Explore 系のサブエージェントに任せて、表と根拠だけを
受け取ってよい(本体のコンテキストを読み込みで埋めないため)。
依存の辺は感覚で語らず数える。 同梱の scripts/import_edges.py が、ディレクトリ同士の import の
本数・外部パッケージへの依存・循環・ディレクトリごとのファイル数を出す(JS/TS 向け。他の言語は
正規表現を直して使う):
python3 ${CLAUDE_SKILL_DIR}/scripts/import_edges.py src --markdown # 第1階層で集計(行数つき)
python3 ${CLAUDE_SKILL_DIR}/scripts/import_edges.py src --depth 2 --markdown # 領域の中まで
python3 ${CLAUDE_SKILL_DIR}/scripts/import_edges.py src --reach protocol # protocol の各ファイルを誰が引くか
--reach は「共有する契約」の層に効く。両側から到達されないファイルは、その層に居る理由が弱い。
変更頻度は git から数える。上位に来たファイルが「変わりやすい場所」の根拠になる:
git log --since=<いまの構造になった日> --format= --name-only -- src | sort | uniq -c | sort -rn | head -20
--since を付けないと、移行で消えたファイルが上位を占めて役に立たない。
数字と並べて、いま痛んでいる兆候も拾う。性質の軸だけだと「何が合っていないか」が抜けやすい。
- 1つのことを理解するのに、小さなファイルを何個も跳ね回る
- 「ここだけが外に触る」という約束がコメントにしかなく、テストが落とさない
- 境界を差し替えられないので、テストが本物のプロセスやネットワークを要る
- 切り出された関数が、呼び出し元の複雑さを隠しているだけで減らしていない
- 同じ定数や経路名が2箇所以上に書かれている
- 新しいものを置く場所が、規約を読まないと決められない
- 正典どうしで層の定義が食い違う、または同じ種類のファイルが層を行き来している(置き場所の基準が 1つでない証拠。これがいちばん効く発見になりやすい)
性質の読み取りが甘いと、後の候補がすべて空回りする。手順1に全体の半分の時間をかけてよい。
2. 足りないところだけ聞く
読んで分からなかった性質のうち、答えによって提案が材料から変わるものだけを聞く。 「どの様式が好きか」は聞かない(それを決めるのがこのスキルの仕事)。 聞くべき例: 今後の実行環境が増える予定があるか、状態の永続化が視野に入っているか、 一番手を入れたい場所はどこか、守りたい既存の判断は何か。 逆に、コードから読み取れることを確認のために聞くのは、ユーザーの時間を奪うだけなのでしない。
無人で回している(質問に答える人がいない)ときは、聞かずに 仮定として提案書の「未決事項」に書き、 仮定が外れたときに変わる箇所を明示して進める。
既存のルールの解釈が割れるとき(設計書の基準が2通りに読める、など)は、提案の本文では 自分の読み方を1つ選んで進め、「未決事項」に「別の読み方を採ると何が変わるか」を書く。 両方を本文に並べると、提案が決まらない。
3. 候補を2つ以上立てて選ぶ
references/style-catalog.md から、性質に合う様式を 2つ以上 選んで並べる。1つしか出さないと、
その1つが正しいかを誰も検証できない。候補ごとに次を書く。
- その様式がこの性質のどこに効くか(一般論ではなく、手順1の軸に対応づけて)
- 合わない点と、それをどう補うか
- その様式が要求するディレクトリの形(ざっくり)
- 確度:
固い(性質の表から直接出る)/試す価値あり(合いそうだが根拠が一部推測)/推測(聞けなかった性質に依存する)。読む側が「どこまで信じてよいか」を見分けるため
現状の様式が性質に合っているなら、それを土台として固定し、候補は「土台の中の割り方」で 立てる(例: 「共有契約 + クライアント/サーバ分割」は動かさず、サーバ側の中を「検査だけ足す」 「核と境界に割る」「概念で縦切り」で比べる)。「現状維持 + 検査だけ」はたいてい有力な候補で、 選ばれなくても移行の第1段階になる。
そのうえで1つ選び、選ばなかった候補を捨てた理由を残す。将来「なぜこの形なのか」を問われたとき、
採らなかった案が書いてあると答えになる。候補を並べる際の考え方は
codebase-design スキルの DESIGN-IT-TWICE.md と同じ。
規模が小さいアプリに多層の様式を当てると、層をまたぐ配線だけが増えて振る舞いは増えない。 層の数は、境界の数と実行環境の数から逆算する。逆算の根拠が書けていれば層が増えてもよい。 根拠なしに4層以上になっていたら疑う。
4. ディレクトリ構造を書く
目標の木を、ディレクトリごとに 「何が住むか」と「何を置いてはいけないか」を1行ずつ 添えて書く。 置いてはいけないものを書かないと、境界が半年で溶ける。
- 名前は概念にする。
helpers/utils/common/miscのような「置き場所」を名前にしたディレクトリは 作らない(そこに何でも入るようになり、依存の向きが読めなくなる) - 単数か複数か、階層の深さは、そのリポジトリの既存の慣習に合わせる。慣習が無ければ提案書で決めて理由を書く
- 用語集(glossary /
CONTEXT.md)があるリポジトリでは、ディレクトリ名もそこの識別子に合わせる。 新しい言葉が要るなら、用語集に足す項目の文面案(1〜2行)を提案書に書く。「足す」とだけ書くと、 採用時に言葉の定義をもう一度考えることになる - 許す依存の辺を明示する(A → B は可、B → A は不可)。すでに依存を落とすテストがあれば、 その一覧をどう変えるかまで書く。無ければ、そのテストを足す段階を移行に入れる
- テストの置き場所(同居か
test/か、境界のテストはどこか)も木に含める - 言葉より図が速く伝わるとき(辺が5本以上、向きが変わる)は、現状と目標の依存図を並べて添える。 図は任意で、表があれば省いてよい
5. 差分と移行の段階を書く
現状の木と目標の木を並べ、どこが動くかをファイル単位で書く。そのうえで移行を段階に切る。
- 各段階は 単独でチェック(型検査・lint・テスト)が通るようにする。途中で壊れた状態を長く持たない
- 最初の段階は「境界を守るテストを足す」か「1つの層を切り出す」のどちらか小さいほうにする。 ファイルを動かさない段階が先にあると、そこで止めても価値が残る
- 動かさないものを明記する(すべてを動かす提案は、たいてい性質を読めていない)
- 段階ごとに「終わったと分かる証拠」を1行書く
- 段階にも確度を付ける。後ろの段階ほど「推測」になりやすく、それ自体が正直な情報になる。 1つの候補を複数の段階に割ったときは、候補の確度を引き継がず段階ごとに付け直す
6. 提案書を置く
提案書は docs/architecture-proposal.md に置く。ユーザーが場所を指定したらそれに従う。
採用後の正典は docs/architecture.md で、提案書とは別ファイル。 このスキルは
docs/architecture.md を書かない。提案が採用されたとき正典へ反映するのは人の作業であり、
既存の設計書(正典)を直接書き換えない。 2回目以降の提案も docs/architecture-proposal.md を
差し替えるだけで、正典は動かさない。
docs/ にファイルを足したら、索引 docs/README.md に1行足す(パスと一行説明)。
docs/README.md が無ければそのとき作る。空の索引を先回りして作らない。
- `docs/architecture-proposal.md` — 様式の候補比較と移行の段階(提案。採否は未定)
書き終えたら、提案書の冒頭に 結論を3行(性質の要約・選んだ様式・最初の段階)で置き直す。 本文を読む前に採否を判断できるようにするため。
提案書を渡すときに、次の一手を1行添える: 選んだ候補を問い詰めたいなら grilling、
採用して段階を進めるなら移行の第1段階から。
やりがちな失敗
- 性質の分析を飛ばして様式から入る: 「クリーンアーキテクチャにしましょう」から始まる提案は、 どの性質に効くのかが書けない。手順1に戻る
- 既存の判断を無視する: 設計書に「採らなかった案」があるのに同じ案を提案する。読んでいない証拠になる。 同じ案を出すなら「当時と何が変わったか」を書く
- 抽象を先に足す: 実装が1つしかないものにインターフェースを切る提案。差し替えの予定が性質から 読めるときだけ切る。ポートとアダプタの様式でも、実装が1つの境界は「置き場所」だけで表してよい
- ディレクトリを増やして解決した気になる: ファイルが2つしかない層。層は境界の数から逆算する
- 移行を1段階で書く: 「全部動かす」は実行されない。小さく切る
- 提案と同時に適用する: ユーザーの採否の機会を奪う。このスキルは書くところまで
他のスキルとの分担
| やること | スキル |
|---|---|
| アプリ全体の様式と木を決める(このスキル) | architecture-proposal |
| 決めた層の中で、モジュールの深さ・シームの置き場所を設計する | codebase-design |
| ディレクトリ名や層の名前を用語集と揃える、ADR に残す | domain-modeling |
| 提案書の選んだ候補を問い詰めて固める | grilling |
| 提案を実装したあと、規約と仕様に沿っているか見る | code-review |
提案書の中で個々のモジュールの内部設計に踏み込みそうになったら、そこは codebase-design の領分なので
「この層の中は別途設計する」と書いて止める。