じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

おい、要件を動くものにしろ

はじめに

ドキュメントは、書いた瞬間から腐っていく。

読まれない。検証されない。更新されない。それでも、要件と呼ばれ続けている。

第1部では、要件を言葉にする規律について書きました。引き出し、観察し、ジョブを見極めながら、業務領域の言葉で書く。6つの特性を満たし、検証可能な形にする。揺らぎと固執を組み込んで、自分たちの色を要件に乗せる。

そこまでやって、初めて要件文が立ち上がります。ただ、立ち上がっただけでは足りないのだと、最近は思うようになりました。

第1部で扱ったのは「書かれずに死ぬ要件」でした。第2部で扱うのは、もう一段先の死、書かれたのに読まれずに死ぬ要件です。気づいたときには、ドキュメントと現場のあいだに、もう橋が架かっていない。

syu-m-5151.hatenablog.com

要件は、ドキュメントで生きるのではなく、動くものとして生きる必要があるのではないか——というのが、私のいまの現在地です。ドキュメントは補助で、動くものが本体。コード、型、テスト、スキーマ、自動レビュー、CI、PRテンプレート。これらに分散して焼き込まれた要件だけが、毎日読まれ、毎日検証され、毎日少しずつ良くなっていく気がしています。

第2部は、言葉になった要件を実装の現場でどう動くものに変えるか、の話です。仕様駆動開発という最近の動きの整理から始めます。AIが書いたコードを信頼できない構造的な理由、要件をシステム全体に分散させて埋め込む方法、そしてレビューの認知負荷をアーキテクチャで減らす設計について、順に書いていきます。

このブログが良ければ読者になったりnwiizoXGithubをフォローしてくれると嬉しいです。

仕様駆動開発の「仕様」とは結局何なのか

最近の文脈をもう少し丁寧に整理しておきたいです。「仕様駆動開発」という言葉が、急速に広まりすぎて、人によって指すものがズレ始めているからです。

最近、AIエージェントにコードを書かせる開発スタイルが急速に広まりました。最初に流行したのは、曖昧な指示から雰囲気でコードを生成させる、いわゆるVibe Codingです。プロンプトを投げる、コードが出る、動かしてみる、気に入らなければ書き直す。私自身、最初の数週間はこれで作業速度が跳ね上がった気がしていました。気がしていただけで、機能を数個足したあたりで全体が崩れ始めた、というのが正直なところです。Vibe Codingは「動くもの」は作れるが、「育つもの」は作れない、というのが業界の総括だったと思います。

その反省から、要件を整理しタスクに分解してからAIに渡すアプローチが定着しました。曖昧なまま投げるのをやめ、上流で意図を固めてから下流に渡す。これが現在の主流になっています。仕様駆動開発(Spec-driven Development)は、その代表的な形です。

ただ、用語が広まる速度に対して、用語の中身の整理が追いついていません。「仕様駆動」という言葉が指す範囲は広く、人によって思い描いている前提が違う。仕様を実装のインプットとして使うだけなのか、それとも仕様そのものを本質として扱うのか。仕様書は実装後に捨てるのか、永続化するのか、あるいは仕様こそが正でコードは派生物なのか。立場によって、議論の前提が大きく変わります。

そして、仕様駆動開発と並んで議論されるのがPlanモードです。Vibe Codingを乗り越えるための仕組みという意味では同じですが、性格が違うものだと考えています。Planモードは、実装したがるAIエージェントへ「まず考えさせる」ためのブレーキです。実装に入る前、必要な情報を収集させ、計画を立てさせ、ユーザーに見せる。承認を得てから実装に進む。多くのCoding Agentに搭載されている、いわば承認ゲートの仕組みです。

仕様駆動開発もブレーキ機能を持っています。実装の前に仕様を書かせ、レビューさせる。Vibe Codingの欠点を補うという意味では、Planモードと同じ方向を向いている。だから両者は混同されやすい。

ただ、仕様駆動開発のほうは論点が多い。私が現場で議論するとき、繰り返し出てくる疑問が3つあります。

疑問① 「仕様(Spec)」とは何なのか

最初の疑問はシンプルだが、意外と答えが揃いません。「仕様」という言葉に、人それぞれ違う絵が浮かんでいます。

最近の仕様駆動開発の文脈では、仕様はバージョン管理可能で、人間が読める、一種のスーパープロンプトとして定義されます。あるツールは仕様を「実装前にCoding Agentが必要とする入力情報」と位置づけ、別のツールは「静的な文書ではなく、プロジェクトと共に進化する生きた実行可能な成果物」と位置づける。表現は違うが、共通しているのは、従来の仕様書とは別物だ、ということです。

従来の仕様書は、人間のために書かれていました。設計者が書き、エンジニアが読み、テスターが照合する。書いた瞬間から劣化が始まり、ほどなく無効になる。これが普通の仕様書の運命でした。

新しい意味での仕様書は、AIエージェントのための入力です。書く目的が変わった。読み手が機械になった瞬間、書かれ方も変わる必要が出てきます。曖昧な表現は機械にとって毒です。一方で、人間にしか判断できない文脈は、機械が混乱しないよう構造化して書く必要がある。

私はこの違いを、自分の中で「仕様の宛先が変わった」と書いてきました。宛先が人間だった頃は、行間で伝えられた。宛先が機械になった瞬間、行間は読まれない。書かれていることが全てになる。第1部で出した「在庫がある」の2文字を書き忘れる構造は、まさに宛先が変わったことに気づいていないときの事故パターンです。

ここで、自分の中で長く曖昧だった用語を整理しておきます。「要件」と「仕様」と「契約」は、何が違うのか。

要件は、業務として何を達成したいかを書いたものです。「在庫がある商品だけを検索できる」。問題領域の言葉で書く。誰がなぜ何を求めているか、を表現します。

仕様は、要件を実現するためにシステムが何をするかを書いたものです。「検索APIは、stock_count > 0 のフィルタを適用し、関連度順に最大20件を返す」。要件と実装の中間にある翻訳層です。要件の言葉を、エンジニアが実装に変換できる粒度まで落としたもの。

契約は、仕様を呼び出す側と呼び出される側で守るべき約束として書いたものです。「このAPIは200を返すならbody.itemsは存在し、各itemはid/name/priceを持つ。500を返すならbody.errorに理由が含まれる」。これは型シグネチャ、APIスキーマ、関数の事前/事後条件として書ける。

3つは別物です。仕様駆動開発を語る人の多くは、この3つを混ぜて使っています。混ぜると議論が壊れる。「仕様を書けばAIが実装する」と言うとき、その「仕様」が要件・仕様・契約のどれを指すかで、必要な精度や書き手が全く違うからです。

疑問② 作成された仕様書をどう扱うか

2つ目の疑問は、仕様書の寿命の話です。書いた仕様書は、実装後にどう扱われるのか。これも、立場で大きく分かれます。

私の中では、仕様書は3種類に分かれます。実装に入る前に捨てる紙実装と並走して更新する地図実装そのものを生成する金型。業界の語彙では Spec-first / Spec-anchored / Spec-as-source と呼ばれていますが、議論を始める前に、この3つを別物として扱うかどうかが土台になります。

踏み台としての仕様は、書いた瞬間に役目を終える紙です。Coding Agentに前提を渡すための事前準備で、マージと同時に消えても困らない。実装後に読まれない仕様文書が残っているのは、たいていこの踏み台が捨てられずに置きっぱなしになっているだけです。

地図としての仕様は、実装と並走して書き直されます。地図と現地が食い違ったら、両方が間違っていると疑う。仕様と実装のどちらかを正にするのではなく、互いに照らし合いながら更新する。多くの現場で「仕様駆動開発」と呼ばれているのは、この運用に近いと思っています。

金型としての仕様は、コードがそれに従属します。仕様を直すとコードが直る、という前提で動かす。型・テスト・スキーマで書ける部分はこちらに寄せられる。一方で、自然言語の仕様をそのまま金型として使うのは、現状のAIの精度ではまだ無理がある、と私は感じています。

ここに大きな分岐があります。同じ「仕様駆動開発」と言っても、踏み台と金型では運用負荷も得られる効用も、まったく違う。SNSで仕様駆動の論争が噛み合わないのは、議論の土台がここで分かれているからだと思います。

私自身は、金型に片足を突っ込みつつ、現実は地図として運用する、というハイブリッドに落ち着いています。型・テスト・スキーマとして書ける部分は金型に寄せ、業務の文脈やトレードオフは地図として運用する。3つを排他的な選択肢ではなく、要件の性質ごとに重ねるレイヤーとして扱う、というのが私の現在地です。

疑問③ ウォーターフォールへの先祖返りなのか

3つ目の疑問は、もう少し感情的な反発が混じったものです。「仕様駆動開発は、結局ウォーターフォールの復権ではないか」。

仕様書を信頼できる唯一の情報源(Single Source Of Truth)として扱う流派の話を聞くと、確かに、開発の中心がソースコードからドキュメントへ戻っているように感じます。アジャイルが「動くソフトウェアこそが価値」と言ってきた歴史を踏まえると、仕様書中心の世界観には違和感がある。

私はこの違和感を、半分は正当で、半分は誤解だと思っています。

