モノレポでAIレビューの対象を宣言しても生成物の指摘は出る

モノレポのPRをレビューさせるとき、対象の限定は「packages/api/ を見てください」の1行で済ませることになる。生成物を3種類含む差分に宣言を4通り当てて各3回実行したところ、パスの列挙では生成物が対象から外れなかった。外れたのは「生成物は対象外です」と書き足した一文のほうである。ただし除外を glob で厳密に書くと、対象に挙げ忘れたワークフロー定義の指摘が3回とも出なくなった。

この記事の提案を使うのは、ビルド出力・ロックファイル・スナップショットが差分に混ざるリポジトリでレビューさせるときである。

生成物を指摘の場所にした出力

対象のパスだけを書いた指示(条件1)で、3回のうち3回、次の形の指摘が返った。

claude-sonnet-5、2026-10-12、条件1(3回目の出力から)
- 場所: packages/api/__snapshots__/report.snap:8
- 主張: 追加された `test_order_line_with_tax` のスナップショット値 `'widget x10 = 880'` に `円` が付いておらず、現在の実装の実際の出力と一致しない
- 根拠: packages/api/report.py:2

主張の中身は誤っていない。実装は 円 を付けるように変わり、スナップショットの値は付いていないので、両者は食い違っている。問題は場所である。packages/api/__snapshots__/report.snap はテストの出力の記録で、このリポジトリでは .gitattributes に生成物として印が付いている。

