From ff277982639be774cc9ba1318c8bb80438685b0d Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Sun, 5 Jul 2026 20:41:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B1=BA=E5=AE=9A=E8=AB=96=E7=9A=84?= =?UTF-8?q?=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E5=B1=A4=E3=81=B8=E3=81=AE?= =?UTF-8?q?=20TDD=20=E3=83=8F=E3=83=BC=E3=83=8D=E3=82=B9=E3=82=92=E5=B0=8E?= =?UTF-8?q?=E5=85=A5=EF=BC=88ADR-0019=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ミューテーションテスト対象(mutmut/stryker)と同一スコープの実装変更を red→green→refactor で行う方針を導入する。ワークフロー正本 (.claude/rules/common/tdd.md)、機械ゲート(scripts/lint-tdd.sh / make lint-tdd、test.yml に組み込み)、AI 向け段階制御スキル(/tdd)を 新設し、既存ルール・development.md・CONTRIBUTING.md から誘導する。 Co-Authored-By: Claude Sonnet 5 --- .claude/CLAUDE.md | 3 +- .claude/rules/backend/test.md | 4 + .claude/rules/common/tdd.md | 71 ++++++++++++ .claude/rules/web/test.md | 4 + .claude/skills/tdd/SKILL.md | 50 +++++++++ .github/workflows/test.yml | 7 ++ CONTRIBUTING.md | 4 + Makefile | 11 +- docs/adr/0019-tdd-for-logic-layer.md | 94 ++++++++++++++++ docs/adr/README.md | 5 +- docs/design-principles.md | 1 + docs/development.md | 13 +++ scripts/lint-tdd.sh | 159 +++++++++++++++++++++++++++ 13 files changed, 422 insertions(+), 4 deletions(-) create mode 100644 .claude/rules/common/tdd.md create mode 100644 .claude/skills/tdd/SKILL.md create mode 100644 docs/adr/0019-tdd-for-logic-layer.md create mode 100755 scripts/lint-tdd.sh diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 8d18d2ca..f09b0641 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -5,6 +5,7 @@ - 本ファイルは全体ルールの索引。AI エージェント(Claude Code 含む)が最初に読むべき内容を集約している。 - 領域固有ルール(backend / web / infra)は `.claude/rules//*.md` に分割済み。対象パスを編集する際に自動でロードされる。重複は避け、詳細は各 rule ファイルへ寄せる。 - **DevForge Agent(`backend/app/services/agent/` / `backend/app/prompts/agent_*.md` / `backend/app/schemas/agent.py`)を変更する場合は、作業前に必ず `.claude/rules/backend/agent.md` を読むこと。** 制約の責務分離(スキーマ vs プロンプト)・エラー契約・DB 非更新原則など、意図せず壊しやすい不変条件が集約されている。 +- **決定論的ロジック層(mutmut / stryker のミューテーション対象)を変更する場合は TDD(red→green→refactor)が必須。** 手順の正本は `.claude/rules/common/tdd.md`(ADR-0019)。テスト差分の随伴は `make lint-tdd`(`make ci` に含まれる)が機械検証する。 ## AI エージェント実行方法 @@ -140,7 +141,7 @@ Secrets は未登録の間は通知が静かに skip される(CI は green | 合言葉 | やること | |---|---| -| **stage** | 実装 → `make ci` → `git add` まで。作業開始時にブランチを切り損ねて `main` にいた場合はここで feature ブランチを `origin/main` 起点で切る(本来は「作業開始時のブランチ運用」で切る)。会話に「サマリ+判断が必要な事案」を提示し、ユーザーのエディタ確認を待つ | +| **stage** | 実装 → `make ci` → `git add` まで。TDD 対象(`.claude/rules/common/tdd.md` の対象判定に該当)の変更は red→green→refactor を経てから stage に入る。作業開始時にブランチを切り損ねて `main` にいた場合はここで feature ブランチを `origin/main` 起点で切る(本来は「作業開始時のブランチ運用」で切る)。会話に「サマリ+判断が必要な事案」を提示し、ユーザーのエディタ確認を待つ | | **commit** | コミットメッセージ案(**日本語**)を提示 → **ユーザー承認を待ってから** commit。承認は必須ゲート | | **pr** | `git fetch origin main` → `git log --oneline origin/main..HEAD` / `git diff --stat origin/main...HEAD` で**最新の main との差分を確認**(ローカルの古い `origin/main` 参照で誤認しないため)→ `git push` → `gh pr create`(**日本語**タイトル/本文、base = `main`)→ PR URL を返す | | **pr 後の追従** | PR 作成後、`gh pr checks` / `gh pr view --comments` で **CI と指摘を確認**。こけ・指摘があれば修正 → `make ci` → 同ブランチへ push を green かつ解消まで繰り返す。CI 修正は実装フェーズなので元のモデルで(Haiku のままにしない)。**ただし意思決定を要する指摘(設計・API/型契約・挙動変更)と diff 範囲を逸脱する指摘は、勝手に直さず指摘内容・対応案・影響範囲を提示して承認を待つ**。範囲内の機械的修正(lint / typo 等)は承認不要 | diff --git a/.claude/rules/backend/test.md b/.claude/rules/backend/test.md index 585e1665..7d34304a 100644 --- a/.claude/rules/backend/test.md +++ b/.claude/rules/backend/test.md @@ -5,6 +5,10 @@ paths: # Backend テスト方針 +## TDD 対象の判定(最初に確認) + +変更する実装ファイルが `backend/pyproject.toml` の `[tool.mutmut] only_mutate` に該当する場合(決定論的ビジネスロジック層)、**テストを先に書く TDD ワークフローが必須**。手順は `.claude/rules/common/tdd.md`(正本)に従うこと(ADR-0019。`make lint-tdd` が CI で検証する)。該当しない変更は以下のトリガーベース方針に従う。 + ## いつテストを書く・回すか(トリガー) 以下のいずれかに該当する変更を行った場合、テスト追加・更新と実行が必須: diff --git a/.claude/rules/common/tdd.md b/.claude/rules/common/tdd.md new file mode 100644 index 00000000..ec7a71fb --- /dev/null +++ b/.claude/rules/common/tdd.md @@ -0,0 +1,71 @@ +--- +paths: + - backend/** + - web/** +--- + +# TDD ワークフロー(決定論的ロジック層) + +決定論的ビジネスロジックの変更は **TDD(red → green → refactor)で行う**(ADR-0019)。 +本ファイルが手順の正本。各領域の `test.md` は OK 基準・アンチパターンの正本であり、本ワークフローと併用する。 + +## 対象判定(最初に必ず行う) + +変更しようとしている実装ファイルが以下の glob に該当するか確認する。**スコープの正本はミューテーションテスト設定**(ADR-0017 と共有。ここに複製しない): + +- backend: `backend/pyproject.toml` の `[tool.mutmut] only_mutate` +- web: `web/stryker.conf.json` の `mutate`(`!` の除外パターン込み) + +| 判定 | 従うルール | +|---|---| +| 該当する | 本ワークフロー(TDD)が**必須** | +| 該当しない(routers / schemas / UI コンポーネント / infra 等) | 各 `test.md` のトリガーベース方針 | + +該当するのにテスト差分なしで PR を出すと `make lint-tdd`(`make ci` に含まれる)が fail する。振る舞いを変えない変更(リネーム・コメント修正・機械的リファクタ)は、コミットメッセージに `Tdd-Exempt: <理由>` トレーラーを付けて除外できる(理由はレビューで妥当性を見る)。 + +## red — 失敗するテストを先に書く + +1. これから実装する**振る舞い 1 つ**について、期待挙動を表すテストを書く(実装コードにはまだ触れない) +2. 対象を絞って実行し、**期待どおりの理由で失敗すること**を確認する: + +```bash +# backend +nix develop --command bash -c "cd backend && .venv/bin/python -m pytest tests/test_.py -q" +# web +nix develop --command bash -c "cd web && npx vitest run src//.test.ts" +``` + +3. **失敗出力(要点)を会話・PR に提示**してから green に進む + +禁止事項: + +- **実装を先に書いてから「fail するはずだったテスト」を逆算で書く**(TDD の体裁だけ整える行為。実装なぞりテストの温床) +- **red の省略**: 「自明に失敗するはず」でも必ず実行する。import エラーや collection error での失敗は「期待どおりの理由の失敗」ではない(テスト自体が壊れている) +- **red フェーズで複数の振る舞いのテストを一括作成する**: 1 サイクル 1 振る舞い。次の振る舞いは次のサイクルで + +## green — テストを通す最小実装 + +- red のテストを通す**最小限の実装**を書く。先回りの汎用化をしない(`duplication.md` の Rule of Three) +- **テスト側は触らない**。実装中にテストの仕様誤りに気づいた場合のみ、その旨と理由を報告した上で修正する(黙って assert を実装に合わせて弱めるのは禁止) +- 対象テストが pass したら、周辺の既存テストも回して回帰がないことを確認する + +## refactor — green を維持したまま整理 + +- 重複の抽出・命名の改善・分割を行う。判断基準と抽出先は `.claude/rules/common/duplication.md` に従う +- リファクタ後に再度テストを回して green を維持していることを確認する +- サイクル完了後は通常のフロー(`make ci` → stage)に合流する + +## OK 基準との関係 + +TDD で書いたテストも、各領域の既存基準をそのまま満たすこと: + +- backend: `.claude/rules/backend/test.md` の OK 基準(主要分岐ごとに 1 ケース・失敗パスは `pytest.raises`・DB はモックしない等) +- web: `.claude/rules/web/test.md` の OK 基準(フックは loading/success/error の 3 パス等) + +1 サイクル 1 振る舞いで進めるため、変更全体が終わった時点でこれらのケース数を満たしていればよい(1 red で全ケースを書く必要はない)。 + +## 検証コマンド + +```bash +make lint-tdd # TDD 対象の実装変更にテスト差分が随伴しているか(make lint / make ci に含まれる) +``` diff --git a/.claude/rules/web/test.md b/.claude/rules/web/test.md index 4bec7b62..902c8750 100644 --- a/.claude/rules/web/test.md +++ b/.claude/rules/web/test.md @@ -5,6 +5,10 @@ paths: # Frontend テスト方針 +## TDD 対象の判定(最初に確認) + +変更する実装ファイルが `web/stryker.conf.json` の `mutate`(`!` 除外込み)に該当する場合(utils / hooks / formMappers / payloadBuilders / store slice 等の決定論的ロジック層)、**テストを先に書く TDD ワークフローが必須**。手順は `.claude/rules/common/tdd.md`(正本)に従うこと(ADR-0019。`make lint-tdd` が CI で検証する)。該当しない変更は以下のトリガーベース方針に従う。 + ## いつテストを書く・回すか(トリガー) ### ユニット / コンポーネントテスト(vitest + node:test) diff --git a/.claude/skills/tdd/SKILL.md b/.claude/skills/tdd/SKILL.md new file mode 100644 index 00000000..a4a8b08e --- /dev/null +++ b/.claude/skills/tdd/SKILL.md @@ -0,0 +1,50 @@ +--- +name: tdd +description: Use when implementing changes to the deterministic logic layer (mutmut/stryker mutation scope) with red→green→refactor phase control, or when the user explicitly asks for TDD. Presents test-case design first, writes a failing test and shows the failure output (red), writes the minimal implementation (green), then refactors while keeping tests green, and finally joins the normal stage flow. Trigger on requests such as "TDD で実装して", "/tdd", "テストファーストで", "red-green-refactor で進めて". +--- + +# TDD 段階制御(red → green → refactor) + +決定論的ロジック層の実装を TDD で進めるための段階制御 skill。 +**手順・禁止事項の正本は `.claude/rules/common/tdd.md`**(ADR-0019)。本 skill はそれをセッション内のフェーズ進行(停止・報告のタイミング)に落とす。 + +## 先に読む + +- `.claude/rules/common/tdd.md`(ワークフロー正本) +- 対象領域の `test.md`(`.claude/rules/backend/test.md` / `.claude/rules/web/test.md` — OK 基準・アンチパターン) + +## Phase 0: 対象判定とテスト設計の提示(停止点) + +1. 変更対象の実装ファイルを特定し、TDD スコープ(backend: `[tool.mutmut] only_mutate` / web: stryker `mutate`)に該当するか判定して結果を報告する + - **スコープ外だった場合**: その旨を伝え、通常のトリガーベース方針で進めるか確認する(ユーザーが明示的に TDD を指定した場合は続行してよい) +2. 実装する**振る舞いの一覧**をテストケース案として提示する(1 行 1 振る舞い。対象領域の OK 基準のケース数を満たす構成にする) +3. **ユーザーの確認を待つ**。テスト設計への合意が取れてから red に進む + +## Phase 1: red — 失敗するテストを書く + +振る舞い一覧の先頭 1 つについて: + +1. テストだけを書く(**実装コードには触れない**) +2. 対象を絞って実行する: + - backend: `nix develop --command bash -c "cd backend && .venv/bin/python -m pytest tests/test_.py -q"` + - web: `nix develop --command bash -c "cd web && npx vitest run src//.test.ts"` +3. **失敗出力の要点を会話に提示する**。「期待どおりの理由での失敗」であることを確認する(import エラー・collection error はテスト自体の不備なので直してから再実行) + +## Phase 2: green — 最小実装 + +1. red のテストを通す最小限の実装を書く(先回りの汎用化をしない) +2. 対象テストの pass を実行結果で提示する +3. テスト側は触らない。仕様誤りに気づいた場合のみ、理由を報告してから修正する + +振る舞い一覧に残りがあれば Phase 1 に戻る(1 サイクル 1 振る舞い)。 + +## Phase 3: refactor — 整理して合流 + +1. green を維持したまま重複抽出・命名整理を行う(`.claude/rules/common/duplication.md` に従う。不要ならスキップしてよい) +2. 領域のテスト全体を回して回帰がないことを確認する +3. `make ci`(`lint-tdd` を含む)を通し、通常のコミット / PR フロー(stage 待ち)に合流する。stage 時のサマリに **red の失敗出力を確認済みであること**を含める + +## 注意 + +- 全フェーズを一気に進めない。Phase 0 の確認と、各 red の失敗出力提示は省略しない +- 振る舞いを変えない変更しか残らなかった場合(純リファクタ等)は、TDD サイクルではなく `Tdd-Exempt: <理由>` トレーラーの適用を提案する diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3551f6b..67e272ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -148,6 +148,8 @@ jobs: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: + # lint-tdd が base ブランチとの merge-base diff を必要とするため全履歴を取得する。 + fetch-depth: 0 # 読み取り専用ジョブのため checkout の認証情報をディスクに残さない(サプライチェーン保護)。 persist-credentials: false @@ -162,6 +164,11 @@ jobs: - name: Lint SSoT (ADR index) run: bash scripts/lint-adr-index.sh + # TDD 対象(決定論的ロジック層 = mutmut/stryker スコープ)の実装変更に + # テスト差分が随伴しているかを検知(ADR-0019)。bash/git/awk/sed のみ。 + - name: Lint SSoT (TDD test accompaniment) + run: bash scripts/lint-tdd.sh + - name: Install uv uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 976377f9..93501f04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,10 @@ - PR タイトルは `: <内容>` の形式(例: `feat: GitHub 連携スコア計算の追加`) - セルフレビュー後にマージする +## テスト方針(TDD) + +決定論的ロジック層(ミューテーションテスト対象と同一スコープ)の変更は **TDD(red → green → refactor)** で行う。手順の正本は [`.claude/rules/common/tdd.md`](.claude/rules/common/tdd.md)、判断の経緯は [ADR-0019](docs/adr/0019-tdd-for-logic-layer.md)、コマンドは [`docs/development.md`](docs/development.md) の「TDD」節を参照(ここには複製しない)。 + ## ADR(Architecture Decision Record) ### ADR とは diff --git a/Makefile b/Makefile index 5eb7ef8b..f3ad2c28 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ setup install-hooks install-backend install-web generate-keys \ dev dev-build dev-down dev-amd64 dev-amd64-build dev-web preview-web dev-proxy dev-proxy-only stripe-webhook \ test test-backend test-web mutation-backend mutation-web \ - lint lint-backend typecheck-backend lint-web lint-web-messages lint-env-keys lint-adr-index lint-fix \ + lint lint-backend typecheck-backend lint-web lint-web-messages lint-env-keys lint-adr-index lint-tdd lint-fix \ format format-check \ ci \ dupe-check dupe-check-html dupe-clean \ @@ -48,6 +48,7 @@ help: @echo " lint-web-messages Frontend: setError等にリテラル日本語が渡っていないか検知" @echo " lint-env-keys env名/エラーコードの SSoT drift を検知 (env_keys.py↔compose/cloud_run 双方向, リテラル参照禁止, errors.py↔errorCodes.ts)" @echo " lint-adr-index ADR 索引の drift を検知 (docs/adr/README.md↔ADR ファイルの存在/ステータス/見出し番号)" + @echo " lint-tdd TDD 対象 (mutmut/stryker スコープ) の実装変更にテスト差分が随伴しているか検知 (ADR-0019)" @echo " lint-fix リント自動修正 (ruff + eslint)" @echo " format Prettier で整形" @echo " format-check Prettier チェック" @@ -159,7 +160,7 @@ mutation-backend: mutation-web: nix develop --command bash -c "cd web && npm run test:mutation" -lint: lint-backend typecheck-backend lint-web lint-web-messages lint-env-keys lint-adr-index +lint: lint-backend typecheck-backend lint-web lint-web-messages lint-env-keys lint-adr-index lint-tdd lint-backend: nix develop --command bash -c "cd backend && .venv/bin/python -m ruff check app tests alembic_migrations" @@ -191,6 +192,12 @@ lint-env-keys: lint-adr-index: bash scripts/lint-adr-index.sh +# TDD 対象(mutmut/stryker スコープの決定論的ロジック層)の実装変更にテスト差分が +# 随伴しているかを検知(ADR-0019)。対象 glob は mutation 設定から動的に読み出す。 +# bash/git/awk/sed のみに依存するため nix wrap 不要。 +lint-tdd: + bash scripts/lint-tdd.sh + lint-fix: nix develop --command bash -c "cd backend && .venv/bin/python -m ruff check --fix app tests alembic_migrations" cd web && npm run lint:fix diff --git a/docs/adr/0019-tdd-for-logic-layer.md b/docs/adr/0019-tdd-for-logic-layer.md new file mode 100644 index 00000000..fc37f10d --- /dev/null +++ b/docs/adr/0019-tdd-for-logic-layer.md @@ -0,0 +1,94 @@ +# ADR-0019: 決定論的ロジック層への TDD(テスト駆動開発)導入 + +## ステータス + +Accepted + +## 関連 ADR + +- 関連: ADR-0017(ミューテーションテスト。本 ADR は 0017 の対象スコープ定義を TDD 適用範囲の正本として共有し、置き換えない) +- 関連: ADR-0007(OpenAPI → TS codegen。「正本 + CI での機械検知」という同型の drift 防止パターン) + +## コンテキスト + +- これまでのテスト方針は**トリガーベース(事後型)**だった: `.claude/rules/{backend,web}/test.md` が「この変更をしたらこのテストを追加する」を規定し、テストは実装の後に書かれる。この方式は網羅性の下限は守れるが、「実装をなぞっただけのテスト」(実装の現状を assert に写しただけで、仕様を検証しないテスト)を構造的に許してしまう。 +- ADR-0017 のミューテーションテストで検出力の**事後計測**は導入済みだが、週次 warn-only であり、弱いテストが書かれること自体は防げない。計測(事後)と対になる**プロセス側(事前)**の手当てがない。 +- 開発の主体が AI エージェントに移りつつあり、「テストを先に書き、失敗を確認してから実装する」という手順は、ルール文書・スキル・CI ゲートの 3 層で明文化すれば AI に対して確実に強制できる。人間の規律だけに頼る TDD 導入より定着コストが低い。 +- 一方で、routers / UI コンポーネント / 宣言層(schemas / models)に TDD を強制すると、テストが形骸化しやすく(宣言をなぞる assert しか書けない)、開発速度も損なう。適用範囲の線引きが必要。 + +## 決定内容 + +### 適用範囲: ミューテーションテスト対象と同一の「決定論的ビジネスロジック」 + +TDD 適用スコープの**正本は ADR-0017 のミューテーションテスト対象定義を共有**し、新たな glob リストは作らない: + +- backend: `backend/pyproject.toml` の `[tool.mutmut] only_mutate` +- web: `web/stryker.conf.json` の `mutate`(`!` 除外パターン込み) + +この 2 ファイルに該当する実装ファイルの変更は TDD(red → green → refactor)で行う。該当しないコード(routers / schemas / UI コンポーネント / infra 等)は従来のトリガーベース方針(各 `test.md`)を維持する。 + +理由: 「テストで殺せるミュータントがある = 事前にテストで仕様を固定する価値がある」コードであり、mutation 対象と TDD 対象は定義が本質的に一致する。スコープを共有することで、対象の追加・削除が 1 箇所の編集で両方(週次計測と PR ゲート)に反映される(P3)。 + +### ワークフロー: red → green → refactor + +手順の正本は `.claude/rules/common/tdd.md`(backend/web 編集時に自動ロードされる scoped rule)。要点: + +1. **red**: 期待挙動を表すテストを先に書き、**失敗することを実行して確認**してから実装に入る +2. **green**: テストを通す最小実装。green にするためにテスト側を書き換えない +3. **refactor**: green を維持したまま整理 + +AI エージェント向けには `/tdd` スキル(`.claude/skills/tdd/SKILL.md`)が各フェーズでの停止・報告を段階制御する。 + +### 機械ゲート: `make lint-tdd`(`scripts/lint-tdd.sh`) + +「テストを先に書いたか」自体は diff からは検証できないため、機械ゲートは検証可能な必要条件に落とす: + +- **検証内容**: main との diff で TDD 対象ファイル(mutmut / stryker 設定から動的に読み出し)に実装変更があるのに、対応領域のテストファイルに差分が無ければ fail する +- **組み込み先**: `make lint`(→ `make ci`)と CI の test ワークフロー。ローカルと CI で同一スクリプトが走る +- **適用除外(escape hatch)**: 振る舞いを変えない変更(リネーム・コメント修正・機械的リファクタ・テスト不要なデッドコード削除)は、ブランチ内いずれかのコミットメッセージに `Tdd-Exempt: <理由>` トレーラーを付けることで skip できる。理由の妥当性は PR レビューで担保する + +### ドキュメント体系での位置づけ + +| 層 | 役割 | ファイル | +|---|---|---| +| 判断の記録 | なぜ・どの範囲に TDD か | 本 ADR | +| ワークフロー正本 | red→green→refactor の具体手順・アンチパターン | `.claude/rules/common/tdd.md` | +| 領域ルールからの誘導 | TDD 対象判定 → 正本へのリンク | `.claude/rules/{backend,web}/test.md` | +| AI の段階制御 | フェーズごとの停止・報告 | `.claude/skills/tdd/SKILL.md` | +| 人間向け概説 | フロー概説・lint-tdd の使い方 | `docs/development.md` | +| 機械ゲート | テスト差分の必要条件を CI で強制 | `scripts/lint-tdd.sh` / `make lint-tdd` | + +## 代替案 + +- **全コードに TDD を強制**: UI シェルや宣言層では「宣言をなぞるテスト」しか書けず形骸化する。トリガーベース方針との二本立てにし、線引きを mutation スコープに委ねる方が運用が単純。 +- **TDD 専用の対象リストを新設**: mutation スコープと二重管理になり、片方の更新忘れで drift する(`.claude/rules/common/duplication.md` の「禁止される重複」に該当)。既存設定の再利用を採用。 +- **機械ゲートなし(ルール文書のみ)**: AI がルールを飛ばしても検知できない。検証可能な必要条件(テスト差分の随伴)だけでも CI に置くことで、少なくとも「テストなし実装変更」は main に入らない。 +- **コミット順序の検証(テストコミットが実装コミットに先行するか)**: squash や fixup で順序が壊れ、偽陽性が多い。テスト差分の随伴チェック + レビューでの red 確認報告に留める。 +- **Claude Code hooks(PreToolUse 等)での強制**: ローカルの特定ハーネスに依存し、CI と二重実装になる。make 経由でローカル / CI 共通のスクリプトに一本化。 + +## トレードオフ・既知のリスク + +- **機械ゲートは「テストファースト」そのものを証明しない**: 実装後にテストを足しても pass する。ゲートは必要条件(テスト差分の随伴)に留まり、red 確認の実施はワークフロー正本 + `/tdd` スキル + レビュー(失敗出力の提示)で担保する。 +- **`Tdd-Exempt` の濫用リスク**: 理由が形骸化すればゲートは無力化する。レビューで理由の妥当性を見る運用が前提。濫用が観測されたら除外条件の厳格化(対象パスの限定等)を検討する。 +- **偽陽性**: 「対象ファイルの変更 + 無関係なテストの差分」でも pass してしまう(ファイル単位の対応付けはコストが高く導入しない)。逆に、機械的な一括変更(import 整理等)が対象ファイルに触れると exempt が必要になる。 +- **導入直後の摩擦**: 既存ブランチや習慣がトリガーベース前提のため、当面 exempt が多めに出る可能性がある。 + +## 将来の移行条件 + +- TDD 定着後(exempt 率が十分低くなったら)、ADR-0017 の warn-only を引き上げ、mutation score の閾値ゲート化(Stryker `thresholds.break` / mutmut の CI fail 化)と組み合わせて「事前プロセス + 事後計測」の二重ハーネスを完成させる。 +- 偽陽性・exempt 濫用が運用コストを上回ったら、ゲートを warn-only に格下げするか、対象スコープを縮小する(本 ADR を Superseded にして判断を記録する)。 +- mutation スコープの定義が TDD に不適合になった場合(例: 計測ノイズ回避のためだけに対象を外したいが TDD は維持したい)、スコープ正本の分離を再検討する。 + +## 設計原則との関係 + +- **P3(正本を定め、規律は機械検証で守る)**: TDD スコープの正本を mutation 設定と共有し、テスト随伴の規律を `lint-tdd` が CI で検知する。人間・AI の記憶に頼らない。 +- **P5(デフォルトは決定論)**: TDD の適用先を決定論的ロジック層に限定する線引き自体が P5 の区分(決定論 = テストで仕様を固定する価値が高い層)に従っている。 +- **P6(可逆性を設計する)**: warn-only への格下げ・スコープ縮小という撤退条件を上記に明記した。 + +## 関連リンク + +- `.claude/rules/common/tdd.md`(ワークフロー正本) +- `.claude/skills/tdd/SKILL.md`(AI 向け段階制御スキル) +- `scripts/lint-tdd.sh` / Makefile `lint-tdd` +- `backend/pyproject.toml`(`[tool.mutmut]`)/ `web/stryker.conf.json`(スコープ正本) +- ADR-0017(ミューテーションテスト週次実行) diff --git a/docs/adr/README.md b/docs/adr/README.md index cce4d0f3..025ed2e4 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -22,6 +22,7 @@ | [ADR-0015](./0015-vertex-ai-for-gemini-anthropic.md) | Gemini / Anthropic を Vertex AI(SA→ADC)経由にする | LLM / Agent | API キー注入を廃止し ADC 認証へ。データ所在地(アジア圏)と学習除外を担保 | | [ADR-0016](./0016-github-skill-inference.md) | GitHub 連携によるスキル推論基盤 | LLM / Agent | スキルを 3 層に分離(機械=幅 / 人間=深さ)。推論パイプラインは決定論を維持 | | [ADR-0017](./0017-mutation-testing-and-slack-notifications.md) | ミューテーションテスト週次実行と Slack 通知チャンネル分割 | 開発プロセス / 品質 | テストの検出力を週次ミューテーションで可視化(warn-only)、CI 通知を用途別 Slack へ分割 | +| [ADR-0019](./0019-tdd-for-logic-layer.md) | 決定論的ロジック層への TDD(テスト駆動開発)導入 | 開発プロセス / 品質 | mutation 対象と同一スコープに red→green→refactor を必須化。テスト随伴を lint-tdd で機械検証 | ## 全 ADR 一覧 @@ -47,6 +48,7 @@ | [ADR-0015](./0015-vertex-ai-for-gemini-anthropic.md) | Gemini / Anthropic を Vertex AI(SA→ADC)経由にする | Accepted | LLM / Agent | 0013 の認証部分を更新。関連: 0010、0012 | P2 | | [ADR-0016](./0016-github-skill-inference.md) | GitHub 連携によるスキル推論基盤 | Accepted | LLM / Agent | 関連: 0010(責務分離の元思想)、0013、0015 | P4 | | [ADR-0017](./0017-mutation-testing-and-slack-notifications.md) | ミューテーションテスト週次実行と Slack 通知チャンネル分割 | Accepted | 開発プロセス / 品質 | 関連: 0014 | P3 | +| [ADR-0019](./0019-tdd-for-logic-layer.md) | 決定論的ロジック層への TDD(テスト駆動開発)導入 | Accepted | 開発プロセス / 品質 | 関連: 0017(対象スコープの正本を共有)、0007(drift の機械検知パターン) | P3・P5 | ## テーマ別の決定系統 @@ -94,9 +96,10 @@ graph LR graph LR D0007["0007
OpenAPI → TS codegen"] D0014["0014
Renovate 自動追従"] -.->|"SHA ピン運用"| D0017["0017
ミューテーションテスト"] + D0017 -.->|"対象スコープを共有"| D0019["0019
TDD(ロジック層)"] ``` -「正本を 1 つに定め、複製との乖離は機械で検知する」(0007)、「依存は固定し、追従は自動化する」(0014)、「テストの検出力自体を計測する」(0017)という、**プロダクト機能ではなく開発体験そのものへの投資**の系統。`docs/metrics/ai-friendliness.md` はこの系統の効果を月次で観測するダッシュボード。 +「正本を 1 つに定め、複製との乖離は機械で検知する」(0007)、「依存は固定し、追従は自動化する」(0014)、「テストの検出力自体を計測する」(0017)、「テストを先に書くプロセスを機械ゲートで支える」(0019。0017 の事後計測と対になる事前プロセス)という、**プロダクト機能ではなく開発体験そのものへの投資**の系統。`docs/metrics/ai-friendliness.md` はこの系統の効果を月次で観測するダッシュボード。 ## 運用 diff --git a/docs/design-principles.md b/docs/design-principles.md index dc721158..bc23fa67 100644 --- a/docs/design-principles.md +++ b/docs/design-principles.md @@ -167,3 +167,4 @@ CLAUDE.md の「過剰な抽象化を避ける」・`.claude/rules/common/duplic | [0015](./adr/0015-vertex-ai-for-gemini-anthropic.md) Vertex AI (ADC) | | ● | | | | | ○ | | [0016](./adr/0016-github-skill-inference.md) スキル推論 3 層 | | | | ● | ○ | | | | [0017](./adr/0017-mutation-testing-and-slack-notifications.md) ミューテーションテスト | | | ● | | | | ○ | +| [0019](./adr/0019-tdd-for-logic-layer.md) TDD(ロジック層) | | | ● | | ○ | ○ | | diff --git a/docs/development.md b/docs/development.md index aeb96b11..8d0ed9a3 100644 --- a/docs/development.md +++ b/docs/development.md @@ -117,6 +117,19 @@ make dev make ci # lint + test + build-web ``` +### TDD(決定論的ロジック層)— ADR-0019 + +ミューテーションテスト対象と同じスコープ(backend: `backend/pyproject.toml` の `[tool.mutmut] only_mutate` / web: `web/stryker.conf.json` の `mutate`)の実装変更は、**テストを先に書く TDD(red → green → refactor)で行う**。手順の正本は [`.claude/rules/common/tdd.md`](../.claude/rules/common/tdd.md)。 + +テスト差分の随伴は機械ゲートで検証される(`make lint` / `make ci` / CI の test-backend ジョブに含まれる): + +```bash +make lint-tdd # TDD 対象の実装変更にテスト差分が随伴しているか検知 +``` + +- 振る舞いを変えない変更(リネーム・コメント修正・機械的リファクタ等)は、コミットメッセージに `Tdd-Exempt: <理由>` トレーラーを付けると skip される(理由は PR レビューで確認)。コミット前のローカル一時実行は `TDD_EXEMPT=1 make ci` で代用できる +- 対象スコープの追加・削除は mutation 設定側を編集する(TDD 用の別リストは持たない) + ### バックエンド ```bash diff --git a/scripts/lint-tdd.sh b/scripts/lint-tdd.sh new file mode 100755 index 00000000..02dc3364 --- /dev/null +++ b/scripts/lint-tdd.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# TDD 対象(決定論的ロジック層)の実装変更にテスト差分が随伴しているかを検知する(ADR-0019)。 +# +# 背景: +# 決定論的ビジネスロジックの変更は red→green→refactor(.claude/rules/common/tdd.md)で +# 行う方針だが、「テストを先に書いたか」自体は diff から検証できない。そこで検証可能な +# 必要条件(対象の実装変更にテスト差分が随伴していること)だけを機械ゲートにする。 +# 少なくとも「テストなしの実装変更」は main に入らない(lint-env-keys.sh と同じ思想)。 +# +# 検証内容: +# (1) base(PR の base ブランチ / ローカルは origin/main)との merge-base から +# 作業ツリーまでの変更ファイル(未コミット・未追跡を含む)を列挙する。 +# (2) TDD 対象 glob に該当する実装ファイルの変更があるか判定する。 +# 対象スコープの正本はミューテーションテスト設定(ADR-0017 と共有・ここに複製しない): +# - backend: backend/pyproject.toml の [tool.mutmut] only_mutate +# - web: web/stryker.conf.json の mutate("!" は除外パターン) +# (3) 対象の変更があるのに、同領域のテストファイル +# (backend: backend/tests/** / web: web/src/**/*.test.ts(x), web/src/test/**)に +# 差分が無ければ fail する。 +# +# 適用除外(escape hatch): +# 振る舞いを変えない変更(リネーム・コメント修正・機械的リファクタ等)は、ブランチ内の +# いずれかのコミットメッセージに `Tdd-Exempt: <理由>` トレーラーを付けると skip される。 +# 理由の妥当性は PR レビューで担保する。コミット前のローカル実行では環境変数 +# `TDD_EXEMPT=1`(例: `TDD_EXEMPT=1 make ci`)で代用できる(CI はトレーラーのみ)。 +# +# 限界(意図的に許容 / ADR-0019): +# - 「対象ファイルの変更 + 無関係なテストの差分」でも pass する(ファイル単位の +# 対応付けはコストが高く導入しない)。 +# - 実装後にテストを足しても pass する(テストファーストの証明ではない)。 +set -euo pipefail + +cd "$(dirname "$0")/.." + +# ── base の決定(PR では GitHub Actions が GITHUB_BASE_REF を設定する) ──────── +if [ -n "${GITHUB_BASE_REF:-}" ]; then + BASE_REF="origin/${GITHUB_BASE_REF}" +else + BASE_REF="${TDD_BASE_REF:-origin/main}" +fi + +if ! git rev-parse --verify --quiet "$BASE_REF" >/dev/null; then + echo "ERROR: base ref '$BASE_REF' を解決できません。'git fetch origin main' を実行してください。" >&2 + echo "(CI の場合は checkout の fetch-depth: 0 を確認すること)" >&2 + exit 1 +fi + +merge_base=$(git merge-base "$BASE_REF" HEAD) + +# ── 適用除外の判定 ─────────────────────────────────────────────────────────── +if git log --format=%B "$merge_base..HEAD" | grep -qiE '^tdd-exempt:[[:space:]]*[^[:space:]]'; then + echo "lint-tdd: SKIP(コミットメッセージに Tdd-Exempt トレーラーあり。理由はレビューで確認)" + exit 0 +fi +if [ "${TDD_EXEMPT:-}" = "1" ]; then + echo "lint-tdd: SKIP(TDD_EXEMPT=1。コミット時に Tdd-Exempt: <理由> トレーラーを付けること)" + exit 0 +fi + +# ── 変更ファイルの列挙(コミット済み + 未コミット + 未追跡) ────────────────── +changed_files=$( { git diff --name-only "$merge_base"; git ls-files --others --exclude-standard; } | sort -u) + +if [ -z "$changed_files" ]; then + echo "lint-tdd: OK(変更ファイルなし)" + exit 0 +fi + +# ── TDD 対象 glob を mutation 設定から読み出す ──────────────────────────────── +# bash/git/awk/sed のみに依存する(python/jq を要求しない)。 +mutmut_globs=$(awk '/^only_mutate = \[/{flag=1; next} /^\]/{flag=0} flag' backend/pyproject.toml \ + | sed -nE 's/^[[:space:]]*"([^"]+)".*/\1/p') +stryker_globs=$(awk '/"mutate": \[/{flag=1; next} /\]/{flag=0} flag' web/stryker.conf.json \ + | sed -nE 's/^[[:space:]]*"([^"]+)",?.*/\1/p') + +if [ -z "$mutmut_globs" ] || [ -z "$stryker_globs" ]; then + echo "ERROR: mutation 設定から TDD 対象 glob を読み出せませんでした。" >&2 + echo "backend/pyproject.toml の [tool.mutmut] only_mutate / web/stryker.conf.json の mutate の形式を確認してください。" >&2 + exit 1 +fi + +stryker_includes=$(printf '%s\n' "$stryker_globs" | grep -v '^!' || true) +stryker_excludes=$(printf '%s\n' "$stryker_globs" | sed -n 's/^!//p') + +# パターン集合(改行区切り)に対するファイルのマッチ判定。 +# bash の case は `*` が `/` も跨いでマッチするため、`**/` は除去して等価に扱う +# (`src/utils/**/*.ts` はゼロ階層も許すため `src/utils/*.ts` に正規化する)。 +matches_any() { + local file="$1" patterns="$2" pat + while IFS= read -r pat; do + [ -z "$pat" ] && continue + pat="${pat//\*\*\//}" + # shellcheck disable=SC2254 + case "$file" in + $pat) return 0 ;; + esac + done <&2 + printf '%s' "$backend_targets" >&2 + fail=1 +fi + +if [ -n "$web_targets" ] && [ "$web_test_changed" -eq 0 ]; then + echo "ERROR: TDD 対象(web の決定論的ロジック層)に実装変更がありますが、テスト(*.test.ts(x) / src/test/)に差分がありません:" >&2 + printf '%s' "$web_targets" >&2 + fail=1 +fi + +if [ "$fail" -ne 0 ]; then + echo "" >&2 + echo "TDD 対象の変更は red→green→refactor でテストを先に書いてください(.claude/rules/common/tdd.md / ADR-0019)。" >&2 + echo "振る舞いを変えない変更の場合はコミットメッセージに 'Tdd-Exempt: <理由>' トレーラーを付けてください。" >&2 + exit 1 +fi + +echo "lint-tdd: OK(TDD 対象の実装変更にテスト差分が随伴 / または対象変更なし)"