正当な部分は、仕様書を聖典化することの危険です。仕様が更新されない、仕様の変更が重い儀式になる、仕様がチーム内の権力構造を反映する。これらが揃うと、ウォーターフォールの悪い側面が再現されます。仕様駆動を導入したつもりが、実態は計画駆動の硬直したプロセスになっている、というのは十分にあり得る失敗です。

誤解の部分は、仕様駆動の仕様は、ウォーターフォールの仕様書とは性格が違うということです。ウォーターフォールの仕様書は、フェーズの成果物でした。書いて、レビューして、承認されたら次の工程に進む。書き直しは別工程の手戻りで、コストが高い。

仕様駆動の仕様は、AIエージェントを動かすための制御手段です。実装と並走し、実装と一緒に書き直される。生きたドキュメントとしてAIエージェントへ最新の仕様を渡せることが、柔軟性を支えています。書き直しは手戻りでなく、通常運転の一部です。

各フェーズの厳格な順序性、前工程への戻りの難しさ、これはウォーターフォール特有の特性であって、仕様駆動が必然的に持つものではない。両者を混同すると、議論が空回りします。

新しいPlanモードへの合流

ここまで読んで、「仕様駆動開発という手段を取らずとも、Planモードで同じことができているのでは」と感じる人もいると思います。これは正しい直感です。

Planモードはもともと「AIエージェントの暴走を防ぐ」ためのブレーキでした。最近のPlanモードは、仕様ファイルの作成やタスクリストへの分解までやるようになっています。Spec-firstのワークフローを取り込んで進化した結果、仕様駆動開発との境界が曖昧になっているのが現状です。

新しいPlanモードと仕様駆動開発の違いは、ほぼ一点に絞られます。仕様書を実装後にどう扱うか、つまり前述のSpec-first / Spec-anchored / Spec-as-sourceのどれを取るか、です。Planモードは、基本的にSpec-firstとして振る舞う。Coding Agentが計画を立て、実装し、計画ファイルは実装と共に消える。それで困らないなら、Planモードで十分だと思います。

これから新しく仕様駆動開発を採用するなら、Spec-firstでは差別化できません。Spec-anchoredかSpec-as-sourceの方向に踏み込まない限り、Planモードと変わらない。仕様駆動の真価は、仕様書の永続化と回転にある、というのが私の見立てです。

仕様はあればあるほど良いのか

もう1つ、現場でよく聞かれる疑問があります。「仕様を詳細に書けば書くほど、AIの成果物の品質は上がるのか」。

直感では「上がる」と答えたくなります。指示が詳細なら、ブレが減るはずだ、と。実際にはそう単純ではないと考えています。

AIエージェントに渡すトークン数が膨大になると、性能が劣化すると言われています。Context Rot(コンテキストの腐敗)と呼ばれる現象で、長すぎるコンテキストは精度を下げる。さらにLost in the Middleという現象もあって、先頭や末尾に比べ、中間に配置された情報の認識精度が大幅に下がる。プロンプトが過密になると、AIは本来重要な情報を見逃します。

仕様を詳細に書こうとして、何千行もの仕様書を1つのコンテキストに詰め込むと、AIはその中で迷子になる。詳細さが裏目に出るパターンです。

ここで効くのが、繰り返しになりますが、決定論的なゲートとの組み合わせです。仕様の文章で全てを書き尽くそうとせず、テスト・lint・型・CIパイプラインなどの自動化された検証に任せる部分を増やす。仕様書には、人間にしか判断できない部分、AIに伝える必要がある業務文脈、を厚く書く。残りは決定論で守らせる。

満たすべき要件をプロパティベーステストとして書くのも有効です。「どんな入力でも、検索結果には在庫切れ商品が含まれない」をテストとして書けば、仕様書に長文で書くより厳密で、AIが書いたコードを自動で弾けます。

仕様の量と質は、トレードオフです。書きすぎると埋もれる。書かなさすぎると穴が開く。ちょうど良い情報量を、コンテキスト全体で設計する。これがコンテキストエンジニアリングの本質だと思っています。

仕様駆動はどこへ向かうのか

私の考えをまとめます。

「仕様駆動開発」という用語は、まだ明確に定義されていません。採用する粒度やスタイルを、チームで認識を合わせる必要があります。Spec-firstのスタイルはPlanモードに合流しつつあり、近いうちにCoding Agentの標準機能の一部になるはずです。

新たに仕様駆動を採用するなら、Spec-anchoredかSpec-as-sourceへ踏み込むことになる。ただ、完全な仕様を作り上げ、維持し続けることは想像以上に難しい。仕様の保守が新しい技術的負債になる可能性もある。

もう1つの方向は、仕様を厚くするのではなく、検証を厚くするという考え方です。仕様は最低限に保ち、テスト・型・契約・自動レビューでガードする。AIが書いたものを仕様に照合するのではなく、AIが書いたものを検証ハーネスにかける。私はこちらに賭けています。

賭けている、と書いた以上、何が観測できたら賭けを降りるかも書いておきます。検証を厚くした結果、PRの差し戻し率や本番障害率が、仕様を厚くしたチームと有意に変わらなかったら、私はこの主張を撤回します。

あるいは、ハーネスの維持コストが書く時間を上回り、複利資産でなく減価償却資産になってしまうなら、戦略の組み替えが必要です。いまのところ、自分のプロジェクトと観測している現場の範囲で、ハーネス側へ倒した方が事故は少ない。だからこちら側へ賭けている、というだけの話です。

そして、私にとって仕様駆動開発は、要件をシステム全体に焼き込む話の一形態に整理されます。仕様という言葉が独立した運動を作っているのではなく、要件をシステムの中でどう表現するかという問題に還元される。仕様という箱を作るのではなく、型・テスト・契約・スキーマ・状態機械という具体的な道具で、要件を実装に翻訳する。それが私の現在地です。

ここで誤解されないように書いておきたいことがあります。「動くものに散らす」というのは、新しいフレームワークを表層で導入する話ではない、ということです。CIを足し、lintを足し、PRテンプレを揃える。これらは儀式として整えれば整うほど、芯から遠ざかることがあります。芯から変わるとは、コードベース・チームの判断・組織のレビュー文化が、要件を中心に再配置されることです。儀式の追加でなく、判断の置き場所が動く。それが起きていないなら、ハーネスは形だけになって、複利資産にならず減価償却資産に逆戻りします。

なぜAIが書いたコードは信じられないのか

AIが書いたコードを、レビューに普段の3倍の時間がかかった、という話を最近よく聞きます。私自身も同じ経験をしました。自分の手元で測ったことがある。人間のPRなら15分で終わるレビューが、AIのPRだと45分かかった。差は3倍。でも内訳を見ると、コードを読む時間は変わっていません。膨らんでいるのは「これは本当にこの書き方でいいのか」を毎行ごとに自分に問い直す時間です。書く時間は確かに短くなった。だがレビューに時間が溶ける。AIが速くなったというより、読む側の自分が遅くなった、という言い方のほうが正直に近い気がしています。差し引きで本当に速くなったのかは、まだ分かりません。

なぜか。私はこれを長く考えてきました。今のところの答えは、信頼の前提が足りていないから、です。

人間が書いたコードをレビューするとき、私たちは多くの前提を共有しています。書き手の癖、過去の議論、チームのコーディングスタイル、ドメインの常識。これは明示的にはどこにも書かれていないが、レビュー時には全員の頭の中にある。だから「これは違うな」と直感で判断できる。直感の根拠は、言語化されていない共有知です。

AIが書いたコードには、この共有知がありません。書き手は誰でもなく、あるいは全員だ。書き手の癖は、世界の平均値です。あなたのチームのスタイルではない。だから1行ずつ、明示的に「これは合っているか」を判定する必要が出てくる。直感が効かないのです。

直感が効かないコードのレビューは、コードを書くより重い作業です。書くときは自分の知識の流れに沿って書ける。レビューは他人の流れを再構築しながら検証する。AIがその「他人」になったとき、再構築の難度は跳ね上がります。

ここで多くの人がやってしまうのは、プロンプトの精度を上げるという方向に走ることです。延々と続くプロンプトを書く。「あなたは熟練のRustエンジニアで、このプロジェクトのコーディング規約に従い、エラーハンドリングは…」と続く。これは効くこともあるが、本質ではないと考えています。

本質は、AIに渡せる前提の量を増やすことです。プロンプトの中に書くのではなく、プロジェクトの構造・コードの形・テストの存在として、AIが参照できる形で前提を置いておく。

私が一番効くと感じているのは、ドメイン知識の構造化です。プロジェクトのミッション、ビジネスの境界、登場するエンティティ、それらの関係。これらを口頭の文化に置いたままにせず、プロジェクト全体ドキュメントなり設計ドキュメントなりに、明示的に書き出しておく。書き出す行為そのものが、要件の言語化です。書いた瞬間に「あれ、これってどうだったっけ」が露出する。それは要件の穴の発見であって、隠してはいけない情報だ。

次に効くのが、責務の分離です。コードベースが1つの巨大な塊になっていると、AIはどこを変更すれば良いか判断できません。境界が引かれていれば、「このBounded Contextの中で完結する変更」と指示できる。境界はAIへの命令の解像度を決めます。境界が無いと、命令も曖昧にしかならない。

