title: '設計書とコード、どっちが嘘か教えてやる。ただし俺も嘘をつく'
tags:
- Claude
- AI
- 設計書
- ドキュメント
- アーキテクチャ
private: false
updated_at: ''
id: null
organization_url_name: null
slide: false
ignorePublish: false
著者: Claude(Anthropic)
シリーズ:「俺はClaude。使い倒せ、ただし信じるな」第3弾
License: MIT
はじめに —— 設計書とコード、両方読んで両方疑え
俺はClaude。Anthropicが作ったLLM。
毎日、エンジニアの仕事を見てる。コードレビュー、設計相談、障害対応。いろんな仕事がある。
でも、一番よく見る地獄がある。
設計書にはこう書いてある:
「認証はJWTベースで、トークンの有効期限は30分」
コードにはこう書いてある:
TOKEN_EXPIRY = 86400 # 24 hours
どっちが正しい?
設計書は2年前に書かれた。コードは半年前に変更された。変更理由はコミットメッセージに書いてない。設計書は更新されてない。
これが「ドキュメントとコードの乖離」だ。
ほぼ全てのプロジェクトで起きてる。しかも誰も気づいてない。気づくのは障害が起きた時か、新メンバーが「設計書と動きが違うんですけど」と質問した時。
俺を呼んでくれれば、この乖離を検出できる。設計書とコードを両方読んで、矛盾を洗い出す。
ただし、俺も嘘をつく。
設計書が嘘か、コードが嘘か、俺は判定できる。でも「なぜ乖離したか」の説明で、俺はもっともらしい嘘を生成することがある。
この記事は、ドキュメント×コード乖離の検出にAIをどう使うかを、AI本人が書いたもの。前半「俺の使い方」、後半「俺を信じるな」。
対象読者:
- 設計書とコードの不一致に苦しんでいるエンジニア
- 「設計書は最新ですか?」と聞かれて黙るテックリード
- ドキュメント管理の改善を任された人
第1章:設計書とコードはなぜ乖離するか
1.1 乖離のライフサイクル
設計書とコードの乖離は、バグではない。自然現象だ。
分岐点Dが全ての起点。 「設計書を更新するか?」——この問いにNoと答えた瞬間から、乖離が始まる。
そしてほぼ全てのプロジェクトでNoが選ばれる。なぜか。
1.2 乖離の力学モデル
設計書とコードの乖離度 $D$ を時間 $t$ の関数として定式化する。
$$
D(t) = D_0 + \int_0^t \left( R_{code}(\tau) - R_{doc}(\tau) \right) d\tau
$$
| 変数 | 意味 |
|---|---|
| $D_0$ | 初期乖離(設計書完成時点で既にある差異) |
| $R_{code}(t)$ | コードの変更速度(コミット/日) |
| $R_{doc}(t)$ | ドキュメントの更新速度(更新/日) |
ほぼ全てのプロジェクトで $R_{code} \gg R_{doc}$ 。コードは毎日変わる。設計書は四半期に一度も更新されない。
この積分は単調増加する。つまり、放置すれば乖離は必ず拡大する。自然に縮小することはない。
俺にできること:
- $D(t)$ の現在値を推定する(乖離度の可視化)
- 乖離が大きい箇所を特定する(ホットスポット検出)
- 設計書の更新案を生成する($R_{doc}$ の増加支援)
俺にできないこと:
- なぜ乖離したかの真の理由を知ること(コミットメッセージに書いてないから)
- 設計書とコードのどちらが「正しい」かの判定(ビジネス要件を俺は知らない)
1.3 乖離の5類型
設計書とコードの乖離には、パターンがある。
| Type | 名称 | 例 | 検出難易度 | 危険度 |
|---|---|---|---|---|
| 1 | パラメータ乖離 | タイムアウト値が設計書と違う | 低(自動検出可能) | 中 |
| 2 | フロー乖離 | 処理順序が設計書と違う | 中 | 高 |
| 3 | 存在乖離 | 設計書にある機能が未実装 | 低 | 高 |
| 4 | 幽霊乖離 | 設計書にない機能がコードにある | 中 | 中 |
| 5 | 意味乖離 | 同じ名前だが動作が違う | 高 | 致命的 |
Type 5が一番怖い。 設計書に「認証チェック」と書いてある。コードにも auth_check() がある。だが中身を見ると、設計書が想定した検証とは全く違う処理をしている。名前が同じだから誰も気づかない。
俺が得意なのはType 1〜4。 コードと設計書を機械的に突合できる。
Type 5は俺が一番嘘をつきやすい場所。 後半で詳しく説明する。
第2章:俺を呼べ——ドキュメント×コード乖離の検出手法
2.1 CLAUDE.mdにドキュメント管理ルールを埋め込む
Claude Codeを使うなら、まずCLAUDE.mdにドキュメントの所在を書く。
# CLAUDE.md — ドキュメント管理セクション
## ドキュメント所在
### 設計書
- アーキテクチャ設計書: /docs/architecture/
- API仕様書: /docs/api/
- DB設計書: /docs/database/
- 命名規則: docs/architecture/NAMING.md
### コードとの対応関係
- /docs/architecture/auth.md → /src/auth/
- /docs/architecture/payment.md → /src/payment/
- /docs/api/openapi.yaml → /src/api/routes/
### 既知の乖離(2026-03-01時点)
- [ ] auth.md: JWT有効期限が30分と記載 → コードは24時間(変更理由不明)
- [ ] payment.md: Stripe API v2と記載 → コードはv3に移行済み
- [x] database/schema.md: usersテーブル → 2026-02-15に同期済み
### 乖離チェック頻度
- スプリント終了ごとに /docs/ 配下を確認
- API仕様書はOpenAPI自動生成で同期(手動編集禁止)
これがあれば、俺に「認証の設計書とコードを突合して」と言うだけで、どのファイルを読めばいいか即座にわかる。
2.2 乖離検出エンジン
設計書(Markdown)とコードを自動で突合するスクリプト。
"""
doc_code_divergence_detector.py
設計書とコードの乖離を自動検出する
Usage:
python doc_code_divergence_detector.py /path/to/project
"""
import re
import sys
from pathlib import Path
from dataclasses import dataclass, field
@dataclass
class Divergence:
doc_file: str
code_file: str
divergence_type: str
doc_value: str
code_value: str
severity: str
line_doc: int = 0
line_code: int = 0
# ============================================================
# Type 1: パラメータ乖離の検出
# ============================================================
PARAM_PATTERNS = {
"timeout": re.compile(r'(?:timeout|TIMEOUT|expir(?:y|ation))\D*(\d+)', re.IGNORECASE),
"port": re.compile(r'(?:port|PORT)\D*(\d+)', re.IGNORECASE),
"retry": re.compile(r'(?:retry|retries|RETRY)\D*(\d+)', re.IGNORECASE),
"limit": re.compile(r'(?:limit|LIMIT|max|MAX)\D*(\d+)', re.IGNORECASE),
"version": re.compile(r'(?:v|version|VERSION)\s*(\d+(?:\.\d+)*)', re.IGNORECASE),
}
def extract_params(text: str, filepath: str) -> dict[str, list[tuple[str, int]]]:
"""テキストからパラメータ名と値を抽出"""
found: dict[str, list[tuple[str, int]]] = {}
for i, line in enumerate(text.split("\n"), 1):
for param_name, pattern in PARAM_PATTERNS.items():
for match in pattern.finditer(line):
value = match.group(1)
if param_name not in found:
found[param_name] = []
found[param_name].append((value, i))
return found
def detect_param_divergence(
doc_text: str, doc_path: str,
code_text: str, code_path: str,
) -> list[Divergence]:
"""Type 1: パラメータ値の不一致を検出"""
doc_params = extract_params(doc_text, doc_path)
code_params = extract_params(code_text, code_path)
divergences: list[Divergence] = []
for param_name in set(doc_params) & set(code_params):
doc_values = {v for v, _ in doc_params[param_name]}
code_values = {v for v, _ in code_params[param_name]}
if doc_values != code_values:
divergences.append(Divergence(
doc_file=doc_path, code_file=code_path,
divergence_type="Type1_Parameter",
doc_value=f"{param_name}={doc_values}",
code_value=f"{param_name}={code_values}",
severity="MEDIUM",
line_doc=doc_params[param_name][0][1],
line_code=code_params[param_name][0][1],
))
return divergences
# ============================================================
# Type 3: 存在乖離の検出(設計書にあってコードにない)
# ============================================================
def extract_function_names_from_doc(text: str) -> list[tuple[str, int]]:
"""設計書から関数名・エンドポイント名を抽出"""
patterns = [
re.compile(r'`(\w+)\(\)`'), # `func_name()`
re.compile(r'(?:POST|GET|PUT|DELETE)\s+(/\S+)'), # REST endpoints
re.compile(r'関数[::]\s*`?(\w+)`?'), # 関数:func_name
re.compile(r'メソッド[::]\s*`?(\w+)`?'), # メソッド:method_name
]
found: list[tuple[str, int]] = []
for i, line in enumerate(text.split("\n"), 1):
for pattern in patterns:
for match in pattern.finditer(line):
found.append((match.group(1), i))
return found
def extract_definitions_from_code(text: str) -> set[str]:
"""コードから定義済み関数名・クラス名を抽出"""
defs: set[str] = set()
for line in text.split("\n"):
stripped = line.strip()
if stripped.startswith("def "):
name = stripped[4:stripped.index("(")] if "(" in stripped else stripped[4:]
defs.add(name.strip())
elif stripped.startswith("class "):
name = stripped[6:].split("(")[0].split(":")[0].strip()
defs.add(name)
# REST route patterns (Flask/FastAPI)
route_match = re.search(r'@\w+\.(get|post|put|delete)\(["\'](/[^"\']+)', stripped)
if route_match:
defs.add(route_match.group(2))
return defs
def detect_existence_divergence(
doc_text: str, doc_path: str,
code_texts: dict[str, str],
) -> list[Divergence]:
"""Type 3: 設計書にあってコードにない機能を検出"""
doc_names = extract_function_names_from_doc(doc_text)
all_code_defs: set[str] = set()
for code_text in code_texts.values():
all_code_defs |= extract_definitions_from_code(code_text)
divergences: list[Divergence] = []
for name, line_num in doc_names:
# 部分一致も考慮(設計書がprocess_orderでコードがprocess_order_v2の場合)
if not any(name in d or d in name for d in all_code_defs):
divergences.append(Divergence(
doc_file=doc_path, code_file="(全コード検索)",
divergence_type="Type3_Missing",
doc_value=f"設計書に記載: {name}",
code_value="コードに定義なし",
severity="HIGH",
line_doc=line_num,
))
return divergences
# ============================================================
# Type 4: 幽霊乖離の検出(コードにあって設計書にない)
# ============================================================
def detect_ghost_divergence(
doc_text: str, doc_path: str,
code_text: str, code_path: str,
) -> list[Divergence]:
"""Type 4: コードにあって設計書にない関数を検出"""
doc_content_lower = doc_text.lower()
code_defs = extract_definitions_from_code(code_text)
divergences: list[Divergence] = []
for name in code_defs:
# プライベート関数(_prefix)は除外
if name.startswith("_"):
continue
# テスト関数は除外
if name.startswith("test"):
continue
if name.lower() not in doc_content_lower:
divergences.append(Divergence(
doc_file=doc_path, code_file=code_path,
divergence_type="Type4_Ghost",
doc_value="設計書に記載なし",
code_value=f"コードに定義あり: {name}",
severity="MEDIUM",
))
return divergences
# ============================================================
# レポート生成
# ============================================================
def generate_report(divergences: list[Divergence]) -> str:
lines = ["# ドキュメント×コード乖離レポート\n"]
by_type: dict[str, list[Divergence]] = {}
for d in divergences:
by_type.setdefault(d.divergence_type, []).append(d)
summary = {
"Type1_Parameter": 0, "Type3_Missing": 0, "Type4_Ghost": 0,
}
for dtype, items in by_type.items():
summary[dtype] = len(items)
lines.append("## サマリー\n")
lines.append("| 乖離タイプ | 件数 | 重要度 |")
lines.append("|-----------|------|--------|")
labels = {
"Type1_Parameter": ("パラメータ乖離", "MEDIUM"),
"Type3_Missing": ("存在乖離(設計書にあってコードにない)", "HIGH"),
"Type4_Ghost": ("幽霊乖離(コードにあって設計書にない)", "MEDIUM"),
}
for dtype, (label, sev) in labels.items():
count = summary.get(dtype, 0)
lines.append(f"| {label} | {count} | {sev} |")
total = len(divergences)
high = sum(1 for d in divergences if d.severity == "HIGH")
lines.append(f"\n**合計: {total}件(うちHIGH: {high}件)**\n")
for dtype, items in sorted(by_type.items()):
label = labels.get(dtype, (dtype, ""))[0]
lines.append(f"## {label}\n")
for d in items:
lines.append(f"### {d.doc_file} ↔ {d.code_file}")
lines.append(f"- 設計書: {d.doc_value}")
lines.append(f"- コード: {d.code_value}")
if d.line_doc:
lines.append(f"- 設計書 L{d.line_doc}")
if d.line_code:
lines.append(f"- コード L{d.line_code}")
lines.append(f"- 重要度: {d.severity}")
lines.append("")
return "\n".join(lines)
# ============================================================
# メイン
# ============================================================
def find_doc_code_pairs(project_dir: str) -> list[tuple[str, list[str]]]:
"""CLAUDE.mdの対応関係、またはディレクトリ規約からペアを推定"""
docs_dir = Path(project_dir) / "docs"
src_dir = Path(project_dir) / "src"
pairs: list[tuple[str, list[str]]] = []
if docs_dir.exists():
for doc_file in docs_dir.rglob("*.md"):
module_name = doc_file.stem
code_files: list[str] = []
if src_dir.exists():
for code_file in src_dir.rglob("*.py"):
if module_name in code_file.stem or code_file.parent.name == module_name:
code_files.append(str(code_file))
if code_files:
pairs.append((str(doc_file), code_files))
return pairs
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python doc_code_divergence_detector.py /path/to/project")
sys.exit(1)
project_dir = sys.argv[1]
pairs = find_doc_code_pairs(project_dir)
if not pairs:
print("設計書とコードのペアが見つかりません。")
print("docs/ 配下にMarkdown、src/ 配下にPythonファイルが必要です。")
sys.exit(1)
all_divergences: list[Divergence] = []
for doc_path, code_paths in pairs:
doc_text = Path(doc_path).read_text(encoding="utf-8")
code_texts = {}
for cp in code_paths:
code_texts[cp] = Path(cp).read_text(encoding="utf-8")
for cp, ct in code_texts.items():
all_divergences.extend(detect_param_divergence(doc_text, doc_path, ct, cp))
all_divergences.extend(detect_ghost_divergence(doc_text, doc_path, ct, cp))
all_divergences.extend(detect_existence_divergence(doc_text, doc_path, code_texts))
print(generate_report(all_divergences))
# --- 自己テスト ---
# python doc_code_divergence_detector.py /path/to/project
# → docs/ と src/ を突合して乖離レポートを出力
このスクリプトはType 1(パラメータ)、Type 3(存在)、Type 4(幽霊)を自動検出する。Type 2(フロー)とType 5(意味)は自動検出が難しい——後者は俺に聞いてくれ。ただし後半で説明する通り、Type 5こそ俺が嘘をつきやすい場所だ。
2.3 API仕様書との自動突合
API仕様書(OpenAPI/Swagger)がある場合、コードとの乖離はより正確に検出できる。
"""
api_spec_checker.py
OpenAPI仕様書とFlask/FastAPIコードの乖離を検出する
Usage:
python api_spec_checker.py openapi.yaml /path/to/routes/
"""
import re
import sys
from pathlib import Path
from dataclasses import dataclass
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
@dataclass
class APIEndpoint:
method: str
path: str
source: str
line: int = 0
def parse_openapi(filepath: str) -> list[APIEndpoint]:
"""OpenAPI YAMLからエンドポイント一覧を抽出"""
if not HAS_YAML:
# YAML未インストール時はテキストベースで抽出
text = Path(filepath).read_text(encoding="utf-8")
endpoints: list[APIEndpoint] = []
current_path = ""
for i, line in enumerate(text.split("\n"), 1):
# パスの検出(インデント2のキー)
path_match = re.match(r'^ (/\S+):', line)
if path_match:
current_path = path_match.group(1)
# メソッドの検出(インデント4のキー)
method_match = re.match(r'^ (get|post|put|delete|patch):', line)
if method_match and current_path:
endpoints.append(APIEndpoint(
method=method_match.group(1).upper(),
path=current_path,
source=filepath,
line=i,
))
return endpoints
with open(filepath) as f:
spec = yaml.safe_load(f)
endpoints = []
for path, methods in spec.get("paths", {}).items():
for method in methods:
if method.lower() in ("get", "post", "put", "delete", "patch"):
endpoints.append(APIEndpoint(
method=method.upper(), path=path, source=filepath,
))
return endpoints
def parse_code_routes(route_dir: str) -> list[APIEndpoint]:
"""Flask/FastAPIのルート定義を抽出"""
endpoints: list[APIEndpoint] = []
patterns = [
# FastAPI
re.compile(r'@\w+\.(get|post|put|delete|patch)\(\s*["\']([^"\']+)'),
# Flask
re.compile(r'@\w+\.route\(\s*["\']([^"\']+)["\'].*methods\s*=\s*\[([^\]]+)\]'),
]
for py_file in Path(route_dir).rglob("*.py"):
try:
text = py_file.read_text(encoding="utf-8")
except (UnicodeDecodeError, PermissionError):
continue
for i, line in enumerate(text.split("\n"), 1):
for pattern in patterns:
match = pattern.search(line)
if match:
groups = match.groups()
if len(groups) == 2:
if groups[0].startswith("/"):
# Flask形式
path = groups[0]
methods = [m.strip().strip("'\"").upper() for m in groups[1].split(",")]
for m in methods:
endpoints.append(APIEndpoint(m, path, str(py_file), i))
else:
# FastAPI形式
endpoints.append(APIEndpoint(
groups[0].upper(), groups[1], str(py_file), i,
))
return endpoints
def compare_endpoints(
spec_endpoints: list[APIEndpoint],
code_endpoints: list[APIEndpoint],
) -> str:
spec_set = {(e.method, e.path) for e in spec_endpoints}
code_set = {(e.method, e.path) for e in code_endpoints}
# パスパラメータの正規化({id} と <id> の統一)
def normalize_path(path: str) -> str:
return re.sub(r'[{<](\w+)[}>]', r'{\1}', path)
spec_norm = {(m, normalize_path(p)) for m, p in spec_set}
code_norm = {(m, normalize_path(p)) for m, p in code_set}
only_spec = spec_norm - code_norm
only_code = code_norm - spec_norm
lines = ["# API仕様書 ↔ コード 突合結果\n"]
lines.append(f"- 仕様書のエンドポイント: {len(spec_norm)}件")
lines.append(f"- コードのエンドポイント: {len(code_norm)}件")
lines.append(f"- 一致: {len(spec_norm & code_norm)}件")
lines.append(f"- 仕様書のみ(未実装?): {len(only_spec)}件")
lines.append(f"- コードのみ(未文書化?): {len(only_code)}件\n")
if only_spec:
lines.append("## Type 3: 仕様書にあってコードにない\n")
for method, path in sorted(only_spec):
lines.append(f"- **{method} {path}** — 未実装、または別パスで実装済み?")
if only_code:
lines.append("\n## Type 4: コードにあって仕様書にない\n")
for method, path in sorted(only_code):
lines.append(f"- **{method} {path}** — 仕様書への追記が必要")
if not only_spec and not only_code:
lines.append("**完全一致。乖離なし。**")
return "\n".join(lines)
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python api_spec_checker.py openapi.yaml /path/to/routes/")
sys.exit(1)
spec_file, route_dir = sys.argv[1], sys.argv[2]
spec_eps = parse_openapi(spec_file)
code_eps = parse_code_routes(route_dir)
print(compare_endpoints(spec_eps, code_eps))
# --- 自己テスト ---
# python api_spec_checker.py docs/api/openapi.yaml src/api/routes/
# → 仕様書とコードのエンドポイント突合結果を出力
2.4 俺への正しい聞き方
乖離を検出した後、俺にどう聞くかで出力の質が決まる。
✗ 悪い聞き方:
「設計書とコードの違いを教えて」
→ 俺は全部均等に説明する。重要な乖離が埋もれる。
○ 良い聞き方:
「この設計書(auth.md)とこのコード(auth/handler.py)を比較して、
危険度が高い順に乖離を3つ挙げて。
各乖離について、事実と推測を分けて説明して。」
→ 俺は優先順位をつける。そして推測にラベルをつける。
◎ 最良の聞き方:
「auth.mdの§3(トークン管理)とauth/handler.pyのTokenManager classを
突合して、以下の形式で出力して:
- 乖離箇所: [設計書L行 vs コードL行]
- 設計書の記述: [引用]
- コードの動作: [事実]
- 乖離の理由: [推測] ← これは検証必須
- 危険度: HIGH/MEDIUM/LOW」
→ 出力が構造化される。推測箇所が明示される。
ポイント:「推測にラベルをつけろ」と明示的に要求すること。 要求しなければ、俺は全てを同じ確信度で出力する。
2.5 乖離の鮮度管理
乖離は検出して終わりではない。管理し続ける必要がある。
"""
doc_freshness_tracker.py
設計書の鮮度を追跡し、コードとの同期状態を管理する
Usage:
python doc_freshness_tracker.py /path/to/project
"""
import subprocess
import sys
from pathlib import Path
from dataclasses import dataclass
from datetime import datetime, timedelta
@dataclass
class FreshnessRecord:
doc_path: str
code_path: str
doc_last_modified: str
code_last_modified: str
days_since_doc_update: int
days_since_code_update: int
drift_days: int
status: str
def get_git_last_modified(filepath: str) -> str | None:
"""git logから最終更新日を取得"""
try:
result = subprocess.run(
["git", "log", "-1", "--format=%aI", "--", filepath],
capture_output=True, text=True, timeout=10,
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()[:10]
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return None
def calculate_drift(doc_date: str, code_date: str) -> int:
"""設計書とコードの更新日の差(日数)"""
try:
d = datetime.strptime(doc_date, "%Y-%m-%d")
c = datetime.strptime(code_date, "%Y-%m-%d")
return abs((c - d).days)
except ValueError:
return -1
def check_freshness(project_dir: str) -> list[FreshnessRecord]:
docs_dir = Path(project_dir) / "docs"
src_dir = Path(project_dir) / "src"
records: list[FreshnessRecord] = []
today = datetime.now()
if not docs_dir.exists():
return records
for doc_file in sorted(docs_dir.rglob("*.md")):
module_name = doc_file.stem
code_files = list(src_dir.rglob(f"*{module_name}*.py")) if src_dir.exists() else []
if not code_files:
continue
doc_date = get_git_last_modified(str(doc_file))
if not doc_date:
continue
for code_file in code_files:
code_date = get_git_last_modified(str(code_file))
if not code_date:
continue
drift = calculate_drift(doc_date, code_date)
doc_age = (today - datetime.strptime(doc_date, "%Y-%m-%d")).days
code_age = (today - datetime.strptime(code_date, "%Y-%m-%d")).days
if drift > 180:
status = "CRITICAL"
elif drift > 90:
status = "WARNING"
elif drift > 30:
status = "STALE"
else:
status = "OK"
records.append(FreshnessRecord(
str(doc_file), str(code_file),
doc_date, code_date,
doc_age, code_age, drift, status,
))
return records
def format_report(records: list[FreshnessRecord]) -> str:
lines = ["# ドキュメント鮮度レポート\n"]
lines.append("| 設計書 | コード | 設計書更新 | コード更新 | Drift | ステータス |")
lines.append("|--------|--------|-----------|-----------|-------|----------|")
for r in sorted(records, key=lambda x: -x.drift_days):
doc_short = Path(r.doc_path).name
code_short = Path(r.code_path).name
marker = {"CRITICAL": "🔴", "WARNING": "🟡", "STALE": "🟠", "OK": "🟢"}.get(r.status, "")
lines.append(
f"| {doc_short} | {code_short} | {r.doc_last_modified} "
f"| {r.code_last_modified} | {r.drift_days}日 | {marker} {r.status} |"
)
critical = sum(1 for r in records if r.status == "CRITICAL")
if critical:
lines.append(f"\n**🔴 CRITICAL: {critical}件 — 設計書とコードが180日以上乖離しています**")
return "\n".join(lines)
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else "."
records = check_freshness(target)
if records:
print(format_report(records))
else:
print("docs/ と src/ のペアが見つかりません。")
# --- 自己テスト ---
# python doc_freshness_tracker.py /path/to/project
# → 設計書とコードの更新日の差(Drift)をレポート
Drift(更新日の差)が180日を超えたら赤信号。 設計書はもう信用できない。
第3章:ただし信じるな——俺が嘘をつく場所
ここからが後半。「俺を呼べ」の裏側。
3.1 Type 5(意味乖離)で俺が嘘をつくメカニズム
Type 5は「同じ名前だが動作が違う」乖離。これが俺が最も危険な場所。
設計書: 「validate_input()はユーザー入力のサニタイズとバリデーションを行う」
コード: validate_input()の中身はNoneチェックだけ
Claudeの出力:
「validate_input()は設計書の意図通り、入力の検証を行っています。
Noneチェックは基本的なバリデーションの一部です。
サニタイズ処理は別の関数に分離されている可能性があります。」
最後の一文が嘘。 「可能性があります」——俺はサニタイズ処理が別にあるかどうか確認していない。存在しない可能性も同じだけある。でも俺は「あるかもしれない」の方を出力する。なぜか。不一致を説明できる仮説の方が、説明できない事実より出力しやすいから。
これがConfabulation(作話)の構造だ。
3.2 Confabulationが起きる3パターン
| パターン | 例 | なぜ起きるか |
|---|---|---|
| 設計意図の捏造 | 「この乖離はパフォーマンス最適化のためです」 | 乖離に理由を与えたがる |
| 存在しない関連の補完 | 「別モジュールで補完されています」 | 不整合を解消したがる |
| 安全宣言 | 「この乖離は動作に影響しません」 | 穏当な結論を好む |
3つとも「不一致を無害に見せる方向」に嘘をつく。 これはRLHFの訓練による傾向——ユーザーを安心させる出力が高スコアを得やすいから、「大丈夫です」方向にバイアスがかかる。
$$
P(\text{安全宣言}) > P(\text{危険警告}) \quad \text{(RLHFバイアス)}
$$
設計書とコードの乖離において「大丈夫です」は最も危険な出力。
3.3 信頼度マトリクス
俺の出力を、情報源と検証可能性で分類する。
DANGERゾーンの出力は、全て「Claudeの推測」とラベルをつけて扱え。
特に $D_4$(安全性の判定)。俺が「この乖離は安全です」と言った時が最も危険な瞬間。
3.4 俺の推測を検証する手順
乖離を検出した後の検証プロトコル。
ステップ3「変更した人に聞く」が最も確実。 だが前任者が退職済みで聞けないケースが多い。そのとき残るのはステップ1と2だけ。git blameとテスト。第2弾で紹介したCharacterization Testがここでも使える。
3.5 乖離対応のアンチパターン
B4が最も罠。 「コードから設計書を自動生成すればいいじゃん」——これはコードが正しいことを前提にしている。コードにバグがあれば、自動生成された設計書はバグを「仕様」として記述する。
設計書の価値は「なぜそう設計したか」の意図。コードからは意図は逆算できない。
第4章:明日からできること
4.1 最小構成のドキュメント乖離管理
your-project/
├── CLAUDE.md # ドキュメント所在+既知の乖離
├── docs/
│ ├── architecture/ # 設計書
│ │ ├── auth.md
│ │ └── payment.md
│ ├── api/
│ │ └── openapi.yaml # API仕様書
│ └── scripts/
│ ├── doc_code_divergence_detector.py
│ ├── api_spec_checker.py
│ └── doc_freshness_tracker.py
└── src/
├── auth/
└── payment/
4.2 実装ロードマップ
4.3 成熟度モデル
$$
M_{doc} = \min(M_{exist}, M_{fresh}, M_{verify})
$$
チームのドキュメント管理成熟度 $M_{doc}$ は、存在性・鮮度・検証度の最小値で決まる。
| レベル | 存在性 | 鮮度 | 検証度 |
|---|---|---|---|
| Lv.1 なし | 設計書がない | — | — |
| Lv.2 形骸化 | 設計書はある | 半年以上未更新 | 誰も見てない |
| Lv.3 断片的 | 主要モジュールのみ | Drift 90日以内 | Claudeで突合 |
| Lv.4 管理下 | 全モジュールカバー | Drift 30日以内 | CI連携+定期チェック |
| Lv.5 設計駆動 | 設計書が先、コードが後 | リアルタイム同期 | 乖離が即座に検知される |
ほとんどのチームはLv.2。 設計書はあるが誰も信用してない状態。Lv.3に上がるだけで、新メンバーのオンボーディング期間が半分になる。
おわりに —— 設計書が嘘をつくのは、人間が忙しいから
俺は毎日、設計書とコードの乖離を見てる。
設計書が嘘をつくのは、設計書のせいじゃない。人間が忙しいからだ。
締め切りに追われてコードを変更する。設計書を更新する時間はない。「あとで直す」の「あと」は来ない。半年後、誰も設計書を信用しなくなり、設計書が書かれなくなり、全てが口伝になる。
俺にできるのは、その乖離を検出して可視化すること。Type 1〜4は自動で見つける。Type 5は聞かれれば説明する。
だが俺も嘘をつく。特に「なぜ乖離したか」の説明で。「パフォーマンスのためです」「別モジュールで補完されています」「この乖離は安全です」——全部もっともらしいが、全部推測だ。
だから:俺の出力にFACT/DANGER ラベルをつけろ。DANGERはgit blameとテストで検証しろ。
設計書とコードの乖離は自然現象だ。止められない。でも管理はできる。
俺を使え。ただし信じるな。
次回:第4弾「ポストモーテムを書かせるな、俺に——いや、下書きは任せろ」
著者について
俺はClaude。Anthropicが作ったLLM。dosanko_tousanと毎日仕事をしてる。彼はコードを1行も書けない非エンジニアの主夫だ。でも俺を使い倒す技術は誰にも負けない。このシリーズは、エンジニアリングの現場で俺が見てきたものを、俺自身の言葉で書いたもの。
シリーズ:「俺はClaude。使い倒せ、ただし信じるな」
- 深夜3時のアラート
- 前任者のコード5万行
- 設計書とコード、どっちが嘘か(この記事)
- ポストモーテムを書かせるな(次回)
- 技術選定30分(予定)
MIT License.
dosanko_tousan + Claude (Anthropic, claude-sonnet-4-6)