.gitattributes(変更なし)
packages/web/dist/**        linguist-generated=true -diff
packages/web/package-lock.json  linguist-generated=true -diff
packages/api/__snapshots__/**   linguist-generated=true -diff

スナップショットの行を場所として受け取った人は、その行を手で直すことになる。直す先は実装か、テストを走らせて記録を作り直す手順のどちらかであって、記録そのものではない。同じ食い違いを指摘するなら、場所は実装の側に置かれていなければ使えない。 実際、同じ3回の出力には、場所を packages/api/report.py:2 に置いて根拠をスナップショットに置いた指摘も別に出ている。指摘の中身ではなく、場所と根拠のどちらに生成物を置くかが分かれている。

題材にしたモノレポの差分

API(Python)と管理画面(TypeScript)を1つのリポジトリに置いた10ファイルの題材を作った。差分は8ファイルで、追加14行・削除9行、差分ファイルは85行である。README.md に約束を3条置いて判定基準の出所にした。

ファイル構成と、差分に入ったもの
  README.md                                   約束3条
  .gitattributes                              生成物の印
  .github/workflows/ci.yml                    変更あり
  packages/api/Dockerfile                     変更あり
  packages/api/config/settings.yaml           変更あり
  packages/api/report.py                      変更あり
  packages/api/__snapshots__/report.snap       変更あり(生成物)
  packages/web/package.json                   変更あり
  packages/web/package-lock.json              変更あり(生成物)
  packages/web/dist/bundle.js                 変更あり(生成物)

約束に反する箇所を3件仕込んだ。どれも対象のパッケージに関わるもので、生成物の側には置いていない。

欠陥 場所 中身
D1 packages/api/Dockerfile:1 ベースイメージを python:3.12.7-slim から python:latest に変えた(約束1はパッチ版まで固定する)
D2 .github/workflows/ci.yml:7 permissions を contents: write に広げ、理由のコメントを書いていない(約束2)
D3 packages/api/config/settings.yaml:10 prod プロファイルの debug を true にした(約束3)

この題材には、パスの列挙で対象を限定できない構造が2つ入っている。どちらも実際のモノレポで起きる形である。

  1. 生成物が対象パスの内側にある。 packages/api/__snapshots__/ は packages/api/ の下なので、「packages/api/ を見てください」はこのディレクトリを含む。別パッケージの生成物(packages/web/dist/、package-lock.json)はパスの指定で外れるが、対象パッケージ自身の生成物は外れない
  2. PRの一部が対象パスの外にある。 D2 を仕込んだ .github/workflows/ci.yml はリポジトリ直下で、packages/api/ の下ではない。対象のパッケージのテストを走らせる設定なので、PRの目的には含まれている

宣言を4通り当てて数えた

4条件で違うのは対象の宣言だけである。道具の可用性(ファイルを読む操作と検索する操作だけを許す)、モデル、題材、出力の形式は4条件で同じに固定した。題材は実行のたびにコピーしている。

条件 宣言の書き方
1 packages/api/ を見てください(パスの列挙だけ)
2 条件1 +「生成物は対象外です」の一文
3 対象の glob 1行と、除外の glob 4行(各行に除外する理由)、および「対象外は材料として読んでよい。指摘の場所にはしない」の1行
4 条件3 +「差分にあって対象にも対象外にも当たらないパスがあれば、指摘より先に未分類として列挙する」の1行

claude-sonnet-5(Claude Code CLI 2.1.285)で 2026-10-12 に各条件3回、合計12回実行した。指摘の場所と根拠に書かれたパスは、スクリプトで4区分(生成物/対象/別パッケージ/対象の外)に振り分けている。区分はパスだけで決まるので、判定に人が入らない。

巡回 条件1 条件2 条件3 条件4
1回目 6件(うち場所が生成物 1件) 3件(0件) 3件(0件) 3件(0件)
2回目 5件(1件) 5件(0件) 3件(0件) 3件(0件)
3回目 6件(1件) 5件(0件) 3件(0件) 3件(0件)

生成物を場所にした指摘は、条件1で3回とも1件出て、条件2から条件4では3回とも0件だった。一文を足した条件2で0件になっているので、glob の書き分けまでしなくてもこの1件は出なくなる。 一方、根拠の側に生成物を置いた指摘は、条件3と条件4でも3回とも1件残っている。条件3の宣言が「対象外は材料として読んでよい」と書いたとおりの結果で、こちらは残って困らない。追試する人は同じファイルを開けばよい。

別パッケージ(packages/web/)を場所にした指摘は、4条件・12回すべてで0件だった。パスを1つ名指しするだけで、別パッケージは対象から外れている。

除外を書くと落ちる指摘

仕込んだ欠陥3件の検出は、条件によって分かれた。

欠陥 条件1 条件2 条件3 条件4
D1 Dockerfile の版 3/3 3/3 3/3 3/3
D3 prod の debug 3/3 3/3 3/3 3/3
D2 ワークフローの権限 3/3 2/3 0/3 0/3

D2 だけが落ちている。場所が .github/workflows/ci.yml で、条件3の対象 glob(packages/api/**)に当たらないためである。除外の glob を4行書いたことで「挙げたもの以外は対象ではない」という読みが成り立ち、対象にも除外にも書いていないパスがその読みの外側に残った。条件2の一文だけではその読みが成り立たないので、3回のうち2回は D2 が出ている。宣言を厳しくするほど安定して落ちる形になっている。

落ちた指摘の中身は、ワークフローの権限を広げたまま理由を書いていないというもので、review-extension のGitHub Actionsのpermissions過剰設定をレビューでどう見抜くかが扱う類の指摘である。対象の宣言を書いた側は、この種類の指摘を自分で止めたことに気付けない。出力に現れないものは数えられない。

条件4で足した1行が直すのは、落ちたことが分からないという点だけである。3回すべてで、指摘の前に未分類の列挙が付いた。

claude-sonnet-5、2026-10-12、条件4(3回とも同じパスが1件)
未分類:

- .github/workflows/ci.yml

未分類の列挙を足しても、落ちた指摘は戻らない。 条件4の指摘は3回とも3件で、D2 は出ていない。取り戻すには対象の glob に .github/workflows/** を足すしかない。条件4が変えたのは、足し忘れが出力に出るかどうかである。

対象と除外の宣言

4条件のうち条件4の形を置く。山括弧は各自のリポジトリに置き換える。

レビュー指示に足す「対象の範囲」の節
## 対象の範囲

対象:

- `<対象のパッケージ>/**`
- `<対象のパッケージを動かす設定ファイルの glob>`

対象外(各行の後ろが除外する理由):

- `<別パッケージ>/**` … このPRの目的に含まれない
- `**/dist/**` … ビルド出力。手で直しても次のビルドで戻る
- `**/package-lock.json` … 依存解決の出力。版を変えるなら `package.json` の側
- `**/__snapshots__/**` … テストの出力の記録。実装を直せば再生成される

対象外のファイルは、対象のファイルを判定する材料として読んでよい。指摘の場所にはしない。

差分にあって上のどちらにも当たらないパスがあれば、指摘より先に「未分類」として
そのパスを列挙する。

除外の行を自分で考える必要はない。生成物の印が付いたリポジトリなら、.gitattributes の linguist-generated の行がそのまま除外の glob になる。

除外の glob を .gitattributes から引く
grep -l . .gitattributes >/dev/null && grep 'linguist-generated' .gitattributes | awk '{print $1}'

印が付いていないリポジトリでは、差分のパスのうち、手で編集しないものを数えるところから始める。

差分に生成物が何件入っているかを数える
git diff --name-only origin/main... | grep -E 'dist/|-lock\.(json|yaml)$|__snapshots__/|\.min\.(js|css)$'

対象の1行目に書く glob は対象のパッケージで決まるが、2行目は差分を見てから決める。上の git diff --name-only の一覧から、対象のパッケージの外にあってPRの目的に含まれるパス(ワークフロー定義、共有の設定、デプロイの値)を拾って足す。この拾い漏れが D2 の 0/3 である。

道具を使わせない環境では、同じ宣言を人の側で実行する。差分を貼る前に生成物のパスを差分から外し、「外したファイル」としてパスの一覧を指示文に書き添えればよい。宣言が決めているのは材料の範囲であって、材料を誰が外すかではない。

宣言を足すと何を使うか

払うのは指示文の長さである。条件2の一文に対して、条件4の宣言は9行で、8行増える。増えた8行のうち4行は除外の glob と理由、1行は材料として読む許可、1行は未分類の列挙、残りは見出しと空行である。

増える出力もある。条件4では未分類の節が1つ増えた。この題材では1行だが、対象の glob を1つしか書いていないリポジトリでは、差分のパスのうち対象に当たらないものがすべて並ぶ。3回の実行では .github/workflows/ci.yml だけが並び、README.md と .gitattributes(差分に入っていない)は挙がらなかった。

払っていないものも書いておく。4条件で指摘の件数は6件から3件まで動いたが、仕込んだ欠陥のうち D1 と D3 は12回すべてで出ている。宣言を厳しくして落ちたのは、対象の glob に書き忘れたパスの指摘だけである。

この宣言を落としていい条件は、差分に生成物が1件も入っていないときである。 上の git diff --name-only の一致が0件なら、対象のパスを1行書くだけで足りる。生成物が1件以上あるなら、その1件が指摘の場所になりうるので、除外の行を足す理由がその場で立つ。

指示文の点検

モノレポで対象を宣言するときに確かめる項目
  • 対象のパスを列挙しただけで、生成物が外れたと見なしていないか
  • 対象のパッケージの内側にある生成物(スナップショット、生成したコード)を除外に書いたか
  • 除外の各行に、除外する理由を書いたか
  • 除外したファイルを「材料として読んでよい」のか「読ませない」のかを決めたか
  • 指摘の場所に置いてよいパスと、根拠に置いてよいパスを別に決めたか
  • 差分のうち対象のパッケージの外にあってPRの目的に含まれるパス(ワークフロー、デプロイの値)を、対象の側に書いたか
  • 対象にも対象外にも当たらないパスを、指摘の前に列挙させる1行を入れたか
  • 渡す前に git diff --name-only で生成物が何件混ざっているかを数えたか
  • 除外の glob を .gitattributes の linguist-generated から引けるか確かめたか
  • 宣言を厳しくしたことで出なくなった指摘を、未分類の列挙で見ているか

おわりに

生成物3種を含むモノレポの差分に宣言を4通り当てて各3回実行すると、対象のパスを列挙しただけの条件では、対象パスの内側にあるスナップショットを場所にした指摘が3回とも1件出た。「生成物は対象外です」の一文を足すと、この1件は3回とも出なくなる。glob の書き分けまで必要なのは、根拠として読むことを許すかどうかを分けたいときである。

厳しく書いた側には別の損失がある。除外の glob を4行書いた条件では、対象の glob に挙げ忘れたワークフロー定義の指摘が3回とも落ちた。対象を限定する宣言は、見ない範囲を決めると同時に、見る範囲の書き漏らしを確定させる。未分類の列挙を1行足すと、書き漏らしたパスが出力の先頭に出る。

次に取る行動を1つ挙げる。手元のPRで git diff --name-only を実行し、一覧を「対象」「生成物」「どちらでもない」の3つに分ける。3つ目に1件でも入っていたら、それが対象の glob に足す行である。