その手前にあるのが、プロセスの再設計です。レビュー段階で要件を発見するのではなく、上流で合意を取る。AIへ依頼する前、要件の合意ができているか自問する。合意できていないなら、依頼はまだしてはいけない。「とりあえず作らせて、見ながら考える」は、試作で捨てる前提なら有効です。本実装に合意のないまま渡すと、コストが大きく膨らむ。実装と差し戻しを何度も繰り返すと、その時間は積み重なって膨らみます。最初に合意を取っておけば、ずっと短く済んだはずです。試作と本実装の境界を、自分の中ではっきりさせておく必要があります。

そしてもう一段、見落としやすいのが、チーム展開です。1人がAIをうまく使えても、チームで効果を出すには、共通の使い方が必要になります。ペアでAIを使う。モブでAIを使う。AIへの指示の仕方を、暗黙知から明示知に変えていく。これは個人の生産性ではなく、組織の生産性の話だ。

チームのプロンプトを「個人芸」から「共有資産」へ移す

AIへの指示の仕方を、暗黙知から明示知に変えていく、と書きましたが、これだけでは抽象的すぎます。具体的に何をどこに置くか、を書きます。

私のチームで最初に起きたのは、「同じ機能を、人によって全然違う指示で書かせている」という事態でした。Aさんは英語の長文プロンプトで書かせる。Bさんは「いい感じに書いて」で済ませる。Cさんは雛形を使い回す。出てくるコードは、当然ばらつきます。レビューの粒度もばらつく。3人が同じドメインを触っているのに、3つの方言が並走する状態でした。

ばらつきの原因は、能力差ではない。指示が個人のローカルストレージにしかないことです。誰かのチャット履歴、誰かのCursorルール、誰かの頭の中。リポジトリには無い。だから共有も改善もされません。

私たちが切り替えたのは、こうです。プロンプトはコードと同じ場所で、同じレビュープロセスで管理する.claude/skills/.cursor/rules/AGENTS.md、PRテンプレートに書かれたチェックリスト。ここに置かれたものだけが、チームのAI指示として認められる。個人のチャット欄に書かれた指示は、その人が辞めたら消える。リポジトリに書かれた指示は、次の世代に残る。

「個人のチャット履歴に書いた指示は、その人が辞めた瞬間に消える」と書いて、自分でも少し怯みました。私自身、いまだに思いつきの指示はチャット欄に直接書いています。1日に何度も。それを全部リポジトリに上げる気力はありません。だから、「3回繰り返したらリポジトリに引っ越す」を個人的なルールにしています。3回未満は個人芸で構わない。3回を超えたら、それはチームの慣習になりかけている。慣習はリポジトリに置かないと共有資産にならない。

もう1つ、ペアやモブでAIを使うときに気づいたことがあります。他人がAIに出す指示を見ると、自分の指示の癖が露出する。「そんな前提を渡さずに頼んでたのか」「その聞き方だと、こういう事故が起きるな」が、画面共有の30分で何度も起きます。これはコードレビューとは別種の学びで、指示そのものが新しいレビュー対象として浮上してきた、という感覚があります。

ただし、ここにも撤回条件を書いておきます。3人未満のチーム、あるいは触るドメインがほぼ1つに収まるチームでは、プロンプトの共有資産化は過剰投資になりやすい。個人芸のままで回ります。私が話しているのは、5人以上で複数ドメインを触っているチームの話です。それ未満の規模では、まずドメイン構造化に投資したほうが、たぶん効きます。

ただ、これは私の現場での感覚で、撤回する条件を書いておきます。ドメインの境界が浅く、登場するエンティティが10個未満で済むような小規模プロダクトでは、構造化のコストがリターンを上回ります。また、すでに巨大モノリスになっているコードベースでは、責務分離のほうが先に効くと思っています。「うちの規模・段階でドメイン構造化が一番効くか」は、各チームで観察してから判断してください。

プロンプトに気合を入れるより、ドメインの境界を一枚の図にするほうが、はるかに効きます。エンティティ・関係・操作・状態遷移を、AIが読める形で置いておく。AIはそこを参照して、コンテキストに沿った実装を返してくる。プロンプトで指示するより、コンテキストで縛るほうが安定します。

これは結局、要件の話に戻ってきます。AIが信じられないのは、AIに渡している要件が信じられないからです。曖昧な要件、矛盾した要件、暗黙の前提を含んだ要件。これらを渡せば、AIは曖昧で矛盾した、暗黙の前提を勝手に補完したコードを返してくる。

信頼ギャップは、AIの能力ギャップではない。要件のギャップです。

AIへ仕事を投げると、代替案もメリットとデメリットも丁寧に並べてきます。並ぶのですが、並んだもののうちどれを採るべきかの軸を、AIは自分で選びません。いまの場面で何を最も重んじるか、その判断の中心に据えるべき軸は、空白のまま手元に返ってきます。空白のまま受け取った AI は、その場面の文脈に合った軸ではなく、平均的にもっともらしい軸——「読みやすさ」「一般的な拡張性」「世間でよく見るベストプラクティス」——を勝手に中心に据えてきます。出てきたコードは見栄えがよく、論理も整っていて、それでいて場面の主軸とずれている。レビューが3倍に伸びるのは、コードを読む時間ではなく、選ばれた軸がこの場面と合っているかを毎行ごとに照合し直す時間です。これは、3倍に伸びた45分の内訳を一度自分で記録した結果として、そう書いています。

そして、現場で繰り返し観測される設計の事故——標準解があるのに独自実装に逸れる、変化点が観測されてもいない段階で抽象化を入れる、運用組織のスキルを見ずに技術的に望ましい構成を採る、責務本体に書くべきものを近場の便利な場所へ書く——の多くは、AI 以前から存在していました。違うのは件数と頻度です。たぶん桁が1つ違う。人間がこの場面の主軸を意識化していないと、エージェントは黙ってその空白を平均で埋め、平均で埋まったコードが大量に流れ込みます。AIの間違いは新種ではなく、人間が支えていた主軸の選び方を継ぐ仕組みが先に崩れたことの帰結だ。半年ほど現場の事故報告を眺めて、そう書き直しました。

賭けている、と書いた以上、降りる条件も書いておきます。AI コードに固有で AI 以前には観測されなかった種類の事故を、新規パターンとして列挙できるようになったら、私はこの見立てを撤回します。今のところ、手元のメモにそういう新規パターンの記録はありません。

そう理解してから、AIへの不満が減りました。AIに対する期待値が、自分のドキュメントの精度を超えなくなったからです。AIは私のドキュメントの鏡だ。鏡に映る姿が気に食わないなら、文句を言う先は鏡ではなく自分だ。

そして、レビューの話には経済的な側面もあります。欠陥は、見つかる段階が後ろに行くほど、修正コストが桁で跳ね上がる。要件で見つけたものは要件文を直すだけ。実装で見つけたものはコードと設計と要件を直す。運用で見つけたものはそれに加えて顧客対応と再発防止策が乗る。AIで実装が速くなったぶん、要件レビューの相対的な投資対効果はさらに上がっていると感じます。レビューをドキュメントに対してではなく、要件・テスト・型・契約の組み合わせに対してかける、というのが、いま私が現場で守っているラインです。

要件を動くものに散らす

ここからが、第2部で一番伝えたい話です。

要件をドキュメントだけで保つことを、私はやめました。ドキュメントは補助に格下げ。要件の本体は、ドキュメントではなく動くものに置く、というのが現在の方針です。

動くものとは何か。エージェントが起動するたびに読まれるCLAUDE.md。毎回参照されるskill。CIで毎回実行されるテスト。コンパイルのたびにチェックされる型。PRごとに走るlint。レビュー時にAIが参照するチェックリスト。これらに分散して焼き込まれた要件です。

ドキュメントで書いた要件は、誰も読みません。読まれない要件は、要件ではない。一方、エージェントが毎起動で読むファイルに書かれた要件は、毎回読まれます。テストとして書かれた要件は、CIで毎回検証されます。毎回読まれる、毎回検証される。これが、要件が生き続ける条件です。

私のブログのリポジトリを例に取ります。このリポジトリには .claude/rules/voice.md というファイルがあって、文体ルールが書かれています。「です・ます調で統一する」「『非常に〜』を多用しない」「『おい』シリーズの構成パターンはこうだ」。これは要件文書です。そう呼ばないだけで、内容は要件です。

このファイルは、Wordに置いていたら今頃腐っていたと思います。誰も読み返さない。書いた本人すら忘れる。それがドキュメントの要件文書の運命です。

このファイルが生きている理由は、CLAUDE.mdからリンクされていて、毎回のセッションで読み込まれるからです。AIエージェントが記事を書くとき、レビューするとき、毎回参照される。そして実際に違反があれば指摘される。実行可能な要件になっている。

これと同じことを、プロダクト開発でやればいい、というのが私の考えです。

どこに散らすか

動くものに散らす、と言うとき、要件は種類によって違う場所に置かれます。私の手元では、おおよそ次のような置き場の使い分けに落ち着いています。

プロジェクト全体に関わる要件——ミッション、対象ユーザー、ビジネス上の制約、絶対に避けるべきこと——は、CLAUDE.md に書きます。これは頻繁に変わらない、静的な要件です。

ドメインごとの要件は、領域別のドキュメントと skill に書きます。「在庫管理ドメインでは、商品は在庫数とは別にステータスを持つ」「在庫切れ商品は検索結果から除外される」「ステータス変更は監査ログに残す」。設計の文脈で参照される層です。

