この記事は、連載「AIの意見を聞かない技術」の第 5 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。

今回は、自動テストも hook(コマンド実行などの直前・直後に割り込むスクリプト)も書いていない禁止事項の話です。禁止事項というのは、開発のときに許さないと決めたルールのことで、私はそれを 1 件ずつ項目にして、一覧にしています。この記事で禁止と呼ぶのは、禁止事項の 1 件のことです。自動テストは書きます。自動テストを書かないのは、破られているかどうかをプログラムが決めることのできない禁止だけです。私の一覧には 17 件あります。そのうち、自動テストが見ていると言い切ることのできるのは 4 件で、残りの 13 件は、破られているかどうかをプログラムが決めることができません。禁止が破られていないかを、プログラム(自動テストや hook)で確かめることを、私は機械検証と呼んでいます。私は、その 13 件の項目それぞれに、機械検証にしないと決めたことを隣に書くことにしました。

前作「読まない技術」の第 6 回で、私は、ルールを、破られたら落ちる自動テストに置き換えると書きました。今回は、自動テストに置き換えることのできなかったほうの禁止の話です。

まず一つだけ、確かめてみてください。

禁止を 1 つ選んで、それが今この瞬間に破られていたら、何が起きるかを答えます。

自動テストが落ちるでしょうか。

CI が止まるでしょうか。

誰かが気づくでしょうか。

どれでもなければ、その禁止は書いてあるだけです。

CC BY 4.0

「全 locale に翻訳を追加する」と書いてあったのに、5,525 件欠けていた

私の手元の本番プロジェクト——typingtube という、YouTube の音楽動画でタイピング練習ができる Web サービスです——には、こういう行動ルール(AIが読むメモリに、守ってほしい行動を 1 件ずつ書いたもの)がありました。「fallback で済ませず、全 locale に翻訳を追加する」。十数言語で提供しているサービスなので、当然の方針です。文面に曖昧さはなく、私はこのルールを、AIにずっと渡していました。

このルールを見る自動テストが無かったので、AIがルールを破っていても、誰も気づきませんでした。

私が数えてみたら、5,525 件の翻訳が欠けていました(2026-08-31 実測)。755 ある翻訳ファイルのうち、241 のファイルに欠落があり、ファイルがまるごと無いものも 2 つありました。欠けていた翻訳のうち 31 件は、翻訳が無いとキーの名前がそのまま画面に出る場所でした。

数えてみて一番こたえたのは、内訳のほうです。日本語と英語だけ揃っている箇所が、多くありました。AIが文言を足すたびに翻訳を 2 言語で止める形が、何度も繰り返されていたということです。

翻訳のルールを自動テストにできるかどうかを、私はそれまで決めていませんでした。行動ルールには、自動テストがあるかどうかを書く場所がありません。自動テストにするかどうかを決めていないルールも、決めたルールも、行動ルールの中では同じ一行です。

私は、翻訳のこのルールを、pre-commit hook(コミットの直前に走るスクリプト)に移しました。翻訳のキーが言語ごとに揃っているかどうかは、hook が数えることができるからです。

禁止を全部自動テストにしようとして、できませんでした

それなら、禁止を全部、自動テストにすればいいはずです。

私もそう思って、17 件の禁止を一つずつ見ていきました。

自動テストにできたのは 4 件でした。

残った 13 件の 1 つは、キャラクター機能のコストを引き下げる提案を却下する、という禁止です。第 2 回で、私が理由を 3 行書いたものです。コストの数字が変わったかどうかなら、プログラムが比べることができます。でも、私が禁止しているのは、引き下げの提案を通すことのほうです。提案を通してよいかを決めるには、提案の理由まで読んで、「この経済バランスは意図どおりか」を判断する必要があります。判断の中身は、初心者がなんとなく設定したキャラクターが、他の人の体験を壊すかどうかです。他の人の体験を壊すかどうかは、数字を比べても決まりません。

プログラムが決めることのできない禁止は、ほかにもあります。サービスの収益構造の方針、レベルの推薦をしないこと、SEO のテキストを変えるときに根拠を要求すること、などです。どれも、守られているかどうかを、数えて決めることができません。

