この記事は、連載「言わない技術」の第 11 回、本編の最後です。各回はファイルやスクリプトを一つ置けば完結します。連載の全体像と各回の一覧は序論にあります。

先に、謝っておきます。このタイトルで、記事は書けませんでした。 私は毎回、AI にレビューをさせています。「読まない技術」の第 12 回から繰り返し書いてきた、セッションの終わりに必ず打つ一文——「ここまで不足点はないですか?」——が、レビューそのものだからです。前作「AIの意見を聞かない技術」を書き上げたときも同じで、AI に全体を読ませて、不足点を出させました。記録が残っています。

それでも、この回を書いています。レビューで返ってきた指摘を並べたときに、気づいたことがあったからです。指摘の半分は、同じものがリポジトリの中にあれば、置いてある自動テストが落としていた種類のものでした。落ちなかったのは、記事のコードブロックの中のコードを、その自動テストが見ていなかったからです。私に足りなかったのはレビューではありませんでした。置いた自動テストが何を見ていないかを、私が知らなかった。 そう分かったとき、書くべきものが「レビューをさせない」から「置いたものを誰が点検するか」に変わりました。

今回は、その話です。AI にレビューをさせれば、指摘はいくつも返ってきます。ただ、そのうち何件が、置いてある仕組みが拾えたはずのものかは、数えるまで分かりません。

前作「読まない技術」の第 6 回で、ルールを、破られたら落ちる自動テストに置き換えると書きました。第 1 回で引いたのと同じ回です。あのときは指示ファイルの側を見ました。今回は、置いた自動テストのほうを疑います

まず一つだけ、思い出してみてください。直近で AI にレビューをさせたときに出た指摘です。そのうち、あなたのリポジトリに既にある自動テストが拾えたはずのものは何件でしたか。

CC BY 4.0

前作のレビューで、返ってきたもの

前作を書き終えたとき、私はサブエージェントを 5 体立ち上げました。読者体験・技術・批判耐性・既存記事との整合の 4 体と、編集者の視点で通読する 1 体です。返ってきた指摘は、こうでした。

  • 記事の中のコードが 5 件、コピペで動かなかった(山括弧をシェルがリダイレクトと解釈する / 未定義の関数を呼ぶ / 正規表現が対象を素通りする)
  • 他人の主張の帰属が間違っていた。 引用した論の 1 つは、私が名前を挙げた人ではなく別の人の別の主張だった
  • 表記の揺れ(「判る」と「分かる」の混在)、番号の対応が壊れた箇所、呼称の不統一

並べて分けると、2 種類になります。1 つ目の、記事の中のコードが動かなかった 5 件は、同じものが app/ の中にあれば、置いてある自動テストが落としていた種類です。落ちなかったのは、自動テスト 26 本はリポジトリを見ていて、記事のコードブロックの中のコードは見ていないからです。同じ文字列でも、app/ にあれば落ち、記事のコードブロックの中なら通る。仕組みが見ている範囲の外は、こんなに近くにありました。

残りの 2 つは性質が違います。他人の主張の帰属も表記の揺れも、機械には真偽を決められません。「AIの意見を聞かない技術」の第 5 回で「禁止のうち機械に持たせられるのは半分以下」と書いた、同じ線引きです。

リポジトリの側は、先にレビューされています

私の手元の本番プロジェクト——typingtube という、YouTube の音楽動画でタイピング練習ができる Web サービスです——を数えました。

何が見ているか
仕様・原則を突き合わせる自動テスト26 本
コマンドや起動に割り込む hook6 本
pre-commit hook のチェック8 つ
増え方を見張るラチェット8 項目
全量テスト14,149 件(この記事を書いた時点で 0 失敗)

ここを通ったコードに、レビューで指摘できることは多くありません。 命名も、禁止された列も、翻訳の抜けも、デザインの逸脱も、コミットの手前で落ちます。「レビューさせろ」の前に、プロジェクトのほうが先にレビュアーになっています。

育つほど、これは進みます。指示ファイルには「Ruby 3.3.x」と書いてありますが、.ruby-version を開けば 3.3.6 です。実物のほうが正確で、書いた日から既にそうでした。 公式の点検コマンドも同じ線を引いていて、モデルがコードベースから導ける内容は削り、落とし穴と理由を残すと書かれています。コードが育つほど「導ける」側が増え、言うことは減ります。

だから、順番の話です

「AI にレビューをさせない」は、させるなという話ではありません。順番の話です。仕組みが落とせるものは仕組みが落とし、残ったところにレビューを当てる。 仕組みの無いところでレビューをやめたら、何も見ていないのと同じです。

問題は、その範囲を私が把握していなかったことです。26 本あることは知っていました。その 26 本が何を見ていないかは、数えるまで知りませんでした。

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

仕組みが見ている範囲は、疑って壊してみないと分からない。だから点検を頼む代わりに、自動テストが空回りしていないかを見る仕組みを置く。