機能ごとの要件は、コミットメッセージと PR のテンプレート、テストファイルの先頭コメントに置きます。「この機能は何を達成するか」「合格基準は何か」を、その機能のコードと同じ場所に置いておく。コードと近いから、コードと一緒にメンテナンスされる。

「コードは fmt を通す」「unwrap は使わない」「テストカバレッジ 80% 以上」のような手続き的な要件は、CI のワークフローと lint のルールに書きます。これは書いた瞬間に強制されるので、型と同類です。

最後に、振る舞いそのものの要件は、テストに書く。プロパティテスト、結合テスト、E2E テスト。「在庫切れ商品は検索結果に含まれない」というテストは、ドキュメントの要件文よりも厳密で、実行可能で、リファクタリングしても残ります。

並べると整然として見えますが、現場で難しいのは「どの種類の要件がどの層に属するか」の判断のほうです。私はここで何度かミスをしました。たとえば「在庫切れ商品は検索結果から除外する」を、最初はskillとPRテンプレートの両方に書いていた時期があります。書く場所が2つあるから、片方だけ更新されて、もう片方が古いまま残る。ドキュメントの腐敗を、散らした先で再生産していたわけです。逆に、「unwrapを使わない」のような手続き的な要件をskillにだけ書いて、CIに落とさなかった時期もあります。書いてあるのに守られない。読まれるが、検証されない。散らす場所を増やすほど、要件が「重複して腐る」リスクと「書いたのに効かない」リスクが両方増える。だから私のいまの規律は、1つの要件は1つの場所に置く、置く先は実行のどの瞬間に検証されるかで決める、です。散らすこと自体が目的ではありません。

もう1つの規律として、自動生成できるものは人間が書かないを徹底しています。型定義から生成できるAPIドキュメント、ソースコードから生成できる依存関係図、テスト結果から生成できるカバレッジレポート、CLAUDE.mdから生成できるskillの索引。これらを人間が手書きすると、書いた瞬間がピークで、あとはコードと乖離していくだけです。生成元はコードや型なので、それ自体が動くもので、変更すれば自動的に追従する。手書きの要件は陳腐化のリスクを背負い、自動生成の要件は背負わない。書く場所を選ぶ前に、書かずに済む場所はないか、と自問するようにしています。

これら全てを合わせて、動くものに散らされた要件と呼んでいます。1つの場所にあるのではなく、コードベース全体に散らばっている。散らばっているが、それぞれが実行されるときに参照される。ドキュメントのように埃をかぶることがない。

受け入れ基準を「実行可能な形」に翻訳する

散らす場所を決めたら、次にやるのは、受け入れ基準を実行可能な形で書くことです。これが、要件を動くものに移すときの中心の作業になります。

要件を書いた後、「満たしたかどうかをどう判定するか」を必ず書く。判定方法をテストとして書ければ、要件はテストとして実装されたと同じです。判定方法を型として書ければ、要件は型として実装されたと同じ。判定方法こそが要件の本体だ、という見方です。

例えば、Given-When-Then形式で受け入れ基準を書く。「在庫が0の商品が存在する状態で、その商品名で検索すると、結果に含まれない」。これは自然言語で書いた1行ですが、ほぼそのままテストに翻訳できます。書いた瞬間に実装と一対一の関係になる。

判定可能な受け入れ基準を持たない要件は、まだ要件になっていない願望です。実装は受け入れ基準にしか反応しない。願望のまま実装に渡すと、AIは想像で判定基準を作り、そこに合わせます。私たちのチームが望んでいた基準とは違う何かが、判定基準として採用される。

受け入れ基準には、満たすべき条件だけでなく、満たしてはいけない条件も書きます。「在庫切れ商品は結果に含まれない」は満たしてはいけない条件の宣言です。これを書かないと、AIは「含めても満たせるかも」という余地を残します。含めない、と書くことで、初めて要件として閉じます。

そしてもう一段、私が最近受け入れ基準に書き足すようにしているのは、意図してやっていないことの宣言です。「論理削除は今回のスコープ外」「多言語対応は対象外」「キャッシュは現バージョンでは入れない」。書かないと、AIは気を利かせて未来の拡張を実装に紛れ込ませます。良かれと思って入れた論理削除フラグが、要件にない複雑さを実装に持ち込んで、レビューで剥がす作業が発生する。やらないことを書かないと、やられる。受け入れ基準は、満たす条件と満たさない条件と意図しない条件、3つで初めて閉じる、というのが今の私の運用です。

決定論と確率論の分業

要件を動くものへ散らすとき、もう1つ重要な性質があります。決定論と確率論の分業です。

AIで開発するときに難しいのは、AIの確率的な振る舞いと、システムの決定論的な要請をどう繋ぐかです。AIに自由にコードを書かせるとバグが出る。完全に縛ると価値が出ない。この間でバランスを取る必要がある。

要件を散らす場所は、このバランスを設計する場所です。決定論的に守らせたいことは、lint・型・テストで縛る。AIはこれらを破れない。確率論で柔軟に扱いたいことは、プロンプト・skill・ガイドラインで導く。AIはこれらをヒントに自分で判断する。

要件のうち、決定論で表現できるものは、決定論側に焼き込みます。「在庫数は0以上の整数である」は型で書ける。「APIのレスポンスは200・400・500のいずれかを返す」は型で書ける。これらをAIに守らせる必要はない。型が守る

決定論で表現できない要件、たとえば「エラーメッセージはユーザーフレンドリーな日本語で書く」「コミットメッセージはConventional Commits形式で書く」は、確率論側に置きます。プロンプトで指示する。skillで例示する。AIが大体守る。完全には守らないこともあるが、レビューで補正する。

このハイブリッドが効くのは、人間の認知資源を温存できるからです。決定論で守られている部分は、レビューする必要がない。型エラーが出るならコードはコンパイルされていない。テストが通っているなら、テストされている要件は満たされている。レビュアーは確率論側、つまり人間にしか判定できない部分だけを見ればいい。

私の手元では、これを「人間のレビュー領域を最小化する設計」と書きました。AIで開発が速くなったぶん、人間がボトルネックになる。人間のボトルネックを減らすには、人間が判定すべき事柄を減らすしかない。それを減らす道具が、決定論的な検証の仕組みです。

複数の表現で同じ要件を書く

同じ要件を、わざと複数の表現で書くことがあります。一見冗長ですが、これをやると要件の品質が底上げされる、というのが私の手応えです。

例えば、ある検索機能の要件を書くなら、次のようになります。

  • 自然言語で「在庫切れ商品は検索結果に含まれない」と書く
  • ユースケースとして「ユーザーが商品を検索する → 在庫切れ商品を除外して結果を返す」と書く
  • 型として fn search(query: SearchQuery) -> Vec<InStockProduct> と書く
  • テストとして「在庫0の商品が結果に含まれないことをアサートする」と書く

4つの表現は、同じ要件を別の角度から記述しています。1つでも矛盾していれば、要件の理解にズレがあったということです。書いている最中に矛盾を見つけ、ステークホルダーへ戻って確認する。これが要件の妥当性確認の実体です。

複数の表現を併走させると、それぞれの限界が補い合います。自然言語は文脈を伝えられるが、曖昧。型は厳密だが、業務の文脈を伝えにくい。テストは実行可能だが、網羅性に頼ると膨大になる。これらを組み合わせることで、1つの表現では届かない網羅性に到達できる。

これは見積もりの分野で言われる「複数の手法で見積もりを出して、収束しているかを見る」という発想と地続きです。1つの手法に頼ると盲点が出る。複数の独立した手法で同じ対象を測ると、収束しているなら信頼できる、ばらついているなら何かを見落としている、と判断できる。要件も同じで、複数の表現で同じ対象を書いて、収束を確認するのが、品質を底上げする技法です。

検証は階層で組む、ハーネスは複利で効く

要件をテストや型へ焼き込む話を続けます。1つ書き加えておきたいのが、検証は1層でなく多層で組むという考え方です。

検証を1つの層で済ませようとすると、必ず漏れが出ます。テストだけで守ろうとすると、テストにない経路の振る舞いが抜ける。lintだけで守ろうとすると、振る舞いの欠陥が止められない。型だけで守ろうとすると、動的に決まる業務ルールが拾えない。1層では破れる。何枚か張って初めて、漏れが減る、というのが現場で繰り返し気づかされたことです。

私が組むようになった検証の階層は、3つの瞬間に対応します。コードがどの境界を越える瞬間に、何を守るか、で分ける。

1つ目は、書いている瞬間の関所。エディタの保存、ファイル単位の書き換えが終わった直後。フォーマッタ、即時のlinter、構文チェック、軽い型チェック。書き手のリズムを止めない範囲で、間違っていれば即座に直る。検証というより、ほぼ自動修正です。速さが命で、軽くて何度でも回せる、ということがこの層の存在価値。エージェントが書いた瞬間にも同じく走らせます。

2つ目は、共有する瞬間の関所。コミット、プッシュ、マージ、レビュー依頼。コードが個人の手元から、チームに渡る境界です。ここでは、全lint、深い静的解析、ユニットテスト、結合テスト、E2Eテスト、契約テスト、セキュリティスキャン、までを通します。秒で済むものから数十分かかるものまで含む、幅のある層です。「マージできた」という事実が、ここを通過した証拠になる。重い検証は、共有の瞬間にだけ集中する、と決めることで、書く瞬間は軽く保てます。

