前回の概念編では、「仕様」と「振る舞い」の違い、TDD/BDD/DDDとSDDの関係を整理しました。
この実践編では、その内容を 実際に動くコード で一周させます。Hello world を題材に、バックエンド(Flask API)とフロントエンド(画面)を分けたモノレポを作り、pytestとPlaywrightで検証します。
手順を追えばそのまま動きます。各ステップで「これは概念編のどの話か」も都度示します。
この記事で作るもの
番号で言語を選ぶと、その言語で「こんにちは世界」を返すだけの小さなアプリです。機能は2つ。
- 多言語あいさつ:番号(1=英語 / 2=日本語)を選ぶと、対応するあいさつを表示
- あいさつ履歴:取得したあいさつを取得順に一覧表示
見た目はこれだけです。
多言語あいさつ
[日本語] [英語]
あいさつ: こんにちは、世界!
履歴
1. こんにちは、世界!
2. Hello, World!
実用性はありません。狙いは、SDDの一周(仕様 → 実装 → 検証)を、BE/FEを分けた現実的な構成で体感することです。
この記事の主役:「どこをAIに出させ、どこを人間が握るか」
実践編の本題は、コードの書き方そのものではありません。SDDの各ステップで、AIに叩き台を作らせ、人間がレビューで枠を握る —— その分担を、動くコードで体感することです。
まず結論を表で示します。spec / feature / 実装 / テストの4ステップは、すべてAIに叩き台を作らせます。 ただし各段階で人間がレビューして通す。そして唯一、いちばん最初の「何を・なぜ作るか」という仕様の方針だけは、人間が先に決めてからAIに渡します。
| ステップ | 叩き台を作るのは | 人間がやること | 人間がレビューで見る勘所 |
|---|---|---|---|
| 0. 仕様の方針(Why/What) | 人間 | 何を・なぜ作るかを決める | ここがSDDの起点。AIには渡さず人間が口火を切る |
| 1. spec(仕様書) | AI | 方針どおりか確認して通す | Whyとズレてないか/要件に過不足ないか |
| 2. feature(振る舞い) | AI | 具体例が妥当か確認して通す | 境界・例外が網羅されてるか/仕様にない振る舞いを足してないか |
| 3. 実装 | AI | 振る舞いを満たすか確認して通す | featureの各シナリオに対応してるか/余計なロジックがないか |
| 4. テスト | AI | テストが番人として機能するか確認して通す | featureと対応してるか/本当に落ちるべき時に落ちるか |
ポイントは2つです。
① 「叩き台はAI、最終責任は人間のレビュー」。 spec も feature も AI に書かせますが、それを承認するのは人間です。「AIが書いたから正しい」ではなく「人間が読んで通したから先に進む」。各ステップのレビューを飛ばすと、これは単なる丸投げ(Vibe Coding)になります。SDDが「速くても安全」なのは、各段階に人間のゲートがあるからです。
② 唯一、ステップ0だけはAIに振らない。 「何を・なぜ作るか」はプロダクトの意思であって、AIが代わりに決められるものではありません。ここを人間が握るからこそ、以降のAIの叩き台に「正解の方向」が生まれます。逆に言えば、人間が握るべきは最上流の方針と、各ステップの承認 —— この2点です。
📌 概念編との対応:概念編で言った「人間が枠を握り、中身はAIが速く埋める」が、この表の正体です。枠=ステップ0の方針+各ステップのレビュー。中身=AIが出す叩き台。以降、各ステップでこの「プロンプト例(AIに出させる)」と「レビューの勘所(人間が握る)」をセットで示していきます。
全体構成:apps/backend と apps/frontend
最初に完成形のディレクトリを示します。
greeting_demo/
├── specs/ … 仕様(Why/What)。BE/FE共通。方針は人間、文面はAI。
│ ├── spec1_greeting.md
│ └── spec2_history.md
├── features/ … 振る舞い(Gherkin)。AIが叩き台、人間が承認。
│ ├── backend_greeting.feature
│ └── frontend_greeting.feature
└── apps/
├── backend/ … バックエンド(Flask API)
│ ├── greeting.py … ロジック本体
│ ├── app.py … APIサーバー(/greet, /history)
│ └── tests/
│ └── test_greeting.py … ロジックの検証(pytest)
└── frontend/ … フロントエンド(APIを呼ぶ画面)
├── index.html … 画面。fetchでBE APIを叩く
└── tests/
└── test_greeting_ui.py … 画面の検証(Playwright)
層の対応(概念編の振り返り)
概念編で「Gherkinは層ごとに存在する」「BEは"返る"、FEは"表示される"が関心事」という話をしました。それがそのまま構成に表れています。
| 層 | Gherkin | 実装 | テスト | 関心事 |
|---|---|---|---|---|
| BE | backend_greeting.feature | apps/backend/ | pytest | 値が「返る」 |
| FE | frontend_greeting.feature | apps/frontend/ | Playwright | 画面に「表示される」 |
ロジックの真実は BE に一本化し、FE はその API を呼ぶだけにします。こうすると「同じロジックがBEとFEに二重に存在する」状態を避けられます。
流派の確認
概念編の最後で触れた「Gherkinをテストとして実行する流派A(behave)」と「Gherkinは設計・指示に使い、テストはpytest/Playwrightで書く流派B」のうち、この記事は流派Bです。.feature は「何を作るか」の合意とAIへの指示書として置き、実際の検証は実ツール(pytest / Playwright)で書きます。AI駆動開発ではこちらが主流でした。
セットアップ
python -m pip install flask flask-cors pytest playwright pytest-playwright
python -m playwright install chromium
Windows の注意:
python3は Microsoft Store のダミーに化けて動かないことがあるため、pythonを使ってください。
ステップ0:仕様の方針を決める(ここだけ人間が先に)
コードでもプロンプトでもなく、まず人間が「何を・なぜ作るか」を一言で固めます。これが唯一AIに振らない部分です。
今回はこうします。
多言語あいさつ:利用者が番号で言語を選び、その言語の「こんにちは世界」を得られる。対応外の番号は英語をデフォルトにする。
あいさつ履歴:取得したあいさつを取得順に振り返れる。
これだけです。文章としての体裁は問いません。「何を・なぜ」さえ人間が決まっていれば、次のステップ1で、これを正式な spec の文面に起こす作業はAIに任せられます。
📌 概念編との対応:ここがSDDの起点。AIは「与えられた方向に対して速く叩き台を出す」のは得意ですが、「そもそもどっちを向くか」は決められません。だから最上流だけは人間が口火を切ります。
ステップ1:仕様書(spec)をAIに起こさせる
ステップ0で決めた方針を、AIに正式な spec の Markdown に起こさせます。概念編で言う spec(方針の宣言) です。
🤖 AIに渡すプロンプト例
あなたはSDD(仕様駆動開発)の設計を手伝うアシスタントです。
以下の方針から、specファイル(Markdown)を2つ作ってください。
# 方針
- 多言語あいさつ:番号で言語を選び、その言語の「こんにちは世界」を返す。
対応外の番号は英語をデフォルトにする。
- あいさつ履歴:取得したあいさつを取得順に振り返れる。
# 出力ルール
- specには「目的(Why)」と「要件(What)」だけを書く
- 「99を入れたらどうなる」等の具体的な入出力例はまだ書かない(次の工程で固定する)
- 対応言語は表で示す(番号=1:英語=Hello, World! / 番号=2:日本語=こんにちは、世界!)
- ファイルは specs/spec1_greeting.md と specs/spec2_history.md の2つ
出てくる叩き台(例)
specs/spec1_greeting.md:
# 仕様:多言語あいさつ機能
## 目的(Why)
利用者が番号で言語を選び、その言語の「こんにちは世界」を得たい。
## 要件(What)
- 番号を受け取り、対応する言語のあいさつ文字列を返す
- 対応外の番号は、英語をデフォルトとして返す
## 対応言語
| 番号 | 言語 | 文字列 |
|------|--------|---------------------|
| 1 | 英語 | Hello, World! |
| 2 | 日本語 | こんにちは、世界! |
specs/spec2_history.md:
# 仕様:あいさつ履歴機能
## 目的(Why)
利用者が過去に取得したあいさつを後から振り返れるようにしたい。
## 要件(What)
- あいさつを取得するたびに、その文字列を履歴に記録する
- 履歴は取得した順に並ぶ
- 履歴をまとめて取得できる
👀 人間がレビューで確認すべきポイント
- ステップ0のWhyとズレていないか。 AIが気を利かせて「言語追加機能」などを勝手に足していないか。方針にないものは削る。
- 要件(What)に過不足がないか。 「対応外は英語」が落ちていないか。逆に、まだ決めていない仕様を断定で書いていないか。
- 具体例を先取りしていないか。 spec段階で「99なら英語」のような入出力例まで書かれていたら、ここでは消す(次のfeatureで固定する役割分担を崩さないため)。
📌 概念編との対応:spec を AI に書かせても、Why に責任を持つのは人間です。ここで通した文面が、以降すべての叩き台の「正解の出どころ」になります。
ステップ2:振る舞い(feature)をAIに書かせる
通した spec を、Given / When / Then で 具体例に固定します。これが概念編で言う 振る舞い であり、流派Bでは「AIへの指示書 / チームの合意」として機能します。これもAIに叩き台を作らせます。
🤖 AIに渡すプロンプト例
承認済みのspec(spec1_greeting.md / spec2_history.md)を添付します。
このspecを、Gherkin(.feature)の具体例に落としてください。
# ルール
- BEとFEで別ファイルにする
- features/backend_greeting.feature … APIの振る舞い(値が「返る」)
- features/frontend_greeting.feature … 画面の振る舞い(「表示される」)
- specの要件を、境界・例外を含む具体的なGiven/When/Thenにする
(対応外の番号=99、履歴の順序など)
- specに書いていない振る舞いは追加しない
- 日本語で書く
出てくる叩き台(例)
features/backend_greeting.feature(BE:APIの振る舞い):
Feature: あいさつAPI
Scenario: 日本語を取得する
Given APIが起動している
When POST /greet に番号 2 を送る
Then "こんにちは、世界!" が返る
Scenario: 対応外はデフォルト英語
Given APIが起動している
When POST /greet に番号 99 を送る
Then "Hello, World!" が返る
Scenario: 履歴が取得順に並ぶ
Given APIが起動している
When 番号 2、番号 1 の順であいさつを取得する
Then GET /history は ["こんにちは、世界!", "Hello, World!"] を返す
features/frontend_greeting.feature(FE:画面の振る舞い):
Feature: あいさつ画面
Scenario: 日本語ボタンを押すとあいさつが表示される
Given あいさつ画面を開いている
When 「日本語」ボタンを押す
Then 画面に "こんにちは、世界!" が表示される
Scenario: 押したあいさつが履歴に並ぶ
Given あいさつ画面を開いている
When 「日本語」ボタンを押す
And 「英語」ボタンを押す
Then 履歴欄に "こんにちは、世界!" と "Hello, World!" がこの順で並ぶ
👀 人間がレビューで確認すべきポイント
- 境界・例外が網羅されているか。 「対応外の番号」「履歴の順序」のような落としやすいケースがシナリオになっているか。AIは正常系だけ出して例外を落とすことがある。
- spec にない振る舞いを足していないか。 AIが「親切に」エラーメッセージ仕様やバリデーションを盛ってくることがある。spec の範囲に戻す。
- BEとFEの関心事が混ざっていないか。 BEは「返る」、FEは「表示される」。BEのfeatureに画面の話が紛れ込んでいたら切り分ける。
📌 概念編との対応:BEは「返る」、FEは「表示される」。同じHelloWorldでも、層が違えば関心事が違うので、Gherkinも別々に書きます。FEのGherkinはBEの書き直しではなく「追加」です。AIが叩き台を出しても、この層の切り分けが正しいかを見るのは人間です。
ここまでで 承認済みの仕様+振る舞い が揃いました。次はこれをAIに渡して実装とテストの叩き台を出させます。
ステップ3:バックエンドを実装する(AIに叩き台→レビュー)
実装もAIに任せます。承認済みの spec と feature を渡し、それを満たすコードを書かせます。
🤖 AIに渡すプロンプト例
承認済みのspecとfeature(backend_greeting.feature)を添付します。
これを満たすバックエンドを実装してください。
# 構成
- apps/backend/greeting.py … あいさつのロジック(GreetingServiceクラス)
- apps/backend/app.py … Flask APIサーバー(POST /greet, GET /history)
- FE(別ポート)から呼ぶのでCORSを許可
- ポートは5001
# 制約
- featureにあるシナリオを満たすことだけを実装する(余計な機能を足さない)
- ロジック(greeting.py)とサーバー(app.py)は分ける
出てくる叩き台(例)
apps/backend/greeting.py:
"""あいさつのロジック(仕様1・仕様2)"""
GREETINGS = {1: "Hello, World!", 2: "こんにちは、世界!"}
DEFAULT = "Hello, World!"
class GreetingService:
def __init__(self):
self._history = []
def greet(self, number: int) -> str:
message = GREETINGS.get(number, DEFAULT) # 仕様1:対応外はデフォルト英語
self._history.append(message) # 仕様2:取得順に記録
return message
def history(self) -> list:
return self._history
apps/backend/app.py:
"""あいさつAPIサーバー(Flask)"""
from flask import Flask, request, jsonify
from flask_cors import CORS
from greeting import GreetingService
app = Flask(__name__)
CORS(app) # FE(別ポート)からの呼び出しを許可
service = GreetingService()
@app.route("/greet", methods=["POST"])
def greet():
number = request.json.get("number")
message = service.greet(number)
return jsonify({"message": message})
@app.route("/history", methods=["GET"])
def history():
return jsonify({"history": service.history()})
if __name__ == "__main__":
app.run(port=5001)
👀 人間がレビューで確認すべきポイント
-
featureの各シナリオに対応しているか。 「対応外はデフォルト英語」が
GREETINGS.get(number, DEFAULT)で実現されているか。シナリオを満たさないコードは戻す。 - 余計なロジックを足していないか。 AIが入力バリデーション、ログ、例外ハンドリングなどをfeatureにない範囲で盛ることがある。今回のスコープでは削る(必要なら別途specに起こす)。
- ロジックとサーバーが分かれているか。 テストしやすさのため、ロジック(greeting.py)がFlaskなしでもテストできるようになっているかを見る。
バックエンドのテスト(pytest)もAIに
apps/backend/tests/test_greeting.py。backend_greeting.feature の各シナリオに 対応させて 書かせます。
🤖 AIに渡すプロンプト例
backend_greeting.feature と greeting.py を添付します。
featureの各シナリオに1対1で対応するpytestを書いてください。
# ルール
- テスト関数名は、対応するシナリオが一目で分かる日本語にする
- 1シナリオ = 1テスト関数
- Flaskを起動せず、GreetingServiceを直接呼んで検証する
出てくる叩き台(例)
"""backend_greeting.feature に対応するロジックのテスト(pytest)"""
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from greeting import GreetingService
def test_日本語を取得する():
s = GreetingService()
assert s.greet(2) == "こんにちは、世界!"
def test_対応外はデフォルト英語():
s = GreetingService()
assert s.greet(99) == "Hello, World!"
def test_履歴が取得順に並ぶ():
s = GreetingService()
s.greet(2)
s.greet(1)
assert s.history() == ["こんにちは、世界!", "Hello, World!"]
検証します。
cd apps/backend
python -m pytest tests/ -v
tests/test_greeting.py::test_日本語を取得する PASSED
tests/test_greeting.py::test_対応外はデフォルト英語 PASSED
tests/test_greeting.py::test_履歴が取得順に並ぶ PASSED
====== 3 passed ======
👀 人間がレビューで確認すべきポイント
- feature と1対1で対応しているか。 3シナリオに対して3テスト。シナリオがあるのにテストがない、という抜けが一番危ない。
- テストが「番人」になっているか。 assert が緩すぎて、間違った実装でも通ってしまわないか。後の「ステップ6」でわざと壊して確かめます。
- AIが実装に合わせてテストを甘くしていないか。 AIに実装とテストを同時に書かせると、「実装の現状を肯定するだけのテスト」になることがある。基準は実装ではなく feature であることを確認する。
📌 概念編との対応:この pytest が、概念編で言った「検証の番人」です。
.featureとテストは機械的には繋がっておらず、「同じ振る舞いを満たすように対応させる」のは人間とAIの責任です(流派B)。AIに書かせるからこそ、対応の正しさを人間が見る意味があります。
ステップ4:フロントエンドを実装する(AIに叩き台→レビュー)
FEはロジックを持たず、BEのAPIを fetch で呼びます。これが「ロジックの真実をBEに一本化」の実体です。これもAIに叩き台を出させます。
🤖 AIに渡すプロンプト例
frontend_greeting.feature と、BEのAPI仕様(POST /greet, GET /history, ポート5001)
を添付します。これを満たす画面を実装してください。
# ルール
- apps/frontend/index.html の1ファイル
- ロジックはFEに持たせない。BEのAPIをfetchで呼ぶだけにする
- 「日本語」「英語」ボタン、あいさつ表示欄、履歴一覧を置く
- featureの「表示される」「履歴順に並ぶ」を満たす
出てくる叩き台(例)
apps/frontend/index.html:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>あいさつ画面</title>
</head>
<body>
<h1>多言語あいさつ</h1>
<button data-number="2">日本語</button>
<button data-number="1">英語</button>
<p>あいさつ: <span id="greeting"></span></p>
<h2>履歴</h2>
<ul id="history"></ul>
<script>
// BEのAPIを叩く(ロジックはBEに一本化。FEは呼ぶだけ)
const API = "http://localhost:5001";
async function greet(number) {
const res = await fetch(`${API}/greet`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ number })
});
const data = await res.json();
document.getElementById("greeting").textContent = data.message;
await renderHistory();
}
async function renderHistory() {
const res = await fetch(`${API}/history`);
const data = await res.json();
const ul = document.getElementById("history");
ul.innerHTML = "";
data.history.forEach(item => {
const li = document.createElement("li");
li.textContent = item;
ul.appendChild(li);
});
}
document.querySelectorAll("button").forEach(btn => {
btn.addEventListener("click", () => greet(Number(btn.dataset.number)));
});
</script>
</body>
</html>
👀 人間がレビューで確認すべきポイント
- FEにロジックが漏れていないか。 AIが気を利かせて、FE側で番号→文字列の対応表を持ってしまうことがある。これをやると「真実がBEとFEに二重化」して、ロジックの一本化が崩れる。あいさつ文字列はBEのレスポンスをそのまま表示しているか。
-
featureの画面要素が揃っているか。 ボタン、表示欄、履歴一覧。Playwrightが触る要素(
data-number属性など)があるか。 - APIの叩き先が合っているか。 ポート5001、エンドポイント名がBEと一致しているか。
フロントエンドのテスト(Playwright)もAIに
apps/frontend/tests/test_greeting_ui.py。テストが内部でBE APIと画面サーバーを起動し、ブラウザでボタンを操作して表示を検証します。
🤖 AIに渡すプロンプト例
frontend_greeting.feature と index.html を添付します。
featureを満たすE2Eテストを Playwright(pytest)で書いてください。
# ルール
- テスト内でBE API(app.py)と画面用の簡易HTTPサーバーを起動する
- ブラウザでボタンを押し、あいさつ表示と履歴順を検証する
- Windowsでの連続実行に備え、ポート再利用(allow_reuse_address)を入れる
- 終了時にBEプロセスとサーバーを確実に止める
出てくる叩き台(例)
"""
frontend_greeting.feature に対応するE2Eテスト(Playwright)。
BE APIを起動 → 画面を開く → ボタン操作 → 表示を検証。
"""
import os, sys, time, subprocess, http.server, socketserver, threading
from playwright.sync_api import sync_playwright
BACKEND = os.path.join(os.path.dirname(__file__), "..", "..", "backend")
FRONTEND = os.path.join(os.path.dirname(__file__), "..")
class _Server(socketserver.TCPServer):
allow_reuse_address = True # 連続実行でポートが塞がるのを防ぐ
def _start_backend():
proc = subprocess.Popen(
[sys.executable, "app.py"], cwd=os.path.abspath(BACKEND),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
time.sleep(2) # 起動待ち
return proc
def _start_frontend():
os.chdir(os.path.abspath(FRONTEND))
handler = http.server.SimpleHTTPRequestHandler
httpd = _Server(("", 8000), handler)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
return httpd
def test_画面操作であいさつと履歴が出る():
backend = _start_backend()
frontend = _start_frontend()
try:
with sync_playwright() as p:
page = p.chromium.launch().new_page()
page.goto("http://localhost:8000/index.html") # Given 画面を開く
page.click("button[data-number='2']") # When 日本語
page.wait_for_function(
"document.querySelector('#greeting').textContent === 'こんにちは、世界!'"
)
assert page.inner_text("#greeting") == "こんにちは、世界!" # Then 表示
page.click("button[data-number='1']") # And 英語
page.wait_for_function(
"document.querySelectorAll('#history li').length === 2"
)
items = page.locator("#history li").all_inner_texts()
assert items == ["こんにちは、世界!", "Hello, World!"] # Then 履歴順
finally:
backend.terminate()
frontend.shutdown()
frontend.server_close()
検証します。
cd apps/frontend
python -m pytest tests/ -v
tests/test_greeting_ui.py::test_画面操作であいさつと履歴が出る PASSED
====== 1 passed ======
👀 人間がレビューで確認すべきポイント
- featureのシナリオを実際に再現しているか。 「日本語→英語の順で押す→履歴がこの順」というfeatureの流れと、テストの操作順が一致しているか。
-
待ち合わせが安定しているか。
wait_for_functionで表示完了を待たずにassertしていると、たまに落ちる不安定テストになる。AIは固定のsleepで済ませがちなので、条件待ちになっているか見る。 -
後始末が確実か。 BEプロセスとサーバーが
finallyで必ず止まるか。ここが甘いと2回目の実行でポートが埋まる。
📌 概念編との対応:BEのpytestと違い、FEは実際にブラウザでボタンを押す E2E(通し検証) です。テストピラミッドの考え方どおり、FEはBEほど網羅せず、主要な画面操作1本に絞っています。検証ツールが pytest から Playwright に変わっただけで、「Gherkinは指示・テストは実ツールで書く」という流派Bの構図は同じです。
ステップ5:起動して動かす
テストだけでなく、実際にブラウザで触ってみます。ターミナルを 2つ 使います。
1. バックエンドを起動(ターミナル1)
cd apps/backend
python app.py
→ http://localhost:5001 で API が起動。このターミナルは起動したままにします。
動作確認(別ターミナルで1行で):
curl -X POST http://localhost:5001/greet -H "Content-Type: application/json" -d "{\"number\":2}"
# => {"message":"こんにちは、世界!"}
2. フロントエンドを起動(ターミナル2)
cd apps/frontend
python -m http.server 8000
→ ブラウザで http://localhost:8000/index.html を開く。日本語/英語ボタンを押すと、BE APIを呼んであいさつと履歴が表示されます。
⚠️ BEが起動していないと、ボタンを押しても何も表示されません。FEはBEのAPIに依存しているためです。これも「層が分かれている」ことの実感に繋がります。
ステップ6:番人を壊して体感する
ここがSDDの肝です。わざとバグを入れて、テストが捕まえることを確認します。AIに書かせたテストが本当に番人として機能するかを、人間が最後に確かめる工程でもあります。
BEのロジックを壊す
apps/backend/greeting.py の append を insert(0) に変えます(履歴の順序が逆になる)。
self._history.insert(0, message) # append から変更(バグ)
BEのテストを実行:
cd apps/backend
python -m pytest tests/ -v
tests/test_greeting.py::test_履歴が取得順に並ぶ FAILED
期待=['こんにちは、世界!', 'Hello, World!']
実際=['Hello, World!', 'こんにちは、世界!']
仕様2の「履歴は取得した順に並ぶ」という方針が破られた瞬間、テストが FAILED で捕まえました。直せばまた通ります。
FEを壊す
同様に apps/frontend/index.html の data.history.forEach(...) の前に data.history.reverse(); を入れると、画面の履歴順が崩れ、Playwrightのテストが落ちます。
📌 概念編との対応:BEのバグはpytestが、FEのバグはPlaywrightが捕まえる。層ごとに番人がいるということです。AIに実装を任せても、各層の番人が仕様からのズレを弾いてくれる —— これが「速く生成して、速く検証して、間違いを弾く」の実体です。そして「その番人自体が本物か」を、わざと壊して人間が確認する。ここまでやって初めて、AIに任せた一周が信頼できるものになります。
まとめ:この型がAI駆動で効く理由
作ったものを振り返ると、SDDの一周がBE/FEそれぞれに対して成立していました。そして注目すべきは、spec / feature / 実装 / テストの叩き台はすべてAIが出していることです。
方針(Why/What) ── 人間が「何を・なぜ」を決める(ここだけ人間が先に)
↓
仕様(specs/) ──── AIが叩き台 → 人間がレビューで承認
↓
振る舞い(features/) ─ AIが叩き台 → 人間がレビューで承認
↓
実装(apps/) ───── AIが叩き台 → 人間がレビューで承認
↓
検証(pytest / Playwright) ─ AIが叩き台 → 人間がレビューで承認
↑__________ どの段階でも、ズレたら方針に戻る
「AIに任せる」とは「丸投げ」ではありません。4ステップすべてでAIが叩き台を出し、人間が各段階のレビューで通す。 人間が握るのは、最上流の「何を・なぜ作るか」という方針と、各ステップの承認です。この2点を握っているから、AIに大量に書かせても安全に弾ける。
Vibe Codingのように「祈りながら丸投げ」するのと、SDDの決定的な違いはここにあります。Vibe Codingは枠も番人もなくAIに全部委ねる。SDDは、方針と振る舞いで枠を立て、各層に番人を置いてからAIに速く埋めさせ、人間が各段で承認する。出力の速さはAIに、正しさの判断は人間に —— この分業が、概念編で語った「AIならSDD」を実際に動く形にしたものです。
⚠️ 誤解しないでほしいこと:「spec も feature も AI が書くなら、結局全部AI任せでは?」と思うかもしれません。逆です。AIが書くからこそ、人間のレビューが各段階に要るのです。叩き台を出すのはAI、最終責任を負うのは人間のレビュー。この非対称が崩れて「AIが書いたから通す」になった瞬間、それはSDDではなくただのVibe Codingに戻ります。
Hello world という最小の題材でも、BE/FEを分けて一周させると、この構造が体で分かります。次は自分の実務の小さな機能で、まず「何を・なぜ作るか」を人間が一言決め、その先のspec起こしからAIに振ってみるところから始めてみてください。
この記事は概念編の続きです。誤りや改善点があればコメントで指摘いただけると嬉しいです。
