差分の途中でスクロールが止まりました。8月12日の 2.8.0 で、ファイルビューアの表示サイズと履歴上の大きな差分の表示量に上限が入ったためです。大きなコマンド出力を含む会話でのクラッシュ修正とセットで入った変更で、安定性の面では歓迎すべきものです。
ただ、その日の私は「エージェントが触った範囲を最後まで見る」という前提で作業していました。上限に当たった瞬間、その前提が実は一度も検証されていなかったことに気づきます。全部見えていたときの私は、本当に全部見ていたのでしょうか。
全部見えていた頃に、見落としていたもの
答え合わせはすぐにできました。個人開発で運用している技術ブログのリポジトリには、日本語記事だけで 1,006 本の MDX が入っています。そこに、コードブロック内の不要なバックスラッシュエスケープがどれだけ残っているかを数えてみました。
grep -rc '\\!' content/articles/ja --include='*.mdx' \
| awk -F: '$2>0{n++; s+=$2} END{print "該当ファイル:", n+0, "/ 総出現数:", s+0}'
# => 該当ファイル: 28 / 総出現数: 130
28 ファイル、130 行。中身はこういうものです。
// 公開されていた行
if (\!researchOutput.sources || researchOutput.sources.length === 0) {
\! は JavaScript として不正です。読者がこれをコピーすれば、その場で構文エラーになります。シェル経由でファイルを書いたときにヒストリ展開の対策として入ったバックスラッシュが、そのまま残ったものでした。
これらの行は、一度も隠れていませんでした。私は commit のたびに差分を眺めていて、その画面を 130 回通過しています。つまり表示上限は、見えなくしたのではなく、見ていたつもりだった部分を可視化しただけでした。
目で追う量は、変更量に比例して増える
なぜ通り過ぎたのかを考えると、構造の問題に行き当たります。
記事1本の追加は、日本語版と英語版で 2 ファイルです。このリポジトリの記事は中央値で 211 行、最大で 1,265 行ありました。1 本追加するだけで、差分は 400 行から 2,000 行になります。ここに既存記事の修正が混ざれば、さらに増えます。
人が集中して読める行数は、変更量に比例しては増えません。増えるのは「読んだつもりになる速度」のほうです。\! のような 2 文字の異物は、この速度の中でもっとも消えやすい種類のものでした。
| 欠陥の種類 | 機械での検出 | 目視での検出 |
| 不要なエスケープ・構文の壊れ | 確実 | 行数に反比例して落ちる |
| 裸URL・記法の崩れ | 確実 | 同上 |
| 存在しないリンク先 | 確実 | ほぼ不可能 |
| 説明と実装のずれ | 不可 | 可能 |
| その主張が正しいか | 不可 | 可能 |
表の上半分を人間が担当していたことが、そもそもの配分ミスでした。上限が入ったのは、その配分を変える合図だったのだと受け取っています。
差分を「要約」と「全量」に分ける
切り替えた方針は単純です。差分をそのまま人に渡すのをやめ、次の2つに分けます。
- 要約 — 画面に収まる固定長。機械で判定できたことと、人が見るべき場所の入口だけを載せる
- 全量 — ファイルとして保存する。捨てないが、既定では開かない
要約が固定長であることが重要です。変更が 400 行でも 4,000 行でも、最初に読むものは同じ長さになります。レビューに使う時間が変更量から切り離されます。
以下が実際に使っているスクリプトです。
#!/usr/bin/env bash
# review-digest.sh — 差分を「1画面の要約」と「全量アーティファクト」に分ける
# 使い方: ./review-digest.sh 未コミットの変更を対象にする
# ./review-digest.sh origin/main 指定リビジョンとの差分を対象にする
set -uo pipefail
BASE="${1:-}"
OUT_DIR="${REVIEW_OUT_DIR:-.review}"
mkdir -p "$OUT_DIR"
FULL="$OUT_DIR/diff-full.patch"
if [ -n "$BASE" ]; then
git diff "$BASE" > "$FULL"; STATUS=$?
STAT=$(git diff --stat "$BASE" | tail -1)
NUMSTAT=$(git diff --numstat "$BASE")
else
# 未追跡ファイルを差分に載せる(後述)
git add -N . >/dev/null 2>&1 || true
git diff HEAD > "$FULL"; STATUS=$?
STAT=$(git diff --stat HEAD | tail -1)
NUMSTAT=$(git diff --numstat HEAD)
fi
if [ "$STATUS" -ne 0 ]; then
echo "git diff が失敗しました (exit=$STATUS)" >&2
exit "$STATUS"
fi
# 追加行だけを対象にする(削除された既存の欠陥を再検出しないため)
ADDED=$(grep -E '^\+[^+]' "$FULL" || true)
E_BANG=$(printf '%s\n' "$ADDED" | grep -c '\\!' || true)
BARE=$(printf '%s\n' "$ADDED" | grep -E '(^|[[:space:]])https?://' \
| grep -vE '\]\(|<https?://|"https?://' | grep -c . || true)
MDTBL=$(printf '%s\n' "$ADDED" | grep -cE '^\+[[:space:]]*\|[-: |]+\|[[:space:]]*$' || true)
echo "== 変更規模 =="
echo " ${STAT:- (変更なし)}"
echo
echo "== 機械検出(全量を開く前にここを直す)=="
echo " 不要な \\! エスケープ : ${E_BANG} 行"
echo " 裸URL : ${BARE} 行"
echo " Markdown テーブル区切り: ${MDTBL} 行"
echo
echo "== 目視が要る箇所(変更量の多い順)=="
printf '%s\n' "$NUMSTAT" | sort -rn | head -5 | while read -r add del path; do
[ -z "${path:-}" ] && continue
printf ' %5s+ %5s- %s\n' "$add" "$del" "$path"
done
echo
echo "全量: $FULL ($(wc -l < "$FULL") 行)"
# 検出があれば非ゼロで終える
[ "${E_BANG:-0}" -eq 0 ] && [ "${BARE:-0}" -eq 0 ] && [ "${MDTBL:-0}" -eq 0 ]
実際に3種類の欠陥を含むファイルを置いて走らせると、こう出ます。
== 変更規模 ==
1 file changed, 3 insertions(+)
== 機械検出(全量を開く前にここを直す)==
不要な \! エスケープ : 1 行
裸URL : 1 行
Markdown テーブル区切り: 1 行
== 目視が要る箇所(変更量の多い順)==
3+ 0- content/articles/ja/tips/_probe.mdx
全量: .review/diff-full.patch (9 行)
終了コードは 1 でした。12 行で状況が分かり、直すべき場所も分かります。
最初の版は、何も検出しませんでした
このスクリプトを書いて最初に走らせたとき、3 つとも 0 と表示されました。欠陥を含むファイルを自分で置いた直後にです。
原因は git diff HEAD にあります。未追跡ファイルは差分に現れません。エージェントが新しいファイルを作った場合、その中身は丸ごと検査の対象外になります。新規作成こそがもっとも検査したい変更なのに、既定では素通りする構造でした。
対策が上のスクリプトにある git add -N . です。intent-to-add でファイルの存在だけを index に登録すると、内容が差分に載るようになります。ステージングはされないため、その後の git add の操作を邪魔しません。
| コマンド | 未追跡ファイルの扱い |
git diff | 現れない |
git diff HEAD | 現れない |
git add -N . の後の git diff HEAD | 全行が追加行として現れる |
検出ゲートを書いたつもりで、実際には何も見ていない状態は起こり得ます。ゲートを足したときは、必ず落ちるはずの入力を一度通して、本当に落ちることを確かめたほうが安全です。エージェントが書いたファイルが検査から漏れる話は、エージェントの生成物が .gitignore に飲まれて消える問題でも似た形で踏んでいます。
要約を head に繋ぐと、判定が消えます
もう1つ、運用に入れてから気づいた点があります。要約が長くなってきたので、呼び出し側でこう書いてしまいました。
./review-digest.sh | head -20 # ← 判定が失われます
パイプラインの終了コードは、最後のコマンドのものになります。head は正常終了するため、スクリプトが 1 を返していても、呼び出し側には 0 が届きます。検出しているのに止まらない、という状態です。
要約を短く保つ責任はスクリプト側に置き、呼び出し側では素直に実行します。どうしても絞りたいときは、判定を先に確定させます。
if ./review-digest.sh > .review/digest.txt; then
echo "機械検出なし"
else
head -20 .review/digest.txt # 判定を取り出した後で絞る
fi
出力を切り詰める操作は、見た目より多くのものを落とします。参照テキストを行数で切ったときに何が届かなくなるかは、エージェントに渡す参照メモを head で切ったときの限界で別の角度から扱っています。
人が見る対象を、意識的に減らす
要約が通ったあとに私が開くのは、変更量上位の 2〜3 ファイルだけです。そこで見るのは、機械が判定できない項目に限ります。
- 説明文と、その下のコードが同じことを言っているか
- 選んだ方法の理由が書かれているか
- 主張している挙動を、実際に確かめたのか
エージェントが書いたコードで危ないのは、構文が壊れているものではありません。構文は通るのに、説明と少しずれているもののほうです。前者は機械が確実に拾い、後者は人しか拾えません。役割を分けたあとのほうが、後者に気づく回数が増えました。全量を追っていた頃は、そこへ到達する前に集中力を使い切っていたのだと思います。
なお、コンテキストにどこまで載せるかという判断そのものについては、エージェントに持たせるコンテキストとコストの関係で扱っています。今回の話は、その手前にある「人が読む側」の設計にあたります。
表示上限との付き合い方
2.8.0 の上限は、履歴上の大きな差分とファイルビューアに効きます。全量が必要な場面がなくなるわけではありません。私はこの区別を、次のように運用へ落としています。
| 場面 | 見るもの |
| 通常のレビュー | 要約のみ。上位ファイルは必要に応じて |
| 要約が非ゼロで終わった | 該当行だけを grep で抜く |
| 原因が分からない不具合 | 保存した .review/diff-full.patch をエディタで開く |
全量はファイルに残っているので、必要になったときに開けば済みます。上限があるのはビューアであって、データではありません。この区別がついてから、上限は制約というより、既定値の変更として受け取れるようになりました。
手元に入れるまでの3ステップ
導入は 15 分ほどで終わります。私自身がこの順で入れました。
1. スクリプトを置いて、出力先を .gitignore に足す
review-digest.sh をリポジトリ直下か scripts/ に置き、実行権限を付けます。全量パッチの保存先 .review/ は .gitignore に追加してください。これを忘れると、パッチ自身が次の差分に載って肥大します。
chmod +x review-digest.sh
echo ".review/" >> .gitignore
2. 必ず落ちる入力を通して、落ちることを確かめる
ここを飛ばすと、前述の「何も検出しない検出ゲート」になります。3種類の欠陥を1行ずつ含む使い捨てファイルを置いて実行し、終了コードが 1 になることを確認します。
printf 'if (\\!ok) {}\nhttps://example.com\n| --- | --- |\n' > _probe.md
./review-digest.sh; echo "exit=$?" # exit=1 になれば正常
rm _probe.md
3. 既存の品質チェックより前に置く
本番運用のパイプラインに組み込む場合は、重いチェック(テスト・ビルド・リンタ)より前に置きます。2 文字の異物のためにビルドを1回まわす必要はありません。検出時にビルドを回避できるぶん、待ち時間が短くなります。
./review-digest.sh || { echo "機械検出あり。先に直します"; exit 1; }
npm run lint && npm test
この場合は、既存のチェックを置き換えるのではなく前段に足すだけで済みます。所要時間は差分の大きさに関わらずほぼ一定で、私の環境では 1 秒未満でした。3,000 行規模の差分でも同じです。
次にやること
もしお手元のリポジトリで似た運用をしているなら、まずは公開済みのコードに対して grep -rc '\\!' を一度走らせてみてください。0 なら安心材料が1つ増えますし、0 でなければ、その日のうちに直せる種類の欠陥です。私自身、130 という数字を見るまでは、自分の目視を信用していました。
私も検出項目はまだ 3 つしかなく、増やしている途中です。同じところでつまずいた方の工夫があれば、ぜひ知りたいと思っています。お読みいただきありがとうございました。