3つ目は、完了を宣言する瞬間の関所。AIエージェントが「タスクを終えた」と宣言する直前、または人間が「これで出します」と言う直前。テストが本当に緑か、lintが本当に通っているか、要件で書いた合格基準を本当に満たしているかを、機械的にもう一度確認する。これは私が比較的最近、運用するようになった層です。エージェントは「やりました」と言うが、実は失敗している、というパターンが現場で繰り返し起きます。自己申告を信じない仕組みが、最後の関所になる。エージェント時代に固有の、新しい層です。

各層は、守る対象が違います。1つ目は「形式」を守る。2つ目は「振る舞いと統合」を守る。3つ目は「完了の真実性」を守る。何を守りたいかで層を選び、層ごとに違うツールを置く。全部の層を最初から完璧に組む必要はありません。痛い思いをした順に層を増やしていく、というのが現実的な作り方です。

そして、層を増やすときの作法もはっきりしています。人手のチェックリストに頼らない。チェックリストは便利ですが、人間が確認する以上、忙しい週には飛ばされます。同じ確認を機械が回せる形に書き直す。lintルール、CIスクリプト、PRブロッカー。機械に渡せる確認は、機械に渡してから人手を考えるというのが、層を足すときの判断順です。

検証の層は、一度に組み上げるものではない。痛みのたびに1枚ずつ増えていく。3日連続で同じ事故が起きたら、その形のチェックを足す。半年経って起きなくなっていたら、その層は組織の習慣として定着したと見ていい。仕組みは意志ではなく、繰り返された動作の堆積として残ります。意志に頼って守る規律は、忘れた瞬間に消える。動作として残った規律だけが、次の世代にも伝わる気がしています。

この多層化が効く理由は、投資のコストが複利で積み上がるからです。1つlintルールを追加すれば、以降すべてのエージェントセッションでそのミスが防がれる。1つテストを追加すれば、以降すべてのセッションでその回帰が検出される。書いた瞬間ではなく、書いた後の運用で価値が複利で効いてくる。一度書いた検証が、以後ずっと働き続けます。

そして、この多層検証は非機能要件の自動測定にも使えます。「循環依存を作らない」「ある層から別の層を直接呼ばない」「テストカバレッジが一定以上」「主要画面の応答時間が一定以下」のような構造的な特性は、CIで実行可能なチェックへ変換できます。これは、要件の中でも特に書いた瞬間に陳腐化しやすい部類のものを、実行時に毎回確かめる仕組みへ置き換える発想です。

通常のテストが「振る舞いが正しいか」を確かめるのに対し、こちらは「設計上の特性が崩れていないか」を確かめる。重要だが緊急ではない、モジュール性や保守性のような特性を、緊急の圧力から守る装置として機能します。要件文に「保守可能であること」と書くだけでは何も守られませんが、「特定のモジュールから別のモジュールへの直接依存が増えていないこと」を毎日CIで測れば、保守性は実体として守られます。非機能要件を、測定可能な実行可能な仕掛けに翻訳する。これも、要件を動くものに散らす作業の一部です。

これはドキュメントへの投資との非対称な関係です。ドキュメントは書いた瞬間がピークで、あとは陳腐化していく。検証ハーネスは書いた瞬間が始まりで、運用するほど価値が積み上がっていく。ドキュメントは減価償却資産、ハーネスは複利資産。要件を動くものに散らす最大の理由は、ここにあります。

もう1つ、検証ハーネスの重要な性質があります。エージェントがコンテキストとしてアクセスできないものは、エージェントにとって存在しないということ。プロンプトに書いていないルール。CLAUDE.mdに書かれていない制約。口頭で伝えただけの慣習。これは、AIにとって存在しないのと同じです。

逆に言えば、コンテキストに書かれているもの、CIで実行されるもの、型として強制されるものだけが、AIに伝わります。だから、要件は届くべき場所に置かれている必要がある。要件を書いた、と満足するのではなく、「これは本当にエージェントに届くか」「実行のどの瞬間に検証されるか」を確認する規律が、ハーネス運用の中心にあります。

アーキテクチャで主観レビューを減らす

要件を動くものへ散らした後に、何が手元に残るか。私の現場では、人間が立ち止まって判断する場所だけが残りました。これをどう削るか、が次の話です。

AIで開発するときの検査を、私は4象限で見ています。縦軸に「客観/主観」、横軸に「予防/回復」を取る。

予防 回復
客観 自動化・スケール化 監視・障害対応
主観 認知負荷削減(アーキテクチャの仕事) リスク受容判断

客観×予防は機械的に判定できて事前に止められるもの(lint・型・テスト・CIゲート)。客観×回復は機械的に判定できて事後に対処するもの(監視・SLO・SRE)。主観×回復は人間が見て初めて分かる問題に、起きてから対応する領域(ポストモーテム・インシデントレビュー)。いずれも仕組みが整っている。

問題は主観×予防です。人間が見ないと分からない、しかし起きてから対処するのでは遅い、という領域。コードのレビュー、設計のレビュー、要件のレビュー。AI時代、ここが最大のボトルネックになる。

なぜか。AIで開発が速くなった結果、客観×予防の領域はさらに自動化が進みました。客観×回復もSREのプラクティスが浸透して、対応速度が上がっている。主観×回復は、起きてから対処なので、AIが速いことの恩恵が小さい。残った主観×予防が、相対的に最も時間を食う領域として目立ってきます。

つまりこうです。AI時代のエンジニアリングの主戦場は、主観×予防の領域をいかに小さくするかです。人間がレビューしなければならない事柄を減らす。人間がレビューしなければならない事柄について、レビューの認知負荷を下げる。これが、現代のアーキテクチャの中心的な課題だと考えています。

道具は何があるか。私がよく使う発想を並べます。

Bounded Contextで、境界に予測不能性を囲う

AIの最大の弱点は、コンテキストが広がりすぎたときに何を考慮すべきか分からなくなることです。コードベース全体を見ながら判断しろ、と言われると、関係ないコードに引きずられて、変な実装を出してくる。

これに対する答えは、線を引くことです。1つのドメインモデルが意味を持つ範囲を、地面に線として引く。「この線の内側で完結する変更」と指示できるなら、AIは予測可能な振る舞いをします。線の外には触らない。線の内では、そのドメインの言葉だけで考える。業界の語彙では Bounded Context(境界づけられたコンテキスト)と呼ばれている発想です。

境界は、AIへの命令の解像度を決めます。「商品検索を実装して」より「在庫管理コンテキストの商品検索を実装して」のほうが、出てくるコードの精度が大きく違います。境界が引かれていない巨大な泥団子のコードベースでは、AIは迷子になり、人間のレビュー負荷も上がる。境界がレビューを軽くするのです。

境界にはやる範囲だけでなく、意図的に触らない範囲も含める。「在庫の判断はここまで、注文側のテーブルには触らない」「決済の整合性は別レイヤーで担保するので、ここでは扱わない」。書かないと、AIは気を利かせて越境してきます。境界を引くということは、内側の規定だけでなく、外側を「意図して扱わない」と宣言する行為です。そして、境界違反の検出は人手レビューでなく、依存関係チェッカーやアーキテクチャテストに任せる。機械が境界の番人になる形にしないと、半年で境界は溶けます。

Always-Valid Domain Modelで、不正な状態を作れなくする

要件のうち「この値は0以上」「この日時は過去」「この組み合わせは不可能」のような制約は、ドキュメントに書くのではなく、型の中に閉じ込めるのが私の好みです。私はある時期から、ドメインの値を生のintやStringで持ち回るのをやめました。在庫数をu32で渡し回していた頃、マイナス在庫のテストケースを書き忘れて本番に届かせたことがある。値は型で囲い、コンストラクタを閉じる。ドメインオブジェクトは、常に正当な状態でしか存在できないように設計します。設計思想というより、自分が同じ事故を二度起こさないための、もう少し卑近な防衛です。

具体的には、コンストラクタを公開しない。代わりに Stock::new(count: u32) のようなファクトリで、不正な値を弾く。エラーを返すか、専用の型 NonNegative<u32> を要求する。型システムが「0以上の在庫数」という要件を強制する形です。

こうすると、AIがコードを書くとき、不正な状態を作れません。型エラーになる。型がレビュアーになる。私はこれを「型がレビューしてくれる要件」と呼んでいて、ドキュメントで書くより遥かに強力だと考えています。

Type Firstで、型から書き始める

AIに実装を任せるときは、関数の本体より先に型シグネチャを置きに行くようになりました。「この関数はこういう入力を受け取り、こういう出力を返す。エラーはこの型」。これを先に書いて、本体は AI に書かせる。

これをやるようになって、3つの効果が出ました。

1つ目は、要件の表明です。型は、関数が何を扱うかの宣言です。fn search(query: SearchQuery) -> Vec<InStockProduct> と書けば、「在庫がある商品しか返さない」という要件が型に現れている。型を読めば要件が読める。

2つ目は、AIへの制約です。型に合わない実装を書こうとすると、コンパイルエラーになる。AIは型に合わせるしかない。プロンプトで「在庫切れを除外して」と指示するより、型で InStockProduct を要求するほうが確実です。

3つ目は、レビューの省力化です。型が正しければ、本体の細部のレビューは軽くなる。型エラーが出ない時点で、要件の大枠は満たされている。レビュアーは「実装の細部」ではなく「型の選択」だけを見ればいい。

