AI開発における仕様・テスト・コードの同期維持

AIエージェントが仕様、テスト、コードから逸脱するのを防ぎましょう。

目次

AIコーディングエージェントは機能を迅速に提供しますが、仕様書、テスト、コードは静かに乖離していきます。このガイドでは、トレーサビリティモデル、仕様からテストおよびコードへのマッピング、そしてマージ前に乖離を検知するCIチェックについて解説します。

実行中のシステムに対して誰も再確認しない仕様書は、存在しない仕様書よりも悪いものです。なぜなら、それは誤った安心感を生み出すからです。レビュアーは差分ではなく文書を信じるようになり、「既存のパターンに従え」と指示されたAIエージェントは、満たすべき要件に反していても、コードが実際にしていることを喜んで模倣します。

解決策はドキュメントを増やすことではありません。これは、ほとんどのリポジトリですでに存在する4つのもの間の小さな、強制可能なリンクです。それは、要件、その背後にある設計決定、それを証明するテスト、そしてそれを変更したコミットまたはプルリクエストです。

traceability links connecting specs, tests, and code

このリンクが共有された理解ではなくデータとして存在することで、あなたはそれをクエリできます。テストカバーのない要件、どの要件にもマッピングされなくなったテスト、要件IDと一致せずに変更されたファイルなどを尋ねることができます。そのクエリこそがこの記事の実際の成果物であり、残りの記事では、おそらくすでに使用しているツールでそれを構築する方法を解説します。

乖離の問題:なぜ仕様、テスト、コードが同期外れるのか

乖離は4つの認識可能な形状として現れ、AI支援チームは手作業で全ての行を書くチームよりも速くこれら全てに遭遇する傾向があります。

  • 仕様は変更されたが、コードは変更されていない。 要件はフォローアップの会話やコメントスレッドで明確化されますが、実装をそれに合わせて再生成または編集する人がいません。
  • コードは変更されたが、仕様は変更されていない。 エージェントまたは開発者がバグを修正またはモジュールをリファクタリングしますが、仕様は古い動作をまだ現在のものであるかのように記述し続けます。
  • テストは意図ではなく実装をカバーしている。 ユニットテストはコードが現在していることをアサートしており、これは循環的です:コードが誤った要件を満たしていても、構築によりパスします。
  • プルリクエストは要件を参照していない。 確認すべき明確な主張がないため、レビュアーは「妥当に見える」という理由だけで差分を承認します。

AI開発フレームワークに関する最近のプロセス研究は、エージェントがコードを迅速かつ反復的に再生成し、各再生成は仕様と実装がさらに乖離する新たな機会であることを precisely に特定し、仕様乖離を再発リスクとして特定しています。仕様駆動開発 vs ヴァイブコーディング に関する議論は、実はこの同じ失敗モードに関する論争です。誰も強制しない仕様は、追加的な儀礼を伴うものの、それなしの場合と同じ乖離に退化します。

現代的なSpec-kitスタイルのワークフローは、これを increasingly 仕様腐敗(specification rot) として捉えています。仕様は権威ある外観を保ちながら、システムが実際にしていることとの接続を静かに失っていきます。仕様駆動開発のコア定義 は仕様を真実の源(source of truth)として扱っていますが、真実の源は、何かが現実に対してそれを不断にチェックし続ける場合にのみ真実であり続けます。

AI支援開発のためのトレーサビリティモデル

機能するトレーサビリティモデルには、ビジネス要件をコード行やそれを実装したプルリクエストまで繋ぐ6つの識別子が必要です。ほとんどのチームは既にこれらのうち3つか4つを持っていますが、欠けているのは通常、設計決定IDと、テストおよびコミットからの明示的なリンクです。

識別子 存在場所
要件ID requirements.md または仕様ツール REQ-014
設計決定ID ADR / 決定記録 ADR-0032
タスクID タスク分解または課題トラッカー TASK-014-3
テストID テストファイルまたはテスト名 test_req_014_password_reset
コミット / PRリンク Git履歴 PR #482
変更ファイル Git差分 auth/reset.go, auth/reset_test.go

