AI

レビュアーの判断は黙って失敗する

  • AI
  • エージェント
  • 品質管理
  • 検証
  • CI
  • Git

画面の検証手順を規約として明文化した3週間後、レビュー担当がそれを実行していなかったことが分かりました。対応は規約をもう1つ足すことではなく、検査を人の手から外して、通らなければマージそのものを機械に拒否させることでした。

ブログ「保存した」は検証ではないエージェントに作らせた画面の見た目を、数値ゲート・オフスクリーンのPNG・実際の導線の三段で確認しています。撮影までは自動化できても、撮った絵を見て判断する行為だけは自動化できていません。

規約は守られなかった

発覚したのは、レイアウトの破綻が長期間見逃されていたことからでした。エージェントの報告には画像のパスが書いてあり、レビュー担当はそれを開いていませんでした。規約には「報告にパスが書いてあるだけでは受け入れない。自分で開く」と書いてあります。

規約が無かったのではなく、規約があって実行されていませんでした。この違いが対応を決めます。

書いてある内容に問題があるなら、書き直せば直ります。書いてある内容が正しくて実行されないなら、書き直しても直りません。同じ人が同じ理由でまた飛ばします。

規約に残っている設計原理はこうです。

レビュアーの判断は黙って失敗する。食い違いを表示するゲートは黙らない。 だから「レビュー担当が検査を飛ばし続ける」ことへの答えは、レビュー担当向けの規約をもう1本書くことではない。

マージを1つの扉に通す

発覚したその日に、受け入れゲートを実装しています。

やっていることは単純で、バージョン管理そのものにマージを拒否させます。ゲートのスクリプトが書き出す使い捨てのトークンを持たない更新は、フックが弾きます。手で merge を叩いても通りません。

順序に工夫があります。先にマージして、マージ後の状態に対して全部のゲートを走らせ、最初の1つが落ちた時点で巻き戻すという形です。

ff-merge → ビルド → 取り込み → 単体テストと規約監査 → レイアウト掃引 → E2E
                                       ↓ どれか1つでも落ちる
                                  main を巻き戻す

この順序を選んだ理由も書かれていて、いちばん失敗が多かった手を潰すためだそうです。先に受け入れて、残りは後で走らせるという手。後で、はありません。後で、はリセットの後です。

もう1つ、飛ばすためのフラグを作っていません。掃引の範囲を広げる引数はありますが、狭める引数はない。そういうフラグは、いずれ全員の通り道になるからです。

ゼロ件を求めない

検査を機械化するとき、いちばん短命なのは「違反ゼロでなければ通さない」形です。既存の指摘を全部片付けるまで永久に赤いので、1週間で切られます。

代わりに、受理済みの所見を値・場所・理由つきで記録したファイルを置いて、そこからの差分だけを見ます。新規なら落とす、悪化したら落とす、記録に残っているのに実物が消えていたら落とす。

生まれつき赤いゲートは1週間で電源を切られる。抑制が理由つきの差分としてレビューできるゲートは切られない。

「悪化」の定義にも失敗の記録が残っています。素朴に「数が増えたら悪化」と読むと、しきい値を下回る方向で発火するルールでは永久に判定されません。この読み違いのせいで、ルールの3分の1が長期間到達不能になっていました。どちらの穴も、ゲートは緑を出し続けていたので気づきませんでした。

ゲートが本当に落ちるか確かめる

ここからが本題です。検査する側を誰が検査するのか。

緑のテストは、一度赤くなるのを見るまで何も証明しません。受け入れゲートには同じ扱いがもっと要ります。他の全部が検査されたかどうかを決めているのがそれ自身なので、黙って発火しなくなったゲートは、ゲートが無いより悪いからです。

そこで、ゲートに対して意図的な改変を22種類当てて、それぞれで本当に落ちるかを確かめています。使い捨ての作業領域の中で壊すので、手元の状態は巻き込みません。

改変の中身が示唆的です。

  • 描画の指定に、それらしい名前のフラグを1つ足す。真っ黒な画像を黙って書き出す組み合わせ
  • パイプラインのエラー伝播を切る。失敗をパイプが飲み込むようにする
  • コントラスト比のしきい値を4.5から1に下げる。1.0 は色を自分自身と比べた比率なので、ルールは動き続けたまま二度と発火しなくなる

最後の型が繰り返し出てきます。止まるのではなく、動き続けて何も見つけなくなる。発火できないルールは、無いルールより悪い。沈黙が合格として読まれるからです。

同じ考えで、完了の印を置いています。子プロセスが何も残さずに終了コード0で終わったなら、それはゲートを走らせていません。「何も出力しなかった」が「何も見つからなかった」と読まれてはいけない、という一行が添えてあります。

壊した検体を1つ作る

部品ごとの改変で確かめられない段がありました。実際に動かすには本物の作業ツリーが要る段です。

そこで、リポジトリを丸ごと複製した検体を用意しています。中には壊し方の違うブランチが5本あり、ゲートの段に1つずつ対応します。コンパイルを通らなくする、単体テストを落とす、規約に反するファイルを置く、レイアウトを崩す、そして実際の導線を通したときだけ落ちる欠陥を仕込む。最後のものは、動いているコードから分岐を数行削っただけです。

段ごとに専用の壊し方が要ったのは、ゲートの段ごとに落ち方が違うからです。ゲートが本物の欠陥に対して実際に赤くなることを、リポジトリ丸ごとで確かめるためだけの使い捨てです。

作られた日付は、受け入れゲートを実装した日と同じでした。ゲートを作った日に、それが落ちることを確かめるものも作っている。

自分自身を検査できない穴

最後に、いちばん厄介だったものを書いておきます。

ゲートの掃引範囲を広げる変更をマージしたとき、ゲートはそれを取り込んだうえで古い範囲を掃引し、新しい範囲を一度も見ないまま合格と表示しました。ゲート自身への変更は、それを統合する実行だけが検査できません。

もう1つ、シェルがスクリプトをバイト位置で読み進める性質も絡みました。走行中に自分自身が書き換わると、続きを読む位置が別の分岐の途中に着地しうる。その回は運で助かっていた、と記録にあります。

対応は、スクリプトを三相に割ることでした。自分を一時領域へ複製してから実行し直す段、マージと巻き戻しだけを担って自分は置き換わらない段、そしてマージ後のツリー側のスクリプトを構文検査してから実行する段。真ん中の段が自分を置き換えないのは、壊れたと分かった時に生きているプロセスが必要だからです。

検査の作り方は、まだ規約になっていない

ここまでの仕組みは、事故のたびに1つずつ足して育ちました。育て方そのものは、どこにも書かれていません。

ゲートに意図的な改変を当てて落ちることを確かめる手法も、壊した検体を1つ作って全体で確かめる手法も、実際に回っている一方で、規約の文書には1行もありません。残っているのはコミットメッセージの中だけです。次に同じものを作る人は、履歴を掘ることになります。

同じ穴が別の場所にもありました。規約の文書そのものを検査するときに、違反をわざと仕込んだ見本を作って、独立した目に見つけさせる手法を使っています。仕込んだ違反がいくつ見つかったかで、読み手が本当に読んだかを測る形です。これも運用されているのに、手順として書かれていません。

ゲートは他人の作業を検査します。ゲートの作り方を検査するものは、まだありません。

マージしたい変更をゲートが検査し、そのゲートを意図的な改変と壊した検体が検査する。ここまでは検査する側に答えがあるが、そのゲートをどう作るかを定めた規約は存在せず、手法はコミットメッセージの中にしか残っていない。列の右端だけが空いている