1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIに仕様駆動開発を“強制”する仕組みを作る(第3回・機械強制)

1
Last updated at Posted at 2026-06-22

はじめに

シリーズ第3回。①②(助言)の続きです。

この記事では、ハーネスの中核「やったら止める」決定論的な強制を、動くコードで紹介します。①②の助言を、破れない約束に変える層です。

  • 依存方向を機械検査する import-linter
  • コミット前に止める pre-commit
  • AIの編集を“その場で”拒否する Claude のフック
  • 最後の砦の CI

📦 ソースコード:https://github.com/NeoSolleil/aim-trainer/tree/harness-only
harness-only ブランチ。main は今後の機能開発で変わるため、記事と一致する状態を固定したブランチです)

↓mainブランチはこちらです

なぜ「機械強制」が要るのか

第2回までの CLAUDE.md.claude/rules/ は助言でした。助言は破れます。「絶対に起きてはならないこと」(設計の根幹を壊す変更)は、人やAIの善意ではなく、機械で止めるしかありません。

そこで、同じ規約を3つの関門で多層に守ります。早い順に:

  1. 編集の瞬間 … Claude Code のフック(AIのツール使用を直前に拒否)
  2. コミットの瞬間 … pre-commit(ローカル)
  3. push の瞬間 … GitHub Actions CI(リモート)

同じ違反を三重に塞ぐことで、どこかをすり抜けても次で止まります。

A. import-linter — 依存方向を機械検査する

本プロジェクトの backend は Clean Architecture で、依存は内向き(外側→内側)のみという根幹ルールがあります。これを文章でなく機械検査にします。

import-linter は、Pythonの import 関係が契約に反していないかをチェックするツールです。pyproject.toml にレイヤ契約を書きます。

[[tool.importlinter.contracts]]
name = "Clean Architecture layer dependencies"
type = "layers"
layers = [
  "app.infrastructure",  # 外
  "app.adapters",
  "app.application",
  "app.domain",          # 内
]

これで「内側(domain)が外側(infrastructure 等)を import したら検査が失敗」します。

矢印は許される依存の向き(外→内)domain は誰にも依存せず、内側ほど安定した中核になります。

さらに「domain はそもそもフレームワークを import しない(純粋に保つ)」を forbidden 契約で縛りました。

[[tool.importlinter.contracts]]
name = "domain must be framework-free"
type = "forbidden"
source_modules = ["app.domain"]
forbidden_modules = ["fastapi", "sqlalchemy", "pydantic", "starlette", "uvicorn"]

「効くこと」を確認する(負のテスト)

仕組みは、実際に違反を検出できて初めて意味があります。試しに domain に import sqlalchemy を一行入れて検査を走らせると:

domain must be framework-free BROKEN
app.domain is not allowed to import sqlalchemy

ちゃんと失敗しました。挿入した行を消せば通ります。「書いたら止まる」を目で確認してから本採用する——この一手間が、ハーネスを“飾り”にしないコツです。

B. pre-commit — コミット前にまとめて止める

pre-commit は、git commit の直前に任意のチェックを自動実行する仕組みです。失敗したらコミットが中断されます。

backend は ruff(lint/整形)・pyright(型)・import-linter(依存方向)・xenon(複雑度)、frontend は ESLint・Prettier・tsc を走らせています。

「直接SQL禁止」を grep で機械化する

「ORM経由で書く(生SQLを書かない)」という規約は、import の有無では判定できません(“どう書いたか”の問題)。そこで、生SQLの入口(text(...) / exec_driver_sql)を grep で検出して落とす小さなスクリプトを pre-commit に足しました。

# backend/app 配下に生SQLの入口があれば失敗させる(抜粋)
if grep -rnE '(^|[^A-Za-z0-9_.])(text|exec_driver_sql)[[:space:]]*\(' \
     backend/app --include='*.py'; then
  echo "ERROR: 直接SQLを検出。ORM経由で書いてください。" >&2
  exit 1
fi

「機械で判定できない規約は、判定できる形に翻訳する」——完璧でなくても、主要な違反を確実に止められれば十分です。

C. Claude Code のフック — AIの編集を“その場で”拒否する

ここが目玉です。Claude Code には PreToolUse フックという仕組みがあり、AIがツール(ファイル編集やコマンド実行)を使う直前に介入して、許可・拒否を返せます

なぜ強力かというと、pre-commit や CI は「コミット後/push後」に止めますが、フックは編集しようとした瞬間に止められるからです。AIが間違ったコードを書く前に差し戻せます。

設定は .claude/settings.json に、対象ツールと実行スクリプトを書きます。

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Write|Edit",
        "hooks": [{ "type": "command",
          "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard_architecture.py\"" }] }
    ]
  }
}

スクリプト側は、編集先のパスと書き込む内容を受け取り、違反なら拒否のJSONを返します(抜粋)。

# 編集先が domain/application で、禁止フレームワークを import していたら拒否
if in_domain_or_app and FORBIDDEN_IMPORT.search(content):
    print(json.dumps({"hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": "domain/application は FW を import できません",
    }}))

これで、AIが backend/app/domain/foo.pyfrom sqlalchemy import ... を書こうとした瞬間、その編集自体が拒否されます。

実装上の注意:フックが壊れても作業を止めないよう、スクリプトは例外時に「許可」を返す設計(fail-open)にしました。後段の pre-commit と CI が最後の砦として残るので、ここで全部止める必要はありません。

ついでに、git commit --no-verify(pre-commit を迂回するオプション)も別のフックで禁止しました。せっかくの関門を、AI自身に飛び越えさせないためです。

D. GitHub Actions CI — 最後の砦

ローカルの pre-commit はうっかり無効化もできます。最後の砦として、push / PR 時に GitHub Actions で同じチェックを再実行します。ローカルをすり抜けても、リモートで止まります。public リポジトリなので Actions は無料・分数無制限で回せます。

設計思想:助言と強制の書き分け+多層防御

  • 書き分け:「これは助言」「これは機械強制」を曖昧にしない。.claude/rules/ には「この項目は import-linter で強制」と注記し、効かないものを“強制”と偽らない。
  • 多層防御:同じ規約を、編集時(フック)・コミット時(pre-commit)・push時(CI)で三重に。

まとめ

「お願い」を「破れない約束」に変えるのが、この③の層です。import-linter で依存方向を、grep で生SQLを、Claude のフックで“編集の瞬間”を、CI で最後を——多層で塞ぎました。

次の第4回は、この土台の上で何を・どう作るか仕様駆動のパイプライン(発見→仕様→設計→分解→実装)に進みます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?