| name | systematic-debugging |
| description | バグ修正・ランタイム計装・テスト失敗解消を担う統合デバッグスキル。「バグを直して」「テストが落ちてる」「エラーが出る」「修正して」「なぜ動かない」「ログを仕込んで」「計装して」「何が起きているか確認したい」「動作を観察したい」「値を追いたい」で発動する。根本原因を特定してから修正する体系的手法と、ランタイムprintf計装の両方を提供する。 |
| metadata | {"version":"2.1.0","tier":"stable","category":"debug","tags":["root-cause","hypothesis","debugging-methodology","printf-debug","logging","runtime-analysis"]} |
体系的デバッグ
概要
場当たり的な修正は時間を浪費し、新たなバグを生む。安易なパッチは根本的な問題を隠す。
基本原則: 修正を試みる前に、必ず根本原因を特定せよ。症状の修正は失敗である。
このスキルは2つの用途に対応する:
| ユーザーの依頼 | 対応フェーズ |
|---|
| 「バグを直して」「テストが落ちてる」「修正して」「エラーが出る」 | フェーズ1〜4(体系的デバッグ全工程) |
| 「ログを仕込んで」「計装して」「動作を観察したい」「値を追いたい」 | 計装プロセス(ランタイム観察のみ) |
| フェーズ1でランタイム計装が必要になった | 体系的デバッグ → 計装プロセスを呼び出す |
鉄則
根本原因の調査なしに修正を行ってはならない
フェーズ1を完了していなければ、修正を提案してはならない。
フェーズ1〜4: 体系的デバッグ
各フェーズを完了してから次に進まなければならない。
フェーズ1: 根本原因の調査
いかなる修正を試みる前に:
-
システム全体を俯瞰する(幅優先)
深く潜る前に、関係するコンポーネントを列挙する。エキスパートはノービスと異なり、即座に1箇所を深堀りせず、まず全体の地図を描く(Badiozamany et al.)。
[入力] → [コンポーネントA] → [コンポーネントB] → [出力]
↑ どこで壊れているか?
-
エラーメッセージを注意深く読む
- エラーや警告を読み飛ばさない
- スタックトレースを最後まで読む
- 行番号、ファイルパス、エラーコードを記録する
-
一貫して再現する
- 確実にトリガーできるか?
- 再現できない場合 → データを収集する、推測しない
-
最近の変更を確認する
- git diff、最近のコミット、新しい依存関係、設定変更
-
マルチコンポーネントシステムでの証拠収集
システムが複数のコンポーネントを持つ場合(CI → ビルド → 署名、API → サービス → データベース)は、計装プロセスを呼び出す。各コンポーネント境界でデータをログ出力し、どこで壊れるか証拠を収集してから分析する。
-
データフローを追跡する
エラーがコールスタックの深い位置にある場合は references/root-cause-tracing.md 参照。
- 不正な値はどこから発生するか?
- ソースが見つかるまで遡り続ける
- 症状ではなくソースで修正する
フェーズ2: パターン分析
修正する前にパターンを見つける:
- 同じコードベースで類似の動作するコードを探す
- パターンを実装する場合、リファレンス実装を完全に読む(流し読みしない)
- 動作するものと壊れているものの違いを全てリスト化する
- 必要な設定、環境、前提条件を理解する
フェーズ3: 仮説とテスト
科学的方法:
-
複数の仮説を立てる(必須: 2〜3つ、異なるコンポーネント/レイヤーから):
- 仮説は互いに直交させる(同じ原因の言い換えは不可)
- 尤もらしさでランク付けするが、全て書き出す
-
ユーザーに仮説一覧を提示し、選択を促す: 以下のフォーマットで表示する
以下の仮説を立てました。どれから調査しますか?
**H1: [コンポーネントA]** — (一行で仮説を説明)
検証方法: ... コスト: 低
**H2: [コンポーネントB]** — (一行で仮説を説明)
検証方法: ... コスト: 中
**H3: [コンポーネントC]** — (一行で仮説を説明)
検証方法: ... コスト: 高
推奨: H1(最も反証しやすいため)
別の仮説があればここで追加できます。
-
ユーザーが選んだ仮説を検証する(一度に一つの変数のみ)
仮説を途中で切り替える場合(ロールバック):
検証中に別の仮説を試したいとユーザーから指示された場合は、先に現在の調査で加えた変更を元に戻してから新しい仮説に進む。
計装コードが入っている場合は計装プロセスの「仮説切り替え手順」に従う。
-
続行前に検証する:
- うまくいった? → フェーズ4へ
- うまくいかなかった? → 直交チェックを行い、残りの仮説一覧を再提示してユーザーに選ばせる(下記参照)
-
わからないとき: 「Xがわからない」と言う。知っているふりをしない
直交チェック(仮説が棄却されたとき):
次の仮説を選ぶ前に自問する:
- 「この新しい仮説は、棄却した仮説とは別のコンポーネント/レイヤーを指しているか?」
- 「同じ原因を別の言葉で言い換えていないか?」
棄却後は自己説明を求めてから残りの仮説一覧をユーザーに再提示し、次を選んでもらう(Wasonの確証バイアス対策: 反証を言語化することで無意識の「確証探し」を防ぐ):
H1 は棄却されました。
**この仮説が間違いだと言える根拠を1文で答えてください:**
(例: 「ログでX値がYだったため、コンポーネントAが原因ではないことが確認できた」)
残りの仮説から次を選んでください:
**H2: [コンポーネントB]** — (一行で仮説を説明)
**H3: [コンポーネントC]** — (一行で仮説を説明)
新しい仮説を追加することもできます。
同じコンポーネントへの仮説が2回連続で棄却された場合 → 別のレイヤーに移動する。
フェーズ4: 実装
症状ではなく根本原因を修正する:
- 失敗するテストケースを作成する — 修正前に必ず用意する
- 単一の修正を実装する — 特定された根本原因に対処(「ついでに」の改善・リファクタリングをバンドルしない)
- 修正を検証する — テストが通るか、他のテストが壊れていないか
修正がうまくいかない場合:
- 3回未満の失敗: フェーズ1に戻り、新しい情報で再分析
- 3回以上: 停止してアーキテクチャを問い直す(下記参照)
3回以上の修正が失敗した場合: アーキテクチャを問い直す
パターン:
- 各修正が異なる場所で新たな問題を明らかにする
- 修正の実装に「大規模なリファクタリング」が必要
- 各修正が別の場所で新たな症状を生む
これは仮説の失敗ではなく、アーキテクチャの誤りである。さらなる修正を試みる前に人間のパートナーと議論する。
計装プロセス: ランタイムログ計装
推測するな。観察せよ。
ログなしに修正なし — コードを読むだけでは不十分。証拠に基づいて仮説を判定する。
ステップ1: 問題の理解
ユーザーから確認する(不明な場合は質問する):
- 症状(期待値 vs 実際の動作)、再現手順、直近の変更
ステップ2: ロギングのセットアップ
環境に合わせて選択する。ログフォーマットとリージョン構文は references/common.md を参照。
言語別パターン:
JavaScript/TypeScript /
Python /
Ruby /
Go /
Rust /
Java /
Kotlin /
Swift /
React Native /
Flutter /
C/C++ /
C#
Web(ブラウザ + JavaScript/TypeScript): コレクターサーバーを起動
node -e "require('http').createServer((q,s)=>{s.setHeader('Access-Control-Allow-Origin','*');s.setHeader('Access-Control-Allow-Methods','POST,OPTIONS');s.setHeader('Access-Control-Allow-Headers','Content-Type');if(q.method==='OPTIONS'){s.writeHead(204).end();return}let b='';q.on('data',c=>b+=c);q.on('end',()=>{require('fs').appendFileSync('debug.log',b+'\n');s.writeHead(204).end()})}).listen(4567,()=>console.log('Collector: http://localhost:4567'))"
サーバーサイドのみ(Node.js, Python, Ruby, Go 等): サーバー不要、直接ファイル書き込み。
ステップ3: 仮説の生成と選択
3〜5つの仮説を生成する。各仮説は異なるコンポーネント/レイヤーを対象にすること(直交性必須)。
生成したらすぐにユーザーへ提示し、どれから計装するか選んでもらう:
以下の仮説を立てました。どれに計装を入れますか?
**H1: [コンポーネントA]** — (一行で仮説を説明)
検証: YのBefore/AfterでXの値を確認 コスト: 低
**H2: [コンポーネントB]** — (一行で仮説を説明)
検証: ... コスト: 中
**H3: [コンポーネントC]** — (一行で仮説を説明)
検証: ... コスト: 高
推奨: H1(最も反証しやすいため)
別の仮説があればここで追加できます。
選択を受けたら、選ばれた仮説のみにステップ4の計装を入れる。
すべてREJECTEDの場合、残りの仮説一覧を再提示してユーザーに次を選ばせる(フェーズ3の直交チェックを参照)。
ステップ3.5: 仮説切り替え(計装済みの状態で別の仮説を選んだとき)
計装コードが既に挿入されている状態でユーザーが別の仮説を選んだ場合は、先に現在の計装をロールバックしてからステップ4へ進む。
1. 計装ファイルを特定する
grep -rl "#region debug:" src/
Select-String -Path src\* -Pattern "#region debug:" -Recurse | Select-Object -ExpandProperty Path
2. 計装ファイルのみをリセットする
#region debug:H1 ... #endregion ブロックをすべて削除する。
git を使う場合は計装が入っているファイルのみを対象にリセットする(他の変更は保持される):
grep -rl "#region debug:" src/ | xargs git checkout --
git stash push -m "debug:H1 instrumentation" -- <計装ファイル1> <計装ファイル2>
# PowerShell(Windows)― 計装ファイルのみリセット
Select-String -Path src\* -Pattern "#region debug:" -Recurse |
Select-Object -ExpandProperty Path -Unique |
ForEach-Object { git checkout -- $_ }
3. debug.log をクリアする
rm -f debug.log
Remove-Item -Force debug.log -ErrorAction SilentlyContinue
4. 新しい仮説の計装を挿入する → ステップ4へ
ステップ4: 計装の挿入
各仮説のログを挿入する(3〜8箇所)。
計装ポイント:
- 関数エントリ(引数)、関数エグジット(戻り値)
- 重要な処理のBefore/After、分岐パス(どのif/elseが実行されたか)
必須: #region debug:{hypothesisId} でラップする(言語別テンプレートは上記リファレンス参照)
ステップ5: 古いログのクリア
rm -f debug.log
Remove-Item -Force debug.log -ErrorAction SilentlyContinue
ステップ6: 再現のリクエスト
1. ロギングが設定済みか確認(必要であればコレクター起動)
2. `debug.log` が削除済みか確認
3. アプリを起動/再起動
4. [バグをトリガーする具体的手順]
5. 完了したら教えてください
ステップ7: ログの分析
cat debug.log | jq .
jq 'select(.h == "H1")' debug.log
tail -f debug.log | jq .
Get-Content debug.log | ForEach-Object { $_ | ConvertFrom-Json }
Get-Content debug.log | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object { $_.h -eq "H1" }
Get-Content debug.log -Wait | ForEach-Object { $_ | ConvertFrom-Json }
各仮説を評価する:
| 判定 | 意味 |
|---|
| CONFIRMED | ログがこの仮説を明確に支持する |
| REJECTED | ログがこの仮説を否定する |
| INCONCLUSIVE | データが不十分 |
REJECTEDと判定した仮説ごとに自己説明を1文記録する(Wasonの確証バイアス対策):
H1 REJECTED: ログで X=42 が返っており、コンポーネントAの出力は正常。
H2 REJECTED: 分岐Bには到達していないことがタイムスタンプで確認できた。
この言語化なしに次の仮説へ進まない。
ステップ8: 修正
仮説がCONFIRMEDになった場合のみ。 最小限の差分で修正し、計装はそのままにしておく。
すべてREJECTEDの場合: 各仮説の自己説明を見直し、別サブシステムから新しい仮説を立てる → ステップ3へ。
ステップ9: 検証
debug.log を削除 → アプリを再起動 → 同じ操作を実行 → 修正前後のログを比較する。
ステップ10: クリーンアップ
ユーザーが修正を確認した後のみ。
grep -rn "#region debug:" src/
Select-String -Path src\* -Pattern "#region debug:" -Recurse
計装を削除し、debug.log を削除し、コレクターを停止する。
ログフォーマット
NDJSON(1行1JSON):
{"h":"H1","l":"state_before","v":{"userId":"123"},"ts":1702567890123}
| フィールド | 意味 |
|---|
| h | 仮説ID |
| l | ラベル |
| v | 値 |
| ts | タイムスタンプ(ミリ秒) |
計装の禁止事項
- ❌ ログ分析前に修正を提案する
- ❌ 仮説が1つだけ
- ❌ 検証前に計装を削除する
- ❌「たぶんこれ」という推測
- ❌ setTimeout/sleep を「修正」として使う
危険信号 — 停止してプロセスに従う
自分がこう考えていることに気づいたら:
- 「とりあえず修正して、後で調査する」
- 「Xを変えてみてうまくいくか試す」
- 「複数の変更を追加してテストを実行する」
- 「たぶんXだろう、それを直そう」
- 「完全には理解していないが、これでうまくいくかも」
- 「もう一回修正を試みる」(すでに2回以上試した場合)
- 各修正が異なる場所で新たな問題を明らかにする
- 同じコンポーネント/レイヤーへの仮説を3回以上繰り返している(一方向バイアスのサイン)
これらすべてが意味すること: 停止。フェーズ1に戻る。
クイックリファレンス
| フェーズ | 主な活動 | 成功基準 |
|---|
| 1. 根本原因 | エラーを読む、再現する、変更を確認、証拠を収集 | 何が、なぜを理解する |
| 2. パターン | 動作する例を見つける、比較する | 差異を特定 |
| 3. 仮説 | 複数の直交仮説を立てる、最も反証しやすいものから検証 | 確認済みまたは異なるレイヤーへ移動 |
| 4. 実装 | テスト作成、修正、検証 | バグ解決、テスト通過 |
| 計装 | セットアップ→仮説提示→ユーザー選択→計装→再現→分析→修正→検証→クリーンアップ | CONFIRMED仮説で修正 |
補助テクニック(references/)
root-cause-tracing.md - コールスタックを逆方向にトレースして元のトリガーを見つける
defense-in-depth.md - 根本原因発見後、複数レイヤーにバリデーションを追加
condition-based-waiting.md - 任意のタイムアウトを条件ベースのポーリングに置き換える
common.md - ログフォーマット、リージョン構文、モバイルログ取得
- 言語別:
javascript.md / python.md / ruby.md / go.md / rust.md / java.md / kotlin.md / swift.md / react-native.md / flutter.md / c-cpp.md / csharp.md
スクリプト(scripts/):
find-polluter.sh / find-polluter.ps1 - テスト汚染者の二分探索スクリプト
サンプルコード(assets/):
condition-based-waiting-example.ts - 条件ベース待機のTypeScript実装例