review-skim: コードレビュー用スキミングビュー
コードの スキミングビュー(レビュアーが1行1行読まずに「全体の流れ」を把握するための圧縮表現)を生成するスキル。AI生成コードのレビューでは、文法レベルの正しさはほぼ問題にならず、変数名も妥当で、個々の末端関数もだいたい看板通りに動く。壊れるのは 全体のロジック — 順序の誤り、ケース漏れ、正しい部品の誤った配線。これらは3階層深くヘルパーに潜っている時には見えず、全体の形を一度に俯瞰した時にだけ見える。
出力は単一のMarkdownファイル。レビュアーはこれを「どこを深く読むか」の判断材料に使う。実コードを読まずに承認するためのものでは ない。単なる案内図である。
基本原則: 詳細を削ぎ落とし、流れを保持する
レビュアーのメンタルモデルは次の3つから組み立てられる:
- 構造 — どのファイル・クラス・関数が存在し、どう関係しているか
- 意図 — 各要素が何を「しようとしている」か。コメントや名前から読み取れるもの
- 流れ — トップレベルでの操作の順序(制御フロー、呼び出し、副作用)
この3つに寄与しないものはスキミング目的ではすべてノイズ。データを変形するだけのループ本体、引数バリデーション、getter/setterの定型、文法的な儀式 — すべて畳む。残ったものは疑似コードのように読めるべきである。
入力モード
ユーザーが「何を」スキミングするかを指定する。3つのモードを用意:
1. ファイルモード
review-skim path/to/a.ts path/to/b.py
指定されたファイルを全体スキミング。
2. PRモード
review-skim pr 123
gh pr diff 123(および gh pr view 123 でメタデータ)で取得し、変更された 変更部分のみ をスキミング。各ハンクに数行の前後コンテキストを含めてレビュアーが位置関係を掴めるようにする。差分がファイル全体でない限りはファイル全体には展開しない。
3. ブランチ差分モード
review-skim branch main..HEAD(または任意の <base>..<head>)
git diff <base>..<head> で変更ファイルと変更領域を特定。PRモードと同じルール: 変更領域 + 小さなコンテキストのみ。
ユーザーの依頼が曖昧な場合(例: "最近の変更をスキミングして")、着手する前にどのモードかを1度確認すること — 3モードは読み込む量が大きく違う。
何を残し、何を畳むか
そのまま残す
- ファイルパスと見出し — 1ファイル1セクション
- トップレベル宣言 — クラス、関数、メソッド、エクスポート定数。シグネチャは正確に保持(名前、引数、返り値型があれば型も)
- 「何を」「なぜ」を説明する既存コメント。特にdocstringや宣言の直上のブロックコメント。これらはレビュアーにとっての意図の第一の情報源
- 非末端関数内の上位制御フロー —
if/else/for/while/try/switchの分岐構造。各ブランチの本体は、そのブランチ自身がさらに流れを持つ時のみ展開する - 他の自作関数への呼び出し。これがレビュアーが見ようとしているフローグラフの辺である。著者が書いた別の部品への受け渡しポイント。ライブラリ/フレームワークの呼び出しは呼び出しサイトとして残す必要はなく、意味があるなら副作用ラベル(下記)で、単なるデータ整形なら省略する
- 副作用 — ネットワークリクエスト、DBクエリ、ファイルI/O、サブプロセス/シェル呼び出し、環境変数読み取り、グローバル状態の変更。レビュアーが「コードが外界に触れる場所」を発見できるようにするため残す。明瞭さが増すなら自然言語に抽象化してよい(下記参照)
畳む、または要約する
- 末端関数の本体。関数が 末端 とは、同じリポジトリ内で定義された他の自作関数 を一切呼ばないものを指す。ライブラリ/フレームワークの呼び出しや言語組み込み関数は末端判定を無効にしない。
fetch、JSON.parse、fs.readFileなどしか呼ばない関数は依然として末端扱い。理由は、レビュアーが見たいのは 著者のコードの形 であり、ライブラリ呼び出しは呼び出しサイトとして展開するより、末端要約の中で自然言語の副作用ラベルとして表現した方が良いから。末端関数では本体を 「何をしているか」の1行自然言語要約 に置き換え、要約である旨を明示する(下記「要約マーキング」参照)。末端関数が注目すべき副作用を持つ場合は要約の中で言及する(例:// [summary] HTTP GET /users/:id、パースしたJSONまたは404ならnullを返す) - 純粋なデータ配管 — 代入、分割代入、フィールドコピー、フォーマット文字列の組み立て — 非末端関数の中に出てきたら、連続する配管行を
// ... (data shaping)1つに畳む - 引数バリデーション / ガード節 —
// ... (argument checks)に畳む。ただしバリデーション自体が非自明(例: レビューに値するフィールド間不変条件)な場合は残す - import — 驚きがある時だけ列挙(新しい外部依存、特殊なモジュールなど)。それ以外は省略
副作用の抽象化(オプション)
副作用はリテラル呼び出し、または自然言語ラベルのどちらで書いてもよい。意図をより伝える方を選ぶ:
- リテラル:
await db.users.findOne({ id }) - 抽象化:
// [side effect] DB read: fetch user by id
リテラル呼び出しが長い・ノイジー・ビルダーチェインに埋もれている時は抽象化形式を使う。正確なAPIや引数自体がレビュー対象として重要な時はリテラル形式を使う。迷った場合はリテラル形式を優先する — レビュアーに飛び込むための正確な足がかりを与えるため。
要約マーキング
末端関数の本体を要約に置き換えた時、その要約は あなたの推測 であって事実ではない。レビュアーは実物と要約を一目で区別できる必要がある。誤った要約はレビューを誤導しうるから。
要約した本体には必ず可視タグをつける。形式は正確に次の通り:
function fetchUserProfile(id) {
// [summary] idをパースし、キャッシュされたプロファイルを引き、あればそれ、なければnullを返す。
}
[summary] タグは必須。言い換え禁止、省略禁止、文脈から推測させない。これは「このブロックは読まれていない、関数名と周辺の呼び出しから推測された」という、レビュアーへのシグナルである。
末端関数を自信をもって要約できない場合(関数名が不透明でコメントもない)は、意図を発明するのではなく // [summary] (unclear — read directly) と書く。
コメント欠落の指摘
本スキルの副次的目的はコメントのギャップを浮かび上がらせること — 著者が説明を残さなかったために、レビュアーがコードを読まざるを得ない箇所を特定する。説明的コメントが無い宣言(関数、メソッド、クラス、エクスポート定数)はすべてレビュー指摘対象。出力末尾の「Review findings」セクションにまとめる。
「説明的」コメントとは、そのコードが 何を しているか、または なぜ を、レビュアーが行動できるレベルで説明しているもの。シグネチャを言い換えただけのコメント(// 名前をセットする)はカウントしない。
指摘セクションはファイル別スキミングを置き換えるものではなく、補完する。レビュアーはファイル別スキミングで流れを理解し、指摘セクションで「どこを著者に質問すべきか / 直接読むべきか」を判断する。
出力フォーマット
単一のMarkdownファイルに書き出す(デフォルト: カレントディレクトリの review-skim.md。ユーザーがパスを指定すればそこへ)。
テンプレート:
# Review Skim — <target description>
**Mode:** <file | pr | branch>
**Target:** <file list | PR #N | base..head>
**Generated:** <YYYY-MM-DD HH:MM>
## このドキュメントの読み方
これは **スキミングビュー** であり、実コードではない。`[summary]` とマークされた行は、本体を読まずに推測した末端関数の仮説である — 事実ではなく検証すべき仮説として扱うこと。`[side effect]` とマークされた行は実呼び出しの自然言語での代替表現。
末尾の「Review findings」セクションには、説明的コメントがない宣言の一覧が載っている。
---
## <file path 1>
> <既存のファイルレベルコメント / docstring があればそのまま>
```<language>
class Foo:
"""<既存のクラスdocstringをそのまま>"""
def handle_request(self, req):
# <既存コメントをそのまま>
if req.is_authenticated:
user = self.load_user(req.user_id)
// [side effect] DB read: fetch user profile
return self.render(user)
else:
return self.redirect_to_login()
def load_user(self, user_id):
# [summary] idでユーザー行を引き、ドメインオブジェクトにマップする。
```
## <file path 2>
...
---
## Review findings
説明的コメントが無い宣言(著者に質問するか直接読む必要あり):
- `src/foo.py:42` — `Foo.handle_request` — docstringなし、目的は本体から推測
- `src/foo.py:58` — `Foo.load_user` — 末端関数、コメントなし、要約は推測
- `src/bar.ts:10` — `parseConfig` — コメントなし
出力のルール:
- 1ファイル1セクション。ユーザーが指定した順、または
git diffが返した順で並べる - 言語タグ付きフェンスコードブロックを使う。レビュアーがファイルを開いた時にシンタックスハイライトが効くように
- 元のインデントを保持する — レビュアーは目で走査しており、一貫したインデントこそが制御フローを読めるものにする
- ソースにないコードを発明しない。追加してよいのは
[summary]行、[side effect]ラベル、// ... (collapsed)プレースホルダのみ。それ以外はすべて実ファイルからのコピー - ファイル内で宣言の順序を入れ替えない。ソース順を保つ
- PR/ブランチモード: 各ファイルに変更範囲を示す短いヘッダ(
+42 / -10 linesまたはハンク範囲など)を付け、レビュアーが各ファイルの変更規模を把握できるようにする
手順
- 呼び出しをパース してモード(file / pr / branch)とターゲットを決める。曖昧なら1度だけ確認
- スキミング対象のソースを取得:
- ファイルモード: 各ファイルを全体読み取り
- PRモード:
gh pr view <n>でメタデータ、gh pr diff <n>で変更ファイルとハンクを特定。その後変更ファイルを読み、ハンク領域に前後数行のコンテキストをつけて焦点化 - ブランチモード:
git diff <base>..<head> --name-onlyとgit diff <base>..<head>で同様に
- 各ファイル について、宣言をソース順に走査。各宣言について:
- シグネチャと説明的コメントがあればそのままコピー
- 末端かどうか判定(同じリポジトリの 自作関数 を1つも呼ばない。ライブラリ呼び出しと組み込みはカウントしない)
- 末端なら: 名前 + コメント + 軽く読んだ本体から、何をしているかの1行
[summary]を書く。確信が持てないならその旨を書く - 非末端なら: トップレベルの制御フローと呼び出しサイトを再現。純粋な配管は畳む。分岐内のネストした非末端呼び出しは、展開せず呼び出しのままにしておく
- 宣言に説明的コメントがなければ、findingsセクション用に記録
- Markdownファイルを書き出す(上記テンプレート)
- ユーザーに報告 — 出力パス、スキミングしたファイル数、記録した指摘件数。ファイルを開く・コミットする・プッシュするは行わない
このスキルではないもの
- 実コードを読むことの代替ではない。出力の冒頭にその旨を明示している。レビュアーがスキミングだけで承認するなら、それはレビュアーの責任
- Linterではない。正しさ・スタイル・セキュリティの評価はしない。
[summary]行は記述的であり判断ではない。唯一の「指摘」はコメント欠落のみ - リライターではない。ソースファイルを編集することは決してない。読むだけで、出力は1つのMarkdownファイルのみ
- 言語依存ではない。構造レベル(宣言、コメント、制御フロー、呼び出し)でソースを扱う。特殊な構文の言語であっても、これらの概念にマッピングする努力をする。バッセリと諦めない
なぜ存在するか
AI生成コードのレビューは人間が書いたコードのレビューとは違う。レビュアーの仕事は変化した: 文法はほぼ常に問題なく、変数名は妥当、個々の末端関数は大抵看板通りに動く。壊れるのは 全体のロジック — 順序の誤り、ケース漏れ、正しい部品の誤った配線。その種のバグは3階層深くヘルパーに潜っている時には見えない。一歩下がって全体の形を一度に見た時にだけ見える。
スキミングビューは、レビュアーがオンデマンドでその「一歩下がった」視点を得るために存在する。まず全体の流れについての仮説を立て、怪しく見える特定の関数にだけ潜っていけるように。[summary] とコメント欠落の仕組みはスキミングを正直にするためにある: レビュアーは常に「どの部分を実際に見たか」と「どの部分を信用しているか」を把握できる。