禁止を自動テストにするときは、その禁止を、数えることのできる状態に置き換えます。置き換えたあとに守られるのは、自動テストが数えている状態のほうです。第 3 回で扱った色リテラルの禁止が、その実例でした。#fff のように色を直に書かない、という禁止を守らせるために、私は、色リテラルの件数を数える pre-commit hook を置いていました。hook が数えていたのは CSS ファイルだけで、AIは、その範囲を、そのまま禁止の範囲として扱いました。

数えている状態が禁止の中身と重なっていれば、禁止を自動テストに置き換えてかまいません。翻訳のキーが言語ごとに揃っているかどうかは、翻訳のルールの中身と重なります。コストの禁止を数字の比較に置き換えると、数字が動かなければ通る、という別の禁止になります。だから私は、コストの禁止を自動テストにしません。

つまり、私の禁止の半分以上は、人が判断する禁止です。

17 件を見終わってから一覧を読み返すと、もう一つ分かったことがありました。自動テストにしないと決めた 13 件のうち、いくつかの項目は、書いてある文が、私が見る前と変わっていませんでした。決めたことは、私の頭の中にしかありません。外から見ると、その項目は、冒頭の翻訳のルールと同じ、まだ自動テストにするかどうかを決めていないルールに見えます。

自動テストにしないと決めた項目が、一覧の上では、まだ決めていない項目と同じに見えています。このとき、あなたなら次のどちらを選びますか。

  • 一覧の全文を読み直して、どの項目をプログラムが見ていて、どこから人が判断しているのかを、そのたびに確かめる
  • 自動テストにしないと決めた項目の隣に、「機械検証: なし」と、自動テストにしない理由を書く

答えは、「なし」と書くほうです。同じに見えたのは、自動テストにしないと決めたことが、私の頭の中にしか無かったからです。全文を読み直しても、AIにも私にも、読むことができるのは同じ文なので、見分けは付きません。「なし」と書けば、決めたことが一覧の文に残り、何も書いていない項目は、まだ決めていない項目だけになります。冒頭の翻訳のルールで 5,525 件の欠落が積み上がったのも、自動テストにするかどうかを決めていないあいだでした。

理解したことは一文にできます。

自動テストにできない禁止は、自動テストにしないと決めても、そう書かなければ、まだ決めていない禁止と見分けが付かない。だから、自動テストにしないと決めたことを、禁止の隣に書いておく。

なぜ、自動テストにしないと決めた禁止は、書かなければ、まだ決めていない禁止と見分けが付かないのか

自動テストが見ているのも、hook が見ているのも、ファイルの中の状態です。翻訳を見る pre-commit hook なら、日本語のファイルにあるキーが、他の言語のファイルにも有るかどうかを見ています。有るかどうかは数えれば分かるので、ルールが守られているかどうかを、プログラムが決めることができます。

書いてある一行のほうは、守られているかどうかを数えるプログラムがありません。その一行に従うかどうかを決めるのは、AIです。Claude Code のドキュメントは、指示ファイル(CLAUDE.md のように、毎回AIに読み込まれるルールの置き場)もメモリも文脈であって、強制される設定ではないと書いています。同じページには、AIは指示を読んで従おうとするが、厳密に従うという保証は無い、とあります。曖昧な指示や矛盾する指示では、とくにそうだ、というのが同じページの説明です。従われなかった回を数えるプログラムが無ければ、欠けた翻訳は、誰にも気づかれないまま残ります。5,525 件は、私が数えるまで、誰も気づかなかった数です。

ルールを hook に移すと、従うかどうかをAIが決める場面が無くなります。Claude Code の hook のドキュメントは、hook が決定論的な制御を与えると書いています。AIがその動作を選ぶかどうかに頼らず、動作が必ず起きる、という説明です。翻訳のルールが守られているかどうかは、hook に移した時点から、コミットのたびに hook が数えて決めています。だから私が自動テストや hook に移すのは、プログラムが決めることのできる禁止です。コストの禁止のように、プログラムが決めることのできない禁止は、書いてある一行のままで、従うかどうかは、読むたびにAIが決めます。