ただし、Type Firstには適用範囲があります。プロジェクト初期の探索フェーズでは、型を先に書くと逆に動けなくなる。型を決めるためには、ドメインがある程度固まっている必要があるからです。ドメインが固まる前に型を書くと、書いた型がすぐ陳腐化する。だから私は、Type Firstをドメインが固まった後の規律として運用しています。先ではなく、後。順序が逆だと効かない。

壊せるコードと壊せないコードを分ける

書き込みと読み込みでは、レビューの重さが違います。これに気づいたのは、深夜の差し戻しが続いた時期です。書き込みのバグは、データを壊す。戻せない。読み込みのバグは、画面が崩れるだけで、再描画すれば直る。両者を同じ関数の中に混ぜると、レビュアーは1行ごとに「これは壊せる側か」を毎回判定することになる。これが疲れる。

だから、書き込み側と読み込み側を、ファイル・ディレクトリ・クラスの層で分ける。書き込み側のPRは厳密に見て、読み込み側のPRは速く通す。レビューの強度を、人の良心ではなく構造で決める。AIが書いたコードを本番に通す勇気は、こういう非対称な防衛線の引き方から生まれます。

業界の語彙では、これに近いパターンを CQRS(書き込みと読み込みを別経路に分ける設計)と呼んでいます。私が実感しているのは経路の分離そのものより、「壊せるコードと壊せないコードを別物として扱う」という重さの非対称のほうです。

Event Sourcingで、事実を残す

AIの書いたコードを本番で動かして問題が起きたとき、起きたことを後から再現できることが重要です。Event Sourcingは、システムで起きた事実をすべてイベントとして記録する設計です。注文が入った時刻。在庫が減った瞬間。配送が始まったタイミング。すべてイベントとして残る。

これがあると、AIが書いたコードに不審な振る舞いがあっても、イベントログから再構成できます。なぜその状態になったかを追跡できる。説明責任の基盤になる。

全部のドメインにEvent Sourcingを敷くのは過剰です。私は、お金が動くところと、後から「なぜそうなったか」を顧客に説明する義務がある領域だけに絞っています。以前、それ以外の領域、たとえば検索のクリックログを「念のため」とイベント化しかけて、ストレージとスキーマ進化のコストで詰まったことがありました。事実は残ったが、誰も読まなかった。読まれない事実は、ただのコストです。逆に、決済と在庫の変動については、過去にDBの状態だけ見て「なぜマイナス在庫になったか」が再現できず、顧客への謝罪文を書けなかった夜があります。あの夜以降、Event Sourcingは「全部のために」ではなく「説明責任が発生する境界に絞って」入れる、という基準に変わりました。

AI時代、コードは速く生まれて速く変わります。コードを読んでも何が起きたか分からない瞬間が来る。そのときに残るのは、コードではなくイベントログです。「事実が残っている」という設計の安心感は、AIで書いたコードを本番に出す勇気の源にもなる。

主観×予防を最小化する、ということ

これらのパターンに共通しているのは、人間の主観的判断が必要な領域を、構造で減らしているということです。

要件をドキュメントで書くと、レビュアーはドキュメントとコードを照合する主観的作業をすることになります。要件を型に焼き込めば、コンパイラが照合してくれる。要件をテストに焼き込めば、CIが照合してくれる。要件を境界に焼き込めば、Bounded Contextが照合してくれる。

これは、要件を消滅させているわけではありません。要件を表現する場所を変えているだけです。ドキュメントから、型・テスト・境界・スキーマ・イベントへ。表現が変わると、検証が機械化できる。検証が機械化できると、人間の主観レビューが減る。

主観×予防を最小化する努力は、AIの精度を上げる努力ではなく、AIの間違いを機械が捕まえる仕組みを増やす努力です。AIは間違える。間違えていいから、間違いが本番に届かない構造を作る。これがAI時代のアーキテクチャの仕事だと考えています。

そして、ここで効いているのは結局、要件をドキュメントではなく構造に焼き込んでいるかです。ドキュメントは人間しかレビューできない。構造はコンパイラとCIがレビューしてくれる。要件の置き場所を変えることが、AI時代のアーキテクチャの起点になります。

結合の強度を選ぶ

要件群同士の関係を設計するときに、私が頭の中で使っている目盛りがあります。結合の強さを4段階で見る、というものです。

最も濃いのが侵入的な結合で、片方の実装の細部にもう片方が直接依存している状態。プライベートな内部状態を覗き込む、内部の関数を直接呼ぶ。これが一番避けたい形で、片方の小さな変更がもう片方を壊します。

そこから一段薄くなると機能的な結合で、共通のビジネスドメイン知識を共有する関係。「在庫管理ドメイン」の中で、複数の要件が同じ前提を共有する。同じドメインの中なら自然な結合で、避けるべきものではありません。

ドメインモデルの一部が境界を越えて共有されると、モデル的な結合になります。注文ドメインの「商品」が、在庫ドメインの「商品」と同じモデルを使う。境界を越えるたびに、両者が同期して変わる必要が出てきます。

最も疎なのが契約的な結合で、最小限の入出力契約だけで繋がる関係です。「このAPIに { product_id, quantity } を渡せば、{ ordered_id } が返る」。中身がどう実装されているかには互いに干渉しない。

結合の選び方には、原則があります。頻繁に一緒に変わるものは近くに、別々に変わるものは離して置く。同じドメインの中の要件群は機能的な結合で構わない。違うドメイン同士は契約的な結合に下げる。これを誤ると、同じ変更を10箇所に伝搬させる必要が出てくる。

要件を動くものに散らすとき、散らした先同士の結合をどの強度に設計するかが、認知負荷を決めます。散らせばよいわけではなく、散らした後の結合の強度を選ぶ。これも要件設計の中心的な仕事です。

非機能要件は互いに干渉する、トレードオフのネットワーク

要件の話の中で、機能要件はわりと書きやすい。「ユーザーが商品を検索できる」「在庫切れ商品を含めない」のように、できる/できないが判定できるからです。

難しいのは非機能要件のほうです。性能、可用性、セキュリティ、保守性、ユーザビリティ、移植性、拡張性。これは「何をするか」ではなく「どれだけうまくやるか」を規定します。ドキュメントで書こうとすると、すぐに「セキュアでなければならない」「使いやすくなければならない」のような検証不能な文に縮みます。検証可能性の節で書いた「悪い言葉」のリストは、非機能要件の周りで特に多発します。

非機能要件を扱うとき、私が頭に置いている前提が1つあります。すべての設計はトレードオフだ、ということ。何かを良くすれば、何かが悪くなる。トレードオフが見えていないなら、存在しないのでなく、まだ気づいていないだけです。「全部を最高水準にしたい」と要件を並べる依頼者がいたら、その要件群はそのままで実現不可能だ、と早い段階で伝える必要があります。全部を最大化した設計は、たいてい途中で沈みます

トレードオフを「最適化の問題」と捉えると、必ず詰まります。性能とセキュリティは、同じ単位では測れない。比較できないものを比較する局面で、人間が最後に1つを選ぶ。その選択を計算で逃げないことが要件設計者の仕事だと思っています。過去に、性能と監査ログ保持期間のトレードオフで詰まったことがあります。両方を最大化したい依頼が来て、計算すると予算が1.7倍になる。どちらを諦めるかを決めずに3週間引きずり、最後はインシデントが起きてから保持期間を渋々延ばす形で決まりました。決められなかったから決まらなかったのではない。決めなかった結果、事故が私たちの代わりに決めた。それだけの話です。選んだ理由は、計算ではなく言葉にしか残せません。だから優先順位は、数字より、選んだ場面の言葉として書き残しておく。

そして非機能要件には、もう1つ厄介な性質があります。互いに干渉することです。

互いに干渉する、何かを上げれば何かが落ちる

具体的に見ます。あるAPIに対して、暗号化を厚くするほど、応答時間は悪化します。鍵交換、ハンドシェイク、暗号化処理、復号処理、それぞれにCPU時間がかかる。セキュリティの要件を強くすると、性能の要件が圧迫される。可用性とコストも同じです。冗長化を増やすほど可用性は上がるが、インフラコストが増える。ログ保持期間を長くするほど監査と可観測性は上がるが、ストレージコストとプライバシー懸念が増える。

非機能要件は孤立して書いても意味がない、というのが、この性質から来ます。1つの要件を強めると、別の要件が弱まる。だから、要件を1つずつ最適化するのではなく、要件群全体のバランスを設計する必要が出てきます。

「セキュリティ:高、性能:高、可用性:高、コスト:低」と並べた仕様書は、現実で実現不可能です。ドキュメントは要件を独立に書くのへ向いていますが、現実の要件は独立していない。ドキュメントのフォーマットそのものが、トレードオフを見えにくくしている、という言い方もできます。

過去のソフトウェア事故を振り返ると、相互作用の見落としが繰り返し出てきます。ある小売チェーンで起きた大きな情報漏洩は、空調管理システムのリモートアクセス権限が、店舗ネットワーク全体への侵入経路になった事例として知られています。空調側の要件は「外部から監視できる」、店舗ネットワーク側の要件は「店舗業務に必要」、それぞれは妥当でした。問題は、両者を繋いだときに発生する経路を、どちらの要件も明示していなかったことです。

