この記事は、連載「言わない技術」の第 11 回、本編の最後です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。
先に、謝っておきます。このタイトルで、記事は書けませんでした。セッション(AIとの、始まりから終わりまで一続きのやり取り)のたびに、AIにレビューをさせているからです。1 作目「読まない技術」の第 12 回から繰り返し書いてきた、セッションの終わりに必ず打つ一文——「ここまで不足点はないですか?」——が、そのレビューです。前作「AIの意見を聞かない技術」を書き上げたときも同じで、AIに全体を読ませて、不足点を出させました。記録が残っています。それでもこの回があるのは、置いた自動テストが何を見ていないかは、レビューの指摘に出てこなかったからです。自動テストは、何も見ずに合格する空回りをしていても、同じ合格を返します。
それに気づいたのは、1 作目のレビューで返ってきた指摘を並べたときです。指摘のうち、記事の中のコードが動かなかった 5 件は、動かしてみれば分かる誤りでした。それでも、どの自動テストも落ちませんでした。置いてある自動テストに、記事のコードブロックの中のコードを動かすものが無かったからです。
に足りなかったのはレビューではありませんでした。置いた自動テストが何を見ていないかを、は知りませんでした。そう分かったとき、この回で書くことが、「レビューをさせない」から「置いた自動テストを誰が点検するか」に変わりました。
今回は、その話です。AIにレビューをさせれば、指摘はいくつも返ってきます。ただ、そのうち何件を、置いてある自動テストが拾うことができたのかは、数えるまで分かりません。
1 作目「読まない技術」の第 6 回で、はルールを、AIが破ったら落ちる自動テストに置き換える、と書きました。この連載の第 1 回で引いたのと同じ回です。その回でが見たのは、指示ファイル(CLAUDE.md や AGENTS.md)に書いたルールの側でした。今回は、置いた自動テストのほうを疑います。
まず一つだけ、思い出してみてください。
直近でAIにレビューをさせたときに出た指摘です。
そのうち、リポジトリに既にある自動テストが拾うことのできたはずのものは、何件でしたか。
CC BY 4.0
1 作目のレビューで、返ってきたもの
1 作目を書き終えたとき、はサブエージェントを 5 体立ち上げました。読者体験・技術・批判耐性・既存記事との整合をそれぞれ見る 4 体と、編集者の視点で通読する 1 体です。返ってきた指摘は、こうでした。
- 記事の中のコードが 5 件、コピペで動かなかった(山括弧をシェルがリダイレクトと解釈する / 未定義の関数を呼ぶ / 正規表現が対象を素通りする)
- 他人の主張の帰属が間違っていた。引用した論の 1 つは、が名前を挙げた人とは別の人の、別の主張だった
- 表記の揺れ(「判る」と「分かる」の混在)、番号の対応が壊れた箇所、呼称の不統一
この指摘を性質で分けると、2 種類になります。1 つ目は、プログラムで見つけることのできる種類です。記事の中のコードが動かなかった 5 件は、動かしてみれば分かる誤りでした。表記の揺れも、揃えたい語の組を決めておけば、プログラムで数えることができます。それでも自動テストは落ちませんでした。26 本の自動テストには記事のファイルを読むものもありましたが、記事のコードブロックの中のコードを動かすものも、記事の表記の揺れを数えるものも、ありませんでした。仕組みが見ている範囲の外は、こんなに近くにありました。
2 つ目の、他人の主張の帰属は、性質が違います。帰属が正しいかは、引いた先の原文と読み比べなければ決まらず、プログラムでは真偽を決めることができません。前作「AIの意見を聞かない技術」の第 5 回で、破られているかどうかをプログラムが決めることができるかどうかで、自動テストにする禁止と、人が判断する禁止を分けたのと、同じ線引きです。
リポジトリの側は、先にレビューされています
の手元の本番プロジェクト——typingtube という、YouTube の音楽動画でタイピング練習ができる Web サービスです——で、コードを見ている仕組みを数えました。
| 何が見ているか | 数 |
|---|---|
| 仕様・原則を突き合わせる自動テスト | 26 本 |
| コマンドや起動に割り込む hook | 6 本 |
| pre-commit hook のチェック | 8 つ |
| 増え方を見張るラチェット | 8 項目 |
| 全量テスト | 14,149 件(この記事を書いた時点で 0 失敗) |
これらの仕組みが落とす種類の誤りは、AIにレビューを頼むより前に、仕組みが見ています。翻訳の抜けとデザインの逸脱は、コミットの手前で pre-commit hook が落とします。禁止された列の使用は、自動テストを走らせた時点で落ちる誤りです。その範囲では、リポジトリに置いた仕組みが、レビュアーの役を果たしています。
だから、順番の話です
この回のタイトルの「AI にレビューをさせない」は、レビューを当てる順番の話です。仕組みが落とすことのできるものは仕組みに落とさせ、残ったところにレビューを当てます。仕組みの無いところでがレビューをやめたら、そこは誰も見ていないのと同じです。
問題は、仕組みが見ている範囲を、が把握していなかったことです。自動テストが 26 本あることは、知っていました。その 26 本が何を見ていないかは、数えるまで知りませんでした。
自動テストの合格が言うのは、見た範囲に違反が無いことだけです。範囲の外に誤りがあっても、自動テストは合格を返します。記事のコードブロックがそうでした。ファイル群を走査して違反を探す自動テストでは、範囲そのものが空になっても、合格は変わりません。「違反が無い」と「そもそも何も見ていない」を、同じ合格で返します。
あなたが置いた自動テストの中に、ファイル群を走査して違反を探すものがあるとします。置いた日から、一度も落ちたことがありません。AIに全体のレビューを頼んだときも、その自動テストについての指摘はありませんでした。あなたなら次のどちらにしますか。
- AIに、自動テストのファイルも含めて、もう一度全体をレビューしてもらう
- 走査する自動テストが、対象が 0 件のときに落ちる行を持っているかを、別の自動テストで調べる
答えは、別の自動テストで調べるほうです。一度も落ちていない走査の自動テストは、違反が無くて合格しているのか、何も見ずに合格しているのかが、合格からは分かりません。どちらも同じ合格を返すからです。そして、0 件のときに落ちる行を持っているかは、自動テストのファイルを読めば、プログラムで決まる種類のことです。ここで、さきほど書いた順番が効きます。プログラムで決まるものは仕組みに落とさせ、レビューは残ったところに当てます。もう一度全体のレビューを頼むのは、仕組みに落とさせることのできるものを、レビューの側へ戻すことです。対象が 0 件のときに落ちる行があれば、何も見ていない自動テストは、合格せずに落ちます。その行を持たない自動テストを名前つきで探し出すのは、別の自動テストの仕事で、あなたが一本ずつ壊して確かめる手間は要らない、ということです。
理解したことは一文にできます。
自動テストの合格は、見た範囲に違反が無いことしか言わず、範囲が空になっても変わらない。だから、範囲が空になったら落ちるかは仕組みに見させ、AIのレビューは、仕組みの範囲の外に当てる。
仕組み: 空回りしうる自動テストを、外から数える
置くのは、自動テストを見る自動テスト(メタテスト)1 本です。メタテストが調べる対象は、ほかの自動テストのファイルです。
# test/reference/zero_target_guard_test.rb(手元のコードを短くしたもの)
SCANNING = /Dir\.glob|Dir\[|\.glob\(/ # ファイル群を走査している
MINIMUM = /assert_operator[^\n]*:>=|MIN_[A-Z_]*\s*=|refute_empty/ # 0 件なら落ちる、の表明
SCANNED_MIN = 20 # ⚠️ この自動テスト自身の読み先が壊れたら落ちる
test "checks that scan a set of files never go green on zero targets" do
files = Dir.glob(DIR.join("*_test.rb")).reject { |p| p == __FILE__ }
assert_operator files.size, :>=, SCANNED_MIN, "this check is looking at the wrong place"
unguarded = files.filter_map do |path|
body = File.read(path)
next unless body.match?(SCANNING)
next if body.match?(MINIMUM)
File.basename(path)
end
assert_empty unguarded, "these checks scan files but never state a minimum. " \
"they stay green on zero targets, so nobody notices when they break\n - " + unguarded.join("\n - ")
end
対象のファイルが移動しただけで、走査する自動テストが見る対象は空になり、合格のまま空回りします。メタテストを置いて最初に走らせたとき、対象が 0 件でも合格する自動テストが 1 本見つかりました。
走査しているのに最低件数を表明していない自動テストがある。
e2_pixel_art_not_in_core_jobs_test.rb
見つかった自動テストは、ディレクトリがあることだけを確かめていて、中のファイルが 1 件も無くても合格になるものでした。は最低件数の表明を 1 行足して、ファイルが 0 件なら落ちるようにしました。
メタテストが見つけるのは、対象が空になっても合格する自動テストです。記事のコードブロックのように、どの自動テストの範囲にも入っていない場所は、メタテストでも見つかりません。そこは、レビューを当てる場所のままです。
メタテストの形には、もう一つ意味があります。3 作 37 本のうち、「検証手順」を置いたのは 30 本で、そのうち 13 本の検証手順が、メタテストと同じ形でした。
| 回 | 検証手順 |
|---|---|
| 1 作目 第 4 回 | 外して、止まることを確かめる |
| 1 作目 第 6 回 | 破って、落ちることを確かめる |
| 第 1 回 | 機構の無い依頼を足して、落ちることを確かめる |
| 第 6 回 | 訳から見出しを落として、失敗になることを確かめる |
| 第 9 回 | 実在するが目印の無いファイルを指定して、落ちることを確かめる |
その 13 本の検証手順の中身は、置いた仕組みをわざと壊すことでした。が、その手順に名前を付けていなかっただけです。メタテストは、人が手で壊さなくても、対象が 0 件でも合格する自動テストを、名前つきで見つけ出します。
CC BY 4.0 はここまで
AIにさせなくなったもの
がAIにさせなくなったのは、「全体をレビューして」という頼み方です。レビューそのものは続けていて、1 作目の原稿も、5 体のサブエージェントに読ませました。やめたのは、仕組みが落とすことのできる範囲まで含めて頼むことです。いまは、仕組みが見ている範囲を除いてから、残りのレビューを頼みます。
CC BY 4.0
そのままでは書くことができなかったタイトルは、2 つありました
冒頭で謝ったとおり、この回は、タイトルのとおりには書くことができませんでした。連載の候補に挙げたタイトルが、3 つあります。
| 案 | 採否 | 落とした/通した根拠 |
|---|---|---|
| 計画を書かない技術 | 落とした | docs/plan/ に計画書が 200 本あった |
| 計画を書けと言わない技術 | 通した | 指示ファイル側に「計画書を書け」が0 件だった(第 4 回) |
| AI にレビューをさせない技術 | 落とした → 通した | 1 作目の記録にレビューさせた記録が残っていた。中身を変えて通した(この回) |
3 件とも、採否を決めたのは記録です。
しかも、一度は落とした 2 つについてが覚えていたのは、「計画は書いていないはず」「レビューは、毎回の一文だけのはず」で、どちらも実際とは反対でした。この表から分かったのは、仕組みが働くかどうかとは別のことでした。
記録が残っていると、思い込みのほうが落ちる。
注意: 自動テストが働くと分かるのは、一度落ちたあとです
一つ、メタテストが見るのは、自動テストの書き方だけです。「最低件数の表明があるか」を静的に見るだけなので、>= 0 と書けば通ります。表明した数が妥当かどうかは、メタテストの対象の外です(この連載の第 1 回から数えて、4 度目の同じ死角です)。
二つ、26 本という数は自慢ではありません。小さなプロジェクトなら 2 本で足ります。あなたに確かめてほしいのは、その 2 本が何を見ていないかを、言うことができるかどうかです。
三つ、自動テストが働くと分かるのは、一度落ちたあとです。1 作目の第 6 回に書いたとおり、守っている自動テストと壊れている自動テストは、失敗を一度見て初めて区別が付きます。メタテストも、対象が 0 件でも合格する 1 本を見つけて初めて、働いているとに分かりました。
検証手順: 自動テストの側を、二方向から壊す
前提
前提は、走査する自動テストが数本あり、それぞれに「走査対象が N 件以上あった」の表明が入っていることです。それらの自動テストに表明があるかを外から調べるメタテストも、置き終えておきます。作業中の変更が無いこと(git status が空)も確かめておきます。
所要時間: 15 分
メタテストも、調べる相手の自動テストと同じく、空回りすることがあります。そのため、確かめる向きが 2 つあります。
手順
- あなたが、メタテストが調べる相手の自動テストを壊します。走査する自動テストのどれか 1 本から、最低件数の表明を 1 行消します(
assert_operator files.size, :>=, 1に当たる行)。メタテストを走らせ、結果を控えます - あなたが、消した 1 行を戻し、メタテストが合格に戻ることを確かめます
- あなたが、今度はメタテスト自身を壊します。メタテストのコードに書いてある、走査する先のディレクトリ名を、存在しない名前に書き換えます。メタテストを走らせ、結果を控えます
合格条件(すべて満たすこと)
合格の条件は 3 つです。手順 1 では、メタテストが落ち、そのメッセージに表明を消したファイルの名前が出ていることを確かめます。手順 3 では、メタテスト自身が落ちること(Expected 0 to be >= 20 に当たる形)を確かめます。
合格しなかったとき
- 手順 1 で、「どれかが足りない」とだけ出て、ファイルの名前が出ない —— 直す側(AIでも、あなたでも)が、数本の中から探すことになります
- 手順 3 で、メタテストが落ちない —— この検証手順で一番確かめたかったのは、ここです。空回りしている自動テストを探すメタテストが、自分も空回りしています。このメタテストは、置いた日から何も見ていません
後始末
ディレクトリ名を戻し、メタテストが合格に戻ることと、git diff が空になることを確かめます。
CC BY 4.0 はここまで
次回は最終回、「何もしない技術」です。11 回かけて渡す言葉を減らしてきました。この回だけでも、は自分の手元を 2 回数え違えています。
連載「言わない技術」
- ← 前回: 第 10 回 質問に答えない技術
- → 次回: 最終回 何もしない技術
- 全回の一覧: 序論