*本記事で掲載しているコードは、所属企業とは何も関係のない、AIが生成しただけの例としての文字列になります。
この記事でわかること
- Claude Codeのカスタムコマンドの活用例
- コードの起源・来歴を調査するプロンプトの設計
作った背景
「このコード、なんでこうなってるの?」
長く運用されているプロジェクトで開発していると、こういう疑問が結構あります。
答えを見つけるには、だいたいこんな流れになると思います:
-
git blameで変更したコミットを特定 - コミットメッセージからPRを探す
- PRの説明やJiraチケットを読む
- それでもわからなければ仕様書を漁る
- 実はファイル移動されていて、移動前のファイルで1からやり直し
これ、毎回手動でやるの結構辛いですよね。
特に厄介なのが「ファイル移動」や「リファクタリング」を挟んでいるケース。表面的な変更だけ見ても真の起源にたどり着けないんですよね。
というわけで、この一連の調査をClaude Codeに任せるカスタムコマンドを作りました。
何を作ったか
/trace-code-origin というカスタムコマンドです。
使い方
/trace-code-origin UserProfileScreen.kt:120
または
/trace-code-origin なぜProfileStateでフォロー状態を管理しているのか?
ファイルパスと行番号でも、自然言語の質問形式でもOKです。
出力イメージ
コマンドを実行すると、こんな感じのレポートが出力されます:
実装履歴レポート: UserProfileScreen の ProfileState
対象コード
app/src/main/java/.../UserProfileScreen.kt:120-150
変更履歴
| 日付 | コミット | バージョン | 変更内容 | Jira | PR |
|------------|---------|--------|------------------------|------------|------|
| 2021-08-15 | a1b2c3d | v2.0.0 | ⭐ 起源: ProfileState新規作成 | TICKET-100 | #500 |
| 2021-09-20 | e4f5g6h | v2.1.0 | フォロー機能追加 | TICKET-120 | #550 |
| 2022-03-10 | i7j8k9l | v3.0.0 | Compose化に伴うリファクタリング | TICKET-200 | #800 |
背景・目的
なぜProfileStateが導入されたのか?
1. 状態管理の一元化: 元々プロフィール画面の状態は複数のLiveDataに分散していたが、UiStateパターンに統一するためProfileStateを導入
2. テスタビリティ向上: 状態を1つのdata classにまとめることで、ViewModelのユニットテストが書きやすくなった
3. SNS機能追加: v2.0.0でフォロー/フォロワー機能が追加され、状態管理が複雑化したため整理が必要だった
PR #500 の説明より:
プロフィール画面の状態をProfileStateで一元管理するようにしました。
今後の状態追加はこのクラスに集約してください。
結論
なぜこのコードがこの形になっているのか?
1. 初回実装の背景: SNS機能追加に合わせて、画面状態の管理方法を刷新するために新規作成された
2. 設計判断の根拠: 複数のLiveDataではなく単一のStateFlowで状態を管理することで、状態の整合性を保ちやすくした
3. 現在の形に至った経緯: Compose化を経て、現在はMVI的なアーキテクチャの中核として機能している
関連リンク
- 初回実装PR: #500
- JIRAチケット: TICKET-100
- Confluence (仕様): [リンク]
PRやJira、Confluenceのバージョン別ページへのリンクも含まれるので、詳細をすぐに確認できます。
工夫したポイント
1. 「振る舞い」の起源を追う
単純に git blame で見つかったコミットを報告して終わり。
例えばこんなdiffがあったとします:
- updateState { it.copy(isFollowing = userRepository.isFollowing(userId)) }
+ updateState { it.copy(profileState = ProfileState(isFollowing = userRepository.isFollowing(userId))) }
ProfileState は新しく追加されたように見えますが、isFollowing を取得する振る舞い自体は変更前から存在していたんですよね。もっと前の期限を追う必要があります。
なのでプロンプトでは「diffの削除行(-)も必ず確認し、変更前のコードの起源も追跡すること」と明示しています。
2.Phaseを明確に定義してスキップを防ぐ
Claudeくん、時々「まぁこれでいいか」と浅い調査で満足してしまうことがあるので、調査手順を7つのPhaseに分けて、チェックリスト形式で定義しました:
- Phase 1: 質問の本質を明確化
- Phase 2: 対象コードを特定
- Phase 3: git blameで直近の変更を確認
- Phase 4: 真の起源まで深掘り
- Phase 5: PR・Jiraチケットを調査
- Phase 6: リリースバージョン特定・Confluence参照
- Phase 7: 統合レポート作成
おわりに
「コード考古学」は地味だけど重要な作業ですよね。特にレビューや設計判断の際に「なぜこうなっているか」を知ることは、より良い意思決定につながると思っています。
Claude Codeのカスタムコマンドを使えば、こういった定型的だけど手間のかかる調査作業を効率化できます。便利ですね。
コマンド全文 一部抽象化しているので、各ユースケースに合わせて改修してお使いください。:pray
---
description: コードの起源・変遷を調査。git履歴→PR→Jira→Confluenceを辿り、真の起源まで深掘りする。
argument-hint: [ファイルパス:行番号] or [クラス名/関数名] or [コードスニペット]
---
# Trace Code Origin
特定の実装コードの**真の起源**と変遷を調査し、包括的なレポートを作成するコマンドです。
## 基本方針
**浅い調査で終わらせない。** 以下のケースを必ず考慮する:
1. **ファイル移動**: 現在のファイルは別の場所から移動されただけかもしれない
2. **コードコピー**: 別のファイルからコピーされたコードかもしれない
3. **リネーム**: クラス名や関数名が変更されているかもしれない
4. **定数/イベントの起源**: 使っているイベントや定数自体の起源も調べておく
### ⚠️ 最重要:「振る舞い」の起源を追う
具体的なクラス名やメソッド名だけではなく、**その振る舞いがいつから存在していたか**も調査する。
- diffを見たとき、削除行(-)にも同じ値やロジックがあれば、それはもっと前から存在していた証拠。→ 変更前のコードがいつから存在していたかを即座に追跡する。
- 削除行に存在しない場合でも、削除漏れなど、起源となるコードが別の場所に存在する可能性がある。必ず検索する。
#### 例
```diff
- updateState { it.copy(isFollowing = userRepository.isFollowing(userId)) }
+ updateState { it.copy(profileState = ProfileState(isFollowing = userRepository.isFollowing(userId))) }
上記の例で、isFollowing の指定は変更前にも存在していた。
→ この「変更前のコード」がいつから存在していたかを追跡する必要がある。
入力形式
以下のいずれかの形式で調査対象を受け取る:
1. ファイルパス + 行番号: path/to/File.kt:123
2. 関数名・クラス名: SomeViewModel や fetchData()
3. コードブロック: 特定のコードスニペット
4. 質問形式: 「なぜこのコードは〇〇なのか?」
---
調査手順(深掘りループ)
重要: 各 Phase を順番に実行し、絶対にスキップしないこと。
特に Phase 6 (リリースバージョン特定・Confluence参照) は忘れがちなので注意。
実行チェックリスト
調査完了前に、以下のすべてにチェックが入っていることを確認する:
- Phase 1: 質問の本質を明確化した(何を調べるべきか具体化した)
- Phase 2: 対象コードを特定した
- Phase 3: git blame で直近の変更を確認し、diffの「変更前」のコードも追跡した
- Phase 4: 真の起源まで深掘りした(コピー元、ファイル移動等を追跡)
- Phase 5: 関連する PR と JIRA チケットを調査した
- Phase 6: リリースバージョンを特定し、Confluence を参照した
- Phase 7: 統合レポートを作成した
---
Phase 1: 質問の本質を明確化する ⚠️ 最初に必ず実行
調査を始める前に、何を調べるべきかを正確に定義する。
確認すべきこと
1. 質問の対象は何か?
- 「なぜこのコードがあるのか」→ コード全体の起源
- 「なぜこの値が使われているのか」→ その値(定数・固定値)の起源
- 「なぜこの分岐があるのか」→ 分岐ロジックの起源
2. 調査対象を具体化する
3. 検索キーワードを決める
- 新しく追加されたクラス名/メソッド名ではなく
- 変わっていない値・パターンを検索キーワードにする
---
Phase 2: 対象コードの特定
# 該当コードを検索
grep -rn "対象のコード" --include="*.kt" --include="*.java"
# または Glob で該当ファイルを探す
Phase 3: 直近の変更履歴を確認 ⚠️ 変更前のコードも追跡!
# 該当行の blame を取得
git blame -L <start>,<end> <file>
# 該当コミットの詳細を確認
git show <commit-hash> --stat -p -- <file>
変更前のコードを追跡する
diffを見たら、必ず「変更前」の行を確認する。
# 変更前のコードに含まれる固定値・パターンで検索
git log -p --all --reverse -S "TYPE_NEW" -- "**/SomeFragment*" | head -300
# 変更前の行が最初に追加されたコミットを探す
判定基準
| 状況 | 対応 |
|-----------------------|----------------------|
| 変更前のコードに調査対象の値が含まれている | そちらを追跡する(真の起源はそこにある) |
| 変更は純粋な新規追加だった | Phase 4 に進む |
Phase 4: 深掘りループ(重要!)
このループを「真の起源」が見つかるまで繰り返す
Step 4.1: ファイル移動の追跡
# --follow でファイル移動を追跡
git log --follow --oneline --all -- "**/FileName*" | tail -30
# ファイルの最初のコミットを探す
git log --follow --diff-filter=A -- <file>
Step 4.2: コードのコピー元を探す
# -S オプションで特定の文字列を含むコミットを探す(逆順で最初を見つける)
git log -p --all --reverse -S "探したい文字列やコード" -- "**/*.kt" "**/*.java" | head -150
Step 4.3: 周辺コミットを確認する
# 直前5件を確認
git log --oneline <hash>~5..<hash>
# 直後5件を確認(HEAD側)
git log --oneline <hash>..HEAD | head -5
Step 4.4: 判定 - さらに深掘りが必要か?
以下の場合は さらに遡る:
| 状況 | 次のアクション |
|-------------------------|-----------------------------|
| コミットが「ファイル移動」だった | 移動元ファイルで同じ調査を繰り返す |
| コミットが「リネーム/リファクタ」だった | 変更前の名前で -S 検索 |
| コードが別ファイルからコピーされていた | コピー元ファイルで同じ調査を繰り返す |
| 使用しているイベント/定数が別で定義されていた | そのイベント/定数の起源を調査 |
| コミットメッセージが「〇〇対応」「〇〇実装」 | そのPR/チケットを調査し、なぜその実装になったか確認 |
以下の場合は 深掘り完了:
- --diff-filter=A(追加)で最初のコミットが見つかった
- コミットメッセージに「新規実装」「初期実装」などの記述がある
- それ以上遡れない(リポジトリの最初のコミットに到達)
Phase 5: 関連PRとチケットの調査
# コミットに関連するPRを検索
gh pr list --search "<commit-hash>" --state all --json number,title,url,body,author
# PRの詳細を取得
gh pr view <pr-number> --json title,body,author,createdAt,mergedAt,url
# チケット番号を探す
git log --oneline -- <file> | grep -oE 'TICKET-[0-9]+'
Phase 6: リリースバージョンの特定とConfluence参照 ⚠️ 必須
このフェーズは絶対にスキップしない!
コードの「なぜ」を理解するには、リリース時の背景情報が不可欠。
よくある失敗パターン
| 失敗 | 正しい対応 |
|----------------------|--------------------------------|
| PRが見つからなかったので諦めた | PRがなくてもConfluenceは必ず確認する |
| 技術的な理由が見つかったので満足した | ビジネス的背景(なぜその設計にしたか)も調べる |
| 古いコミットだから情報がないと決めつけた | 古いほどConfluenceに仕様書が残っている可能性が高い |
コミットがどのバージョンでリリースされたかを特定する:
# コミットを含む最初のリリースタグを特定
git tag --contains <commit-hash> | head -5
Confluenceのバージョン別ページを参照:
バージョンが特定できたら、Confluenceでそのバージョンの案件詳細を確認する。
Phase 7: 統合レポートの作成
レポート作成前の最終確認:
✅ Phase 1-6 をすべて実行したか?
✅ 特に Phase 6 (リリースバージョン・Confluence) を忘れていないか?
✅ 真の起源(最初のコミット)まで遡れたか?
✅ PR/JIRA の情報を取得したか?
上記すべてが完了してから、以下の形式でレポートを作成する。
---
出力形式
⚠️ 重要: 結論セクションには必ず以下のリンクを含めること:
- 最初の実装コミットへのリンク
- JIRAチケットへのリンク
- Confluenceバージョンページへのリンク
# 実装履歴レポート: [対象コード]
### 対象コード
[ファイルパス:行番号]
---
### 変更履歴
| 日付 | コミット | バージョン | 変更内容 | Jira | PR | 補足 |
|------|---------|-----------|---------|------|-----|------|
| YYYY-MM-DD | `hash` | v1.0.0 | **⭐ 起源: 初回実装** | TICKET-1 | #100 | 備考 |
| YYYY-MM-DD | `hash` | v2.0.0 | ファイル移動 | - | - | |
| YYYY-MM-DD | `hash` | v3.0.0 | リファクタリング | TICKET-2 | #200 | |
---
### 関連する定義/イベントの起源(該当する場合)
| 対象 | 起源コミット | 日付 | 背景 |
|------|-------------|------|------|
| `TYPE_NEW` | `hash` | YYYY-MM-DD | [なぜこの定義が作られたのか] |
---
### 結論
**なぜこのコードがこの形になっているのか?**
1. [理由1 - 初回実装の背景]
2. [理由2 - 設計判断の根拠]
3. [理由3 - 現在の形に至った経緯]
**潜在的な問題**: [もしあれば]
注意事項
- 古いコードの場合、git履歴が途切れている可能性がある
- チケット番号がコミットメッセージに含まれていない場合もある(PRから探す)
- ファイル移動が複数回行われている場合がある
- 「表面的な変更」で満足せず、真の起源を探ること