仕組み: 空回りしている自動テストを、外から数える

置くのは自動テスト 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

走査する自動テストは、「違反が無い」と「そもそも何も見ていない」を同じ合格で返します。ファイルが移動しただけで対象は空になり、黙って空回りします。置いた最初の実行で、1 本見つかりました。

走査しているのに最低件数を表明していない自動テストがある。

  • e2_pixel_art_not_in_core_jobs_test.rb

ディレクトリの存在だけは確かめていて、中のファイルが 1 件も無くても合格でした。1 行足して塞ぎました。

この形にはもう一つ意味があります。3 作 37 本のうち、「検証手順」を置いたのは 30 本。そのうち 13 本が、これと同じ形でした。

検証手順
前作 第 4 回外して、止まることを確かめる
前作 第 6 回破って、落ちることを確かめる
第 1 回機構の無い依頼を足して、落ちることを確かめる
第 6 回訳から見出しを落として、失敗になることを確かめる
第 9 回実在するが目印の無いファイルを名指しして、落ちることを確かめる

その 13 本でやっていたのは、置いた仕組みを壊すことでした。 名前を付けていなかっただけです。この回の自動テストは、それを人がやらなくても一度は目に入る形にしたものです。

CC BY 4.0 はここまで

させなくなったもの

「全体をレビューして」です。レビュー自体はやめていません——前作は 5 体に読ませました。やめたのは、仕組みが落とせる範囲まで含めて頼むことです。いまは範囲を切ってから頼みます。

CC BY 4.0

書けなかったタイトルは、3 つありました

冒頭で謝ったとおり、この回はそのままでは書けませんでした。 連載の候補に挙げて落としたタイトルが、3 つあります。

採否落とした/通した根拠
計画を書かない技術docs/plan/計画書が 200 本あった
計画を書けと言わない技術指示ファイル側に「計画書を書け」が0 件だった(第 4 回
AI にレビューをさせない技術❌ → ✅前作の記録にレビューさせた記録が残っていた。中身を変えて通した(この回)

3 件とも、決めたのは記憶ではなく記録のほうです。 そして私は、2 つとも逆を思っていました——「計画は書いていないはず」「レビューは、毎回の一文だけのはず」。証明されたのは「仕組みが効く」ではありません。

記録が残っていると、思い込みのほうが落ちる。

注意: 落ちたことのない自動テストは、まだ何も証明していません

一つ、この自動テストは書き方しか見ていません。 「最低件数の表明があるか」を静的に見るだけなので、>= 0 と書けば通ります。妥当な数かは測れません(第 1 回から数えて 4 度目の同じ死角です)。

二つ、26 本という数は自慢ではありません。 小さなプロジェクトなら 2 本で足ります。見てほしいのは、その 2 本が何を見ていないかを言えるかのほうです。

三つ、落ちたことのない自動テストは、まだ何も証明していません。 前作の第 6 回のとおり、守っている自動テストと壊れている自動テストは失敗を一度見るまで区別が付きません。この回の自動テストも、1 本見つけて初めて動いていると分かりました。

検証手順: 自動テストの側を、二方向から壊す

前提

  • 走査する自動テストが数本あり、それぞれに「走査対象が N 件以上あった」の表明が入っていること
  • それを外から数えるメタテスト(自動テストを見る自動テスト)を置き終えていること
  • 作業中の変更が無い状態(git status が空)から始めること

所要時間: 15 分

この回の自動テストは自分と同じ壊れ方をしうるので、確かめる向きが 2 つあります。

手順

  1. あなたが、見られる側を壊します。走査する自動テストのどれか 1 本から、最低件数の表明を 1 行消します(assert_operator files.size, :>=, 1 に当たる行)。メタテストを走らせ、結果を控えます
  2. あなたが、消した 1 行を戻し、メタテストが合格に戻ることを確かめます
  3. あなたが、今度はメタテスト自身を壊します。メタテストが走査する先のディレクトリ名を、存在しない名前に変えます。メタテストを走らせ、結果を控えます

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

  • 手順 1 で、メタテストが落ちる
  • 手順 1 のメッセージに、表明を消したファイルの名前が出ている
  • 手順 3 で、メタテストが自分で落ちるExpected 0 to be >= 20 に当たる形)

合格しなかったとき

  • 手順 1 で「どれかが足りない」だけ出て、名前が出ない —— 直す側(AI でも、あなたでも)が、数本の中から探すことになります
  • 手順 3 で落ちない —— ⚠️ ここが本体です。 0 件で合格になる自動テストを探す自動テストが、0 件で合格になっています。このメタテストは、置いた日から何も見ていません

後始末

  • ディレクトリ名を戻し、合格に戻ることと git diff が空になることを確かめます

CC BY 4.0 はここまで

次回は最終回、「何もしない技術」です。11 回かけて渡す言葉を減らしてきました。この回だけでも、私は自分の手元を 2 回数え違えています。

連載「言わない技術」