書いてある一行のまま残る禁止には、自動テストにしないと決めて残したものと、まだ決めていないものがあります。そして、自動テストにしないと決めたこと自体は、書かなければ、ディスクのどこにも残りません。Claude Code のドキュメントは、AIとの始まりから終わりまで一続きのやり取り(セッション)について、セッションは独立していて、新しいセッションは前のセッションの会話履歴を持たないと書いています。次のセッションへ渡るのは、ディスクにあるものだけです。私が頭の中で決めたことは、次のセッションのAIには届きません。届くのは一覧に書いてある文だけで、自動テストにしないと決めた項目と、まだ決めていない項目は、書いてある文が同じです。半年後の私が見るものも、同じ文です。

見分けが付かないと、まだ決めていない項目を、一覧の上で見つけることができません。その項目に自動テストを書くことができるかを再検討するのは、誰かが思い出したときだけです。冒頭の翻訳のルールは、自動テストにできるルールでしたが、私が決めていないあいだは書いてある一行のままで、そのあいだに 5,525 件の欠落が積み上がりました。

仕組み: 「機械検証」の見出しに、「なし」と、自動テストにしない理由を書く

第 1 回で、私は禁止を 1 枚のファイルに集めて、一覧にしました。その一覧には、項目ごとに「機械検証」の見出しがあります。その禁止を見ているもの(自動テストや hook)があるかどうかを、ここに書きます。今回書くのは、その中身です。

### NG3 ユーザー投稿物に名前フィールドを持たせない

**結論**: キャラクター・ドット絵・背景などの投稿物は、名前の列を持たない。
識別はビジュアルと設定で行う。

**理由**: (なぜ禁止なのか。ここは第 2 回の主題)

**適用範囲**: 対象 4 モデルへの `name` / `title` / `nickname` 等の列追加を禁止

**関連 memory**: `feedback_no_naming_for_ugc.md`

**機械検証**: あり。`a1_name_field_absence_test.rb` が、その 4 モデルに
該当する列が無いことをスキーマの側から検査する

この例の最後の 1 行が、「機械検証」です。自動テストがある項目には、私は、どの自動テストかを書きます。自動テストの無い項目には、私はこう書きます。

**機械検証**: なし(推薦 UI の機械検知は実装ドメイン横断のためレビューで担保)
**機械検証**: なし(プロセスルール、レビューで担保)
**機械検証**: なし(条文の書き方なので、追加・改訂時のレビューで見る)

何も書かないことと、「なし」と書くことは、別のことです。何も書いていない項目を見ても、AIにも私にも、「まだ検討していない」のか「検討して、プログラムには無理だと決めた」のか、見分けが付きません。「なし」と書いた瞬間に、その項目は決着済みになり、書いてあるだけの禁止から、人が判断すると決めた禁止に変わります。上の 3 行のように、理由の欄には、プログラムにできない訳と、破られていないかを代わりに誰がどこで見るか(レビューや運用の手順)を書きます。冒頭の 3 つ目の問い「誰かが気づくでしょうか」の「誰か」を、一覧の上に書いておく場所が、この欄です。

私の一覧は 17 項目で、内訳は、「あり」が 4 件、「なし」が 10 件です。自動テストが部分的に見ているだけで、「あり」か「なし」かを決めきっていない項目が 2 件あります。残りの 1 件には、「機械検証」の見出しそのものがありません。

「なし」の 1 件には、自動テストの名前が書いてあります。第 3 回で扱った、判定軸(禁止に入るかどうかを、何で数えて決めるか)が 3 つある禁止です。禁止に入るかどうか迷うケースを見る自動テストはありますが、自動テストが見ているのはその禁止の一部だけなので、私は「なし」の側に置いています。

見出しそのものが無い 1 件には、この回を書くために数えたとき、初めて気づきました。「機械検証」を書く場所を決めておくと、まだ書いていない項目まで、私に見えるようになります。

半分以上が「なし」になるのは、プログラムが決めることのできる禁止を、自動テストにした結果です。残ったのは、人が判断する禁止でした。「機械検証」の見出しは、その内訳を、一覧の上で見えるようにしています。