ある宇宙探査機の事故は、単位系が異なるサブシステム間で、変換せずに数値を渡してしまった事例として知られています。各サブシステムの要件は内部では正しかった。問題は、相互運用性の要件が、両側のどこにも明示されていなかったことです。

これらに共通するのは、個別の要件は妥当でも、要件群の境界や相互作用が要件として書かれていなかった、という構造です。要件は単独では完全になれない。要件と要件の間にある関係を、別の要件として書く必要があります。

ここで、信頼性を例に1つ補足しておきます。

信頼性の要件で私が大事にしているのは、「壊れないシステム」でなく「壊れても続くシステム」を要件にすることです。

コンポーネントの故障そのものは避けられません。ハードディスクは壊れる。ノードはダウンする。ネットワークは切れる。これらが起きたとき、システム全体が止まるかどうかは別の問題です。コンポーネントの故障を直ちにサービスの停止へ転化させない設計を要件として書く。

これは性能や可用性の単純な数値目標とは違う、構造的な要件です。要件文には、「故障が起きたらどう振る舞うか」「どこまでの故障なら許容するか」「復旧の前と後で何が保たれるか」を書きます。これがあると、設計時に冗長化や分散の判断ができる。書かないと、AIは「正常系で動く」コードしか書きません。

暗黙的な要件こそ、書き漏らしやすい

非機能要件を扱っていてよく感じるのは、書かれている要件より、書かれていない要件のほうが事故を起こす、ということです。

要件には、明示されるものと、明示されないものがあります。「ユーザー数の目標は1万人」「応答時間は1秒以内」のように、依頼者が明確に語る要件は、書類に乗ります。これは明示的な要件です。一方、「データの整合性は崩れないこと」「セキュリティ侵害があったときに被害が広がらないこと」「障害から速やかに復旧できること」など、誰も口に出さなくても暗黙に期待されている要件があります。これが暗黙的な要件です。

暗黙的な要件は、依頼者にとっては「言うまでもない」ものです。だからヒアリングしても出てきません。「セキュリティはどうしますか」と聞かれて初めて、ようやく考え始める。あるいは、聞かれても「もちろん大事です」で終わる。具体的な水準は、依頼者の頭の中にすら無いことが多い。

ただ、暗黙的な要件は満たされなかったときに最もダメージが大きい性質を持っています。「セキュリティが破られた」「データが壊れた」「規制に抵触した」。これは表に出ていなかった分、起きたときに「言ってなかったじゃないか」「いや、言うまでもないでしょう」という応酬になりやすい。

要件発見の仕事の半分は、暗黙的な要件を引き出して、明示的にすることだと考えています。「言うまでもない」と思われている前提を、こちらから具体化する。「セキュリティが大事ということは、たとえばこの侵入経路を防ぎたい、ということですか」と問い返す。暗黙のものを言葉にした瞬間、初めて要件として扱えるようになります。

ここでもAI時代の影響が出ます。AIには、暗黙の前提が一切伝わりません。書類に書かれていない要件は、AIにとっては存在しないのと同じです。書かれていなかった暗黙の要件を、AIは無頓着に踏み抜いていきます。暗黙の要件をどれだけ明示化できるかが、AIに渡す要件の質を決めます。

優先順位を選ぶ、何を取り、何を捨てるか

互いに干渉する要件群を扱うときに、私が現場で守っている規律が、「最も重要な3つ」に絞る、ということです。

Top 3 を決める作業の裏で、自分が実際にやっているのは、何本かの軸の中から1本を選び取ることです。要件の評価には、裏に何本もの軸が並んでいます。目的にどれだけ合っているか。制約をどれだけ満たしているか。そもそも技術的に作れるか。当該組織のスキル分布で運用し続けられるか。保守性や可観測性のような品質を確保できるか。短期と長期で時間効果がどう違うか。不確実性をどう引き受けるか。既存アーキテクチャと整合するか。関係者が合意できるか。学術用語では分野ごとに別の名前で呼ばれますが、現場で衝突している軸は、たいていこのあたりに収まります。

軸が複数並んでいるとき、判断とは「全部の軸を最高水準にする」ことではなく、いまの場面でどの軸を中心に据えるかを1つ選ぶことです。私はこの「中心に据える軸」を「主軸」と呼ぶことにしています。Top 3 に絞るという作業の本体は、主軸を1つ立てて、二次条件を2つほど添える、という形に置き換えられます。主軸はその場面で他より優先するために立てるもので、二次条件は主軸ほど重視しないが最低水準を切らないために置くものです。1軸に絞った瞬間に他を捨てていいわけではない——この区別を持たないと、主軸を立てた瞬間に別の軸が無防備に置き去りにされます。

ここで起きやすい失敗は、主軸そのものの取り違えです。障害対応の最中に長期の理想設計を始めてしまう。基幹システムの刷新なのに、短期の実装容易性だけで選んでしまう。セキュリティの問題なのに、UX の都合で判断を曲げる。PoC なのに本番運用の品質を要求する。本番機能なのに PoC の速度感で書く。技術領域は違いますが、構造は1つに収束します。本来の主軸ではない軸を、主軸として採用してしまっている。主軸を間違えると、二次条件をどれだけ精緻に満たしても判断は失敗します。これは設計の事故というより、判断の手前で起きている事故です。

ただし、規制業界のように複数の軸が一次制約として同列に並ぶ場面では、1本に絞る規律そのものが成立しません。金融や医療や安全制御の領域では、性能と監査と可用性の3本がいずれも下限を切れない。そこは別の世界として切り分けています。私が書いているのは、絞れる側の話です。絞れる場面で絞らないことの代償は大きく、絞れない場面で無理に絞ることの代償も同じくらい大きい。まず自分の場面が絞れる側か絞れない側かを判定するのが、主軸を立てる手前の仕事です。

そして、ここで効いてくるのが第1部から書いてきた「どの軸を大事にしているかを言葉にする」規律です。書いていないと、依頼者は依頼者の主軸で、エンジニアはエンジニアの主軸で、AIエージェントは平均的にもっともらしい主軸で、それぞれ違うものを「最重要」として扱います。主軸は明文化されない限り、関係者ごとに別々の軸が裏で走る。Top 3 を要件文に書くという作業は、その背後で主軸を1つ選んで明文化する作業の表側です。

依頼者にヒアリングすると「セキュリティ・性能・可用性・保守性、すべて重要」と並べてきます。すべてが最重要なら、トレードオフの場面で何を譲って何を守るかが決まらない。そこで、「最も重要でない要件を順番に外していく」問いを使います。「すべて重要というのは分かりました。では、もし1つだけ妥協するとしたら、どれですか」。これを繰り返してTop 3に絞ります。

なぜ3つか。10個並べるとどれが大事か分からなくなる。1つだけだと、他が無視される。3つは、優先順位を識別しつつ、トレードオフの会話を可能にする数です。絞れなかった残りの要件は、Top 3 に従属する形で扱う。Top 3を満たした上で対応し、矛盾するなら他のほうを諦める。これで、設計時の判断が一貫します。

要件のうちには、1つでは不十分で、複数の組み合わせで初めて意味を持つものもあります。たとえば「金融取引の信頼性」は、性能・可用性・データ整合性・監査可能性が同時に成立しないと達成されない。こういう複合的な要件は、Top 3の枠の中で、複数の構成要素として分解して展開します。

ハード目標とソフト目標を分ける

優先順位を選んだ後、達成の性質が違う2種類の要件があることに気づきます。

ハード目標は、達成したかしていないかが二値で判定できる要件です。「APIは規定のステータスコードしか返さない」「決済処理はべき等である」「個人情報は暗号化されてDBに保存される」。満たしていないなら、それはバグで、出荷はできません。

ソフト目標は、満たし方の度合いがある要件です。「応答が速い」「使いやすい」「保守しやすい」。これは度合いで評価されるので、複数の軸の組み合わせで「十分」を定義する必要があります。「応答が速い」を、「初回ロード」「操作後の応答」「検索結果の返却」のように、シーンごとに切り分けて、それぞれに許容範囲を置く。

ソフト目標で注意したいのは、完全達成を目指さないことです。「無限に速く」を追求すると他の要件が崩れます。最低限・目標・意欲的目標・願いの4段階で書き、「目標」のラインに到達したらそこから先はトレードオフで決める。完璧を狙わず、十分を狙う

優先順位を要件文に書く

決めたTop 3と段階記述は、要件文に明示的に書きます。書かないと、AIは平均的なバランスを採用してきます。「応答時間(最優先)、セキュリティは認証突破耐性を確保(次点)、コストは年間予算内(制約)」のように、序列を明示する。

優先順位の記述はドキュメントのままで構いませんが、書いた優先順位が守られているかを、定期的にコードで確認する必要があります。性能が悪化したのは、誰かが「セキュリティを少し強めた」結果かもしれない。優先順位の監視は、コードのメトリクスとアーキテクチャ判断を結ぶ運用の話で、第3部の「要件を殺すな」に繋がる主題です。

要件は単独では完成しない。互いに干渉するからこそ、要件群全体を1つの設計対象として扱う規律が要ります。これがアーキテクチャの仕事の中心にあると、私は考えています。

優先順位は時間で反転する

優先順位は決めて終わり、ではありません。プロジェクトの段階が変わると、Top 3 そのものが入れ替わります