これらの識別子の間の関係はグラフを形成し、直線ではありません。なぜなら、1つの要件が複数のタスクを生み出し、1つのプルリクエストが同時に複数の要件に触れる可能性があるからです。

graph TD REQ["要件
REQ-014"] --> ADR["設計決定
ADR-0032"] ADR --> TASK["タスク
TASK-014-3"] TASK --> CODE["コード変更
auth/reset.go"] TASK --> TEST["テスト
test_req_014_password_reset"] CODE --> PR["プルリクエスト
#482"] TEST --> PR PR --> COMMIT["コミット履歴"]

このグラフを文章ではなく構造化データとして保存することが、後でそれをクエリできるようにするのです。GitHubのSpec Kitエコシステムは正確にこの方向へ移動しており、spec-kit-trace のような拡張機能は、仕様ファイルとテストファイルに埋め込まれた REQ-XXX トークンをスキャンし、そのリテラルなテキストマッチから決定論的なマトリックスを生成し、静かな偽陽性をもたらすあいまいな名前ベースの推測を意図的に避けています。

仕様からテストへのマッピング:受け入れ基準をテストケースへ変える

仕様の各受け入れ基準は、構築により行動アサートです:この状態が与えられ、アクターがこの行動を取る場合、システムはこう応答すべきです。これはすでにテストケースの形状です。そのため、最も強力なSDDワークフローは、コード生成エージェントに事後で独自のテストを発明させる代わりに、コードを生成するのと同じ受け入れ基準からテストを生成します。

これらの基準を書くために広く使用されているフォーマットはEARS(Easy Approach to Requirements Syntax)で、これは各要件を「<トリガー>の場合、システムは<応答>すべきである」といった曖昧性のない、テスト可能なパターンに強制します。この構造は、各要件が持つべき4つのテストカテゴリにクリーンにマッピングされます:

  • ポジティブテスト — 要件が明示的に記述するハッピーパス。
  • ネガティブテスト — 要件が拒否すべきと述べる入力または状態。
  • 境界テスト — 受け入れ基準で言及された範囲、制限、閾値の端点。
  • マイグレーションテスト — 要件に先立つデータまたは状態の動作。古いレコードが新しいルールを静かにバイパスしないようにするため。
要件タイプ 追加すべきテストカテゴリ 一般的な見落とし
「システムはXを拒否する」 ネガティブ 受け入れパスのみがテストされる
「上限はN件」 境界 N-1、N、N+1 が全てカバーされない
「新しいフィールドが古いフィールドを置き換える」 マイグレーション 新しいフィールドのない古いレコードが静かにクラッシュする
「60秒以内」 境界 + タイミング テストは論理はアサートするが、実際の時間予算はアサートしない

このように書かれたユニットテストは、金字塔の高速で低コストなレイヤーとして依然として重要です。それらを構造化する実用的なパターンは、GoユニットテストガイドPythonユニットテストガイド でカバーされています。トレーサビリティがそれに加えるのは、テスト名またはテストコメントに埋め込まれたリテラルで安定した要件トークンです。これにより、後続のクエリは REQ-014 がカバーされていることを推測するのではなく、証明できます。

仕様からコードへのマッピング:設計計画からトレーサビリティテーブルへ

仕様からテストへのマッピングは動作を証明し、仕様からコードへのマッピングは範囲を証明します。それは別の質問に答えます:どのファイルが実際にこの要件のために変更されるべきであり、差分はその境界内に留まったか、それとも関連のないモジュールに溢れたか?

最初に影響を受けるファイルをリストする設計計画(粗いリストでも)は、後で実際のプルリクエストとの差分を取るための基準を提供します。コード内のコメントは、レビュアーが仕様自体から得られない情報を追加する場合にのみ要件IDを参照すべきです。要件テキストをそのまま繰り返すコメントはノイズですが、// enforces REQ-014 boundary: max 5 reset attempts per hour (// REQ-014の境界を強制:1時間あたり最大5回のリセット試行)は、その数値が差分ではそれ以外では不可視であるため、その地位を勝ち取ります。

生成されたトレーサビリティテーブルは、これをレビュアーが両方の文書を並べて読むことで再構築しなければならないものではなく、数秒でレビュー可能なものに変えます:

要件 設計決定 変更ファイル テスト ステータス
REQ-014 ADR-0032 auth/reset.go, auth/reset_test.go test_req_014_* (4) カバー済み
REQ-015 ADR-0032 auth/reset.go なし ギャップ
REQ-016 auth/notify.go test_notify_basic 孤立した仕様リンク

この単一のテーブルは、最も一般的な2つの失敗パターンを一目で浮き彫りにします。REQ-015 はゼロのマッチするテストでコードを変更し、REQ-016 に添付されたテストは実際には要件IDを参照していません。これは、仕様が欠落しているか、テストが誤って分類されたことを意味します。

プルリクエストワークフロー:仕様、コード、テストの差分を一緒にレビューする

トレーサビリティを中心に構築されたプルリクエストは、1つではなく3つの差分を並べてレビューします。仕様の変更、コードの変更、テストの変更です。レビューの質問は「これは正しいように見えるか?」から止まり、はるかに具体的なもの、「この変更はどの要件を満たし、証拠はそれを証明しているか?」へと変わります。

sequenceDiagram participant Dev as 開発者またはエージェント participant PR as プルリクエスト participant CI as CIパイプライン participant Rev as レビュアー Dev->>PR: 仕様差分 + コード差分 + テスト差分を含むPRを開く PR->>CI: トレーサビリティチェックをトリガー CI->>CI: PR記述にREQ-IDが存在するか検証 CI->>CI: 仕様からテストのカバレッジチェックを実行 CI->>CI: 仕様からコードのファイルスコープチェックを実行 CI-->>PR: トレーサビリティレポートをPRコメントとして投稿 Rev->>PR: 「どの要件を満たすか?」に対してレビュー Rev->>PR: 承認または変更をリクエスト

ここでは長いチェックリストよりも短く具体的なレビュアーチェックリストの方が機能します。なぜなら、レビュアーは締切プレッシャーの下で長いチェックリストをスキップするからです:

  1. PR記述は、満たす要件ID(s)を名前として記載しているか?
  2. 変更された全てのファイルは、設計計画の影響ファイルリストに含まれているか、または追加のスコープが説明されているか?
  3. このPRが触れる各要件IDを、少なくとも1つの新規または既存のテストが参照しているか?
  4. 仕様が変わった場合、コードとテストは同じPR内で変更されたか、または追跡されたフォローアップがあるか?

CIでのトレーサビリティの自動化

手動レビューは、レビュアーが探すことを覚えている頻度しか乖離を捕捉できません。そのため、上記のチェックは誰も再読しないwikiページではなくCIに属します。ビルドとテストジョブですでに使用しているのと同じ GitHub Actionsチートシート パターンがここでも直接適用されます — トレーサビリティチェックは、同じパイプラインにおける単なる別のジョブです。

実用的な自動化アイデア(おおよその努力の順序):

  • 仕様ファイルのCIチェック — 同じPRで対応するコードまたはテスト変更なしで仕様ファイルが編集された場合、またはその逆の場合、ビルドを失敗させる。
  • PRタイトルまたは記述での要件IDの必須化 — 軽量な正規表現チェック (REQ-\d+) は、実装するものを名前として指定しないマージをブロックする。
  • エージェント生成のトレーサビリティサマリー — エージェントにPRが触れる要件の短いサマリーを生成させ、人間が書き直すのではなく確認させる。
  • 行ではなく受け入れ基準によるテストカバレッジ — 行カバレッジはコードが実行されたことを教えてくれますが、要件カバレッジは主張がチェックされたことを教えてくれます。
  • 古い仕様の警告 — リンクされたファイルに触れるN回のコミットで触られていない仕様をフラグ立てる。長く静かな仕様は、静かに腐敗している可能性が最も高いものです。

GitHubのSpec Kitの上に構築された拡張機能は、すでにこれらのいくつかを機械的に実装しています。1つは仕様ファイルとテストファイル全体にわたってリテラルな REQ-XXX トークンをスキャンしてマトリックスを構築し、孤立したテストをフラグ立てます。より厳格なVモデル指向のパックはさらに進んで、各開発仕様に対してペアのテスト仕様を生成し、IEC 62304やISO 26262などの規制フレームワーク下で作業するチームのために複数のトレーサビリティマトリックスを生成します。ほとんどのプロジェクトではそのレベルの儀礼は必要ありませんが、根本的なアイデア — 手動で維持するスプレッドシートではなく、スクリプトで生成された決定論的なマトリックス — は、スケールアップと同様にスケールダウンも可能です。

トレーサビリティのためのAIエージェントの使用、オラクルとしては使用しない

AIエージェントはトレーサビリティの機械的な部分に適していますが、要件が実際に満たされたかどうかの最終審判者としては適していません。3つのタスクがエージェントの強みに直接適合します:

  • 仕様と差分の比較 — PRで触られた仕様ファイルで言及された全ての要件、および対応するコードを見つけられなかった全ての要件をリストするようエージェントに依頼する。
  • カバーされていない要件の発見 — テストスイート全体をスキャンし、仕様内のどの要件がトークンを持っていないかをレポートするようエージェントに依頼する。
  • 仕様で記述されていないコードの検出 — 要件を持つモジュールに触れるが、差分内のどの要件IDにも対応していない変更ファイルまたは関数をフラグ立てるようエージェントに依頼する。

警戒すべき失敗モードは、エージェントのサマリーをレビューの起点ではなく真実として信じてしまうことです。エージェントはコメントを誤読し、2つのファイルに跨る要件トークンを逃し、または表面的にしかコードパスを実行しないテストに対してカバレッジを自信を持って宣言する可能性があります。エージェント生成のトレーサビリティレポートはすべて、ジュニアレビュアーのパスを扱うのと同じように扱ってください。有用で速いが、マージをゲートする前に2度目の確認が必要なものとしてです。これは AI駆動開発のための決定記録 に適用されるのと同じ注意です — 記録は、それを書いたエージェント以外のものが最終的にチェックしない限り、信頼できるとは限りません。

コピーできる最小限のトレーサビリティテンプレート

開始するために重量级なフレームワークは必要ありません。記述するコードの横のリポジトリにチェックインされた5ファイルのテンプレートが、本質的な部分をカバーします:

docs/
  requirements.md     # EARSスタイルの受け入れ基準を持つREQ-ID
  design.md           # ADR-ID、影響ファイル、アーキテクチャ決定
  tasks.md            # 1つ以上のREQ-IDにマッピングされたTASK-ID
  tests.md            # どのテストファイル/関数がどのREQ-IDを参照するか
  traceability.md     # 生成されたテーブル:REQ -> ADR -> TASK -> ファイル -> テスト -> PR

requirements.mddesign.mdtasks.md は、仕様駆動開発ワークフロー がすでに記述しているのと同じように、人間とエージェントが共同で書いたり編集したりします。tests.mdtraceability.md は、ジェネレーターがテストディレクトリと仕様ファイル全体で REQ-\d+ をgrepするだけの短いスクリプトであっても、手動で維持せず生成されるべきです。手動で維持されたトレーサビリティテーブル自体が乖離リスクの一種です。なぜなら、誰も締切プレッシャーの下でスプレッドシートを更新しないからです。

結論

仕様駆動開発は、エージェントからコードが出力された瞬間に終わりではありません。コード、テスト、仕様が、PR、リファクタリング、数か月後に到着する要件変更を通じて、互いを誠実に保ち続けるようになったとき、初めて有用になります。6つの平文識別子から構築され、いくつかのCIチェックにより強制され、短いPRチェックリストでレビューされるトレーサビリティモデルは、完全なコンプライアンスフレームワークのオーバーヘッドなしで、利点の大部分を提供します。最小限のテンプレートで始め、最もコストのかからないCIチェック(PR記述内の要件ID)を最初に接続し、その習慣が定着したらトレーサビリティテーブルと古い仕様警告を追加してください。

トレーサビリティは、App Architecture in Production クラスター全体でカバーされているより大きなテストおよびドキュメンテーションの規律の一部であり、エージェントワークフローの標準化を選択するチームのための AI開発ツール クラスターで探求されているツールに関する質問の横に位置しています。

購読する

システム、インフラ、AIエンジニアリングの新記事をお届けします。