「なし」と書くときに、もう一つ決めることがあります。その禁止を残すかどうかです。残す基準は、その禁止を作るきっかけになった失敗が、今も再現するかどうかです。「AIがまだこの禁止を必要としているか」で考えると、どの禁止も要りそうに見えて、私は 1 つも消すことができません。Claude Code のドキュメントが、指示ファイルに指示を書き足す場面の最初に挙げているのも、AIが同じ誤りを二度目にしたときです。

「機械検証」の見出しがあるのは、一覧に載せた項目だけです。冒頭の翻訳のルールのように、一覧の外にあるルールには、この見出しがありません。そのルールを一覧へ上げるかどうかを決めているのは、いまも私です。

CC BY 4.0 はここまで

読み直さなくなったもの

禁止事項の一覧そのものです。「今どこまでプログラムが見ていて、どこから人が判断しているのか」を知るために、私は一覧の全文を読み直していました。いまは、各項目の「機械検証」の 1 行を見るだけで、項目が増えても、読むのはその 1 行ずつです。

CC BY 4.0

注意: 「あり」は空回りし、「なし」は逃げ道になる

「機械検証」の「あり」にも「なし」にも、壊れ方があります。

「あり」の壊れ方は、空回りです。走査対象をパスの指定でまとめて集める自動テストは、そのパスを打ち間違えると 0 件を走査して、永遠に通ったままになります。その自動テストには、「走査対象が N 件以上あった」ことを確かめる行を、必ず併置してください(詳しくは前作の第 6 回に書きました)。

「なし」の壊れ方は、逃げ道になることです。書くのが面倒な自動テストを、私は「なし(レビューで担保)」と書いて済ませることができてしまいます。「あり」と「なし」の線引きは単純で、破られているかどうかをプログラムが決めることができるかどうか、それだけです。プログラムが決めることができるのに「なし」と書いたなら、それは、自動テストを書くことの先送りです。半年後の私は、その「なし」が先送りだったのか、検討して決めたものだったのかを、見分けることができません。その場で自動テストを書いてしまうほうが、結局は安上がりでした。

検証手順: 禁止をわざと破って、自動テストが落ちることを確かめる

前提

  • 禁止事項の一覧に「機械検証」の見出しを足し、「あり」と書いた項目が 1 つ以上あること
  • 作業中の変更が無い状態(git status が空)から始めること

所要時間: 10 分

手順

  1. あなたが、「機械検証: あり」と書いた項目を 1 つ選び、そこに名前が書いてある自動テストを開きます。自動テストの名前が書かれていないなら、確かめる前に、どの自動テストがその禁止を見ているのかを、まず項目に書いてください
  2. あなたが、その禁止への違反をわざと 1 つ作ります。自動テストが走査している範囲の中に書きます
  3. あなたが、その自動テストを走らせます

合格条件(すべて満たすこと)

  • 自動テストが落ちる
  • メッセージに、手順 2 であなたが書いた違反の場所が出ている

合格しなかったとき

  • 落ちなかった —— その項目の「あり」は嘘です。あなたが、「機械検証: なし(レビューで担保)」へ直すか、自動テストの走査の範囲を広げるか、どちらかをその場で決めます
  • 落ちたが、場所が出ない —— 違反を直す側(AIでも、あなたでも)が、違反の場所を毎回探すことになります。あなたが、自動テストのメッセージに違反の場所を含める形へ直します

後始末

  • あなたが、手順 2 で書いた違反を消し、自動テストが合格に戻ることと、git diff が空になることを確かめます

禁止を破ったのに落ちない自動テストは、無い自動テストより悪いです。

今回私が置いたのは、一覧を読み直さずに済ませるための 1 行でした。その 1 行が嘘なら、一覧を読み直さないと私が決めたことは、その時点で根拠を失っています。


CC BY 4.0 はここまで

次回は「AIの確認依頼を断る技術」。AI に「画面を開いて目視で確認してください」と言われても、私は開きません。 それでも、壊れたまま先へ進むことはありません。

連載「AIの意見を聞かない技術」