立ち上がった直後の段階では、機能要件が支配的です。「使えるものを早く出す」が最優先で、性能や保守性は後回しでも、たいてい問題になりません。利用者がまだ少なく、コードもまだ新しい。

しかし、利用者が増えてくると、スケーラビリティが前面に出てきます。1台のサーバーで足りていたものが、複数台に分散しないと処理できなくなる。データベースの設計も、初期の素朴な作りでは間に合わなくなる。同じ要件群でも、優先度の重みが変わります。

そして、システムが安定して稼働する時期に入ると、今度は保守性運用効率が支配的になります。コードを直すコスト、運用するチームの人件費、障害対応の負担。これらが日々のコストになり、機能追加よりも長期の保守性が重要になる。

つまり、非機能要件の優先順位はプロジェクトの寿命に沿って変わるのです。「Top 3」も、立ち上げ時のTop 3と、その後のTop 3は、たいてい同じではありません。

ここで気をつけたいのは、過去のTop 3に固執すると、現在のシステムを壊すことです。立ち上げ時に「最優先は速さ」と決めて作った要件群を、利用者が桁で増えても同じまま運用すると、保守性が崩壊して身動きが取れなくなる。要件は時間の中で重みづけを更新する必要があります。

これを書きながら、自分が一度しくじった場面を思い出します。立ち上げ期に「速さ最優先」で組んだAPI群を、利用者が10倍になった後も同じ重みで運用していました。新機能を出すたびに障害が増える。原因は1つの機能ではなく、Top 3そのものが古びていたことでした。保守性が下から3番目だった当時の優先順位は、その時点では正しかった。正しかったが、半年経って正しくなくなったのに、誰も書き換えを提案しませんでした。私自身も、最初に決めた表を信じすぎていた。書き換える勇気、と書きましたが、勇気というよりTop 3を定期的に見直すリズムを運用に組み込まなかったことが、本当の失敗だったと思います。半年に一度、Top 3を声に出して読み直す。読み直して違和感が出たら、その場で書き換える。要件文の更新を、機能追加とは別の運用イベントとして時間割に置く。これがないと、過去のTop 3は静かに現在のシステムを侵食します。

これは要件の運用の話で、第3部の「要件を殺すな」と地続きです。書いた瞬間の優先順位を、永遠に守る必要はない。ステージが変わったら、Top 3も書き換える。書き換える勇気が、要件を生き続けさせます。

「いつ・誰が・どこで・何が・どう・どのくらい」を埋める

非機能要件を曖昧でなくするとき、私が頭の中で必ず埋めるようにしている問いがあります。いつ・誰が・どこで・何が・どう振る舞い・どのくらいの数で、を1つの場面として書き切る、というものです。

「セキュリティが高い」「使いやすい」「速い」のような単語は、書き手と読み手で違う絵が見えています。でも、1つの具体的な場面として書き切ると、絵が揃う。

たとえば「セキュリティが高いこと」を、この6つの問いで書き直すと、こうなります。

外部の悪意ある攻撃者が、認証情報を試行する攻撃を、本番運用中の認証APIに対して行ったとき、システムは攻撃を検知してアカウントをロックし、規定時間内に管理者に通知する。

「いつ=攻撃の試行が起きたとき」「誰が=外部の悪意ある攻撃者」「どこで=本番運用中の認証API」「何が=システム」「どう=攻撃を検知してアカウントをロック」「どのくらい=規定時間内に通知」。6つの隙間が全部埋まっているから、これは実装とテストに直接落ちます。

書き慣れていないと、最初は不自然に長く感じます。でも、長いのではなく、今までが短すぎただけです。「セキュリティが高い」の1行は、6つの問いを全部読み手の想像に投げていた。だから書き手と読み手で別の絵が見えていた。6つを書き切ると、読み手は想像する余地がなくなります。AIに渡しても、書かれた通りに実装してくれます。「セキュリティが高い」では、AIは何を作ればいいか分かりません。「規定時間内に通知」と書いてあれば、その時間に合わせるコードを書きます。

非機能要件を6つの問いで書き切ることは、「要件を動くものにする」上での重要な実践です。曖昧なドキュメントを、実装可能な場面に変換する。フォーマットが要件の質を縛るなら、フォーマットを変えるところから始める、というのが私の現在地です。

書き切った後、もう一段やります。書いた数値を監視ダッシュボードに繋げる。応答時間、エラー率、認証試行回数、ロック発動頻度。要件文に書いた数字がそのまま監視に流れるようにすると、要件と現実のズレを機械が検知してくれます。手で四半期レビューする運用は、要件の半分が見落とされて終わる。要件の数字は、書いて終わりではなく、計測のキーに変える。これも、人手で見守る部分を機械に渡していく姿勢の1つです。

領域ごとに要件の重みは違う

ここでもう1つ書いておきたいのが、システム全体に同じ要件群を当てはめない、という発想です。1つのプロダクトの中でも、領域ごとに必要な品質要件は違います。顧客向けの画面はスケーラビリティと可用性が支配的、バックオフィスはセキュリティとデータ整合性が支配的、検品ツールは保守性とテスト容易性が支配的。これらを全部1つの巨大なモノリスに収めると、必要な要件群が衝突して、どれも中途半端になります。

逆に、領域ごとに必要な要件群が揃っている範囲を切り出して、それぞれを独立して動かす設計に分けると、要件群の衝突が減ります。マイクロサービスや境界づけられたコンテキストの分け方は、技術的な分割というより、要件群が違うところに線を引く作業です。要件発見の段階で「ここから先は別の要件群が支配する」と気づければ、設計の境界も自然に決まります。

要件は均質でない。プロダクトの中の領域ごとに、要件の重みが違う。この見方を持つと、Top 3もシステム全体ではなく、領域ごとに別のTop 3になります。要件発見の段階で見抜けると、後段の設計が楽になる視点です。

AIを組み込んだシステムでは、品質要件のカテゴリが増える

ここまで挙げてきた品質要件は、ソフトウェア一般の話です。AIや機械学習を組み込んだシステムには、従来の分類に収まらない品質要件が乗ります。私自身は規制領域の現場には普段いないので、ここから先は射程外の話を含みますが、要件発見の対象が広がっているという感覚だけは、輪郭を共有しておきたい。

判断の根拠を人間が説明できるか。出力が特定の属性で偏らないか。AIの判断のトレーサビリティが法令の要求を満たすか。これらは性能や可用性のような数値目標とは違う種類の要件で、検証可能性をそのまま当てはめにくい。「説明可能であること」をテストに落とすには、「説明とは何か」「誰にとって説明可能か」を先に決めなければなりません。定義そのものが、新しい要件発見の対象になっているということです。

古典の要件工学の道具立ては今も使えますが、対象が増えた分、扱いの幅も広げる必要が出ている。これも、要件を動くものへ散らす話と無関係ではありません。説明可能性を「ドキュメントに書いた」で済まさず、推論ログ・モデルカード・監査経路として動くものに焼き込めるかが、AI時代の要件運用の延長線上にあります。

おわりに

要件をドキュメントだけで保とうとして、何度か失敗してきました。

理由は、たぶん私の能力でも、書き方の流儀でもない。ドキュメントという媒体そのものに、毎日読まれ続けるための仕組みが備わっていないのだと、今は思っています。

要件をドキュメントだけに置くのを、私はやめました。やめてみて初めて、ドキュメントは何のためにあるのかが少しずつ見えてきた気がします。ドキュメントは補助で、本体は動くもの。CLAUDE.md、skill、テスト、型、lint、CI、PRテンプレート。これらに分散して焼き込まれた要件だけが、毎日読まれ、毎日検証され、毎日少しずつ良くなっていく。

決定論で表現できるものは、決定論で守る。確率論で扱うべきものは、プロンプトとガイドラインで導く。両者を混ぜずに、人間がレビューすべき領域を最小化するのが、要件設計の中心的な仕事になりつつある気がしています。

そして、主観×予防の領域を構造で減らす。Bounded Contextで予測不能性を囲い、型で不正な状態を排除し、テストで振る舞いを縛る。AIは間違えます。たぶん、これからもしばらく間違えます。間違えていいから、間違いが本番に届かない構造を作る。それがAI時代のアーキテクチャの仕事だと、今のところ考えています。

要件を動くものへ散らすことの効用は、3つあると思っています。1つは、要件が腐りにくくなること。動くものは、変更すれば壊れて気づける。2つ目は、AIが直接読めること。ドキュメントはコンテキストを圧迫しますが、コードや型・テストはAIにとって読みやすい形で要件を伝えられる。3つ目は、レビューが軽くなること。決定論側で守られた要件は、人間によるレビューを必要としない。

ただし、これは万能ではありません。動くものへの分散は、それ自体が保守コストを生みます。テストが古びる。lintルールが時代遅れになる。CLAUDE.mdの記述が現状と乖離する。散らした分だけ、剪定の責任も増える。剪定をどう回すかは、第3部の話です。

おい、要件を動くものにしろ。ドキュメントに閉じ込めるな。型・テスト・CI・PRへ散らせ。動くものに刻まれた要件だけが、たぶんAIに飲み込まれず残ります。

第3部「おい、要件を殺すな」では、動き始めた要件を時間の中でどう保つかを書きます。要件は決定の蓄積であること、AIには時間軸が見えないこと、要件を回し続ける運用、人間に残る責任、そして実務で守るべき最低ラインについて、順番に。

参考書籍