Claude Code でリファクタリングや新機能追加を行うと、既存機能の出力品質が意図せず劣化することがある。機能は正しく動いておりテストも通るが、ユーザーが期待する情報が出力から消えている。この記事では、実際に遭遇した「静かなデグレード」の事例と、出力仕様テストによる対策を紹介する。
何が起きたか
日本株・BTC のトレーディングシステムで、日次の投資提案を GitHub Issues に自動投稿している。このシステムにポートフォリオ統合最適化機能を追加した際、以下の流れで問題が発生した。
- 統合最適化機能を追加。成功時は 1 つの統合 Issue に「市場概要」「総評」を含む詳細な分析を出力する設計
- 失敗時のフォールバックとして、銘柄別 Issue + サマリー Issue を作成するパスも実装
- テスト全パス、PR マージ
- ある日、ポートフォリオ最適化が失敗しフォールバックが発動
- サマリー Issue を見るとポートフォリオ一覧とリンクテーブルだけ — 「結局ホールドすべき?買うべき?」がわからない
統合パスには存在する「銘柄横断の総評」が、フォールバックパスでは最初から実装されていなかった。しかしテストは両方とも通っていた。
サマリー Issue の Before / After
デグレード状態(修正前):
# 2026-04-03 総合投資評価
## 現在のポートフォリオ
(ポジション一覧テーブル)
## 銘柄別計画
(リンクテーブル)
## 子課題チェックリスト
(チェックボックス一覧)
修正後:
# 2026-04-03 総合投資評価
## 現在のポートフォリオ
(ポジション一覧テーブル)
## 銘柄別計画
(リンクテーブル)
## 銘柄別サマリー ← 追加
(各銘柄の総評を一覧表示)
## 総評 ← 追加
### アクション分布
| BUY | SELL | HOLD | 合計 |
### 全体判断
(HOLD多数→「積極エントリー不向き」等の方針)
平均信頼度: 55%
## 子課題チェックリスト
(チェックボックス一覧)
修正前は「データの一覧」しかなく、修正後は「判断に必要な分析」が加わっている。
なぜ Claude Code で起きやすいか
新しいパスに注力し、既存パスは最低限になる
Claude Code はタスクを忠実に実行する。「ポートフォリオ最適化を追加して」と指示すると、統合パス(成功時)は丁寧に設計される一方、フォールバックパス(失敗時)は「最低限動く」安全網として実装される。
人間の開発者でも同じことは起きるが、Claude Code はユーザーとして出力を見た経験がないため、「この情報がないと困る」という暗黙の期待を推測しにくい。
会話が切れるとコンテキストが消える
統合最適化を実装した会話と、フォールバックの問題に気づく会話は別セッション。前回の設計意図や「統合パスと同等の出力品質を保つ」という暗黙の要件は引き継がれない。
テストが「動作」を検証し「品質」を検証しない
既存のテストは以下を検証していた。
- 銘柄別 Issue が作成される
- DB に紐付けが保存される
- サマリー Issue が作成される
しかし「サマリー Issue に総評セクションが含まれるか」は検証していなかった。テストは全パスするため、デグレードに気づけない。
対策
以下の 3 つのアプローチで静かなデグレードを防止できる。
出力仕様をテストする(最も効果的)
「Issue が作成される」だけでなく「Issue に何が書かれているか」までテストする。
def test_fallback_summary_issue_contains_required_sections(self, ...):
"""サマリーIssueに銘柄別サマリーと総評が含まれる (#360)."""
# ...フォールバック実行...
body = summary_issue_call.kwargs["body"]
# 必須セクションの存在を検証
assert "## 銘柄別サマリー" in body
assert "## 総評" in body
assert "### アクション分布" in body
assert "### 全体判断" in body
# 各銘柄のサマリーが含まれている
assert "7203" in body
assert SAMPLE_PLAN_7203["summary"] in body
これにより、将来のリファクタリングで総評が消えたら即座にテストが落ちる。
ただし、Claude Code に「テストを書いて」とだけ指示すると、「Issue が作成される」レベルの動作確認テストで終わりがちだ。出力内容のアサーションまで書かせるには、CLAUDE.md にルールとして明記するのが効果的。
## テスト規約
- 外部出力(Issue本文、メール、通知、レポート等)を生成する機能のテストでは、
出力が作成されることだけでなく、出力に含まれるべき必須セクション・フィールドも
アサーションで検証する
- 複数の実行パス(成功/失敗/フォールバック)がある場合、全パスで出力内容を検証する
CLAUDE.md はセッションをまたいで参照されるため、毎回プロンプトで指示する必要がない。特に重要な関数にはコードコメントで # テスト時: 出力に必須セクション(総評, アクション分布)が含まれることを検証 のように補足しておくと、さらに確実になる。
複数パスがある機能は出力同等性を意識する
CLAUDE.md やコードコメントに明記しておく。
## 開発規約
- 新しいコードパスを追加する際は、既存パスの出力内容が劣化しないか確認する
- 特にフォールバック/エラーパスは「最低限動く」ではなく「同等の情報量」を目指す
Claude Code はコードコメントや CLAUDE.md のルールを忠実に守るため、明示しておくだけで効果がある。
出力のゴールデンファイルを用意する
ゴールデンファイルとは、「正しい出力はこうあるべき」という期待値を記録したファイルのこと。テスト時に実際の出力と比較し、差分があればテストを落とす。
たとえば、サマリー Issue のテンプレートと期待値ファイルを以下のように用意する。
テンプレート(Jinja2):
# {{ date }} 総合投資評価
## 現在のポートフォリオ
{{ portfolio_table }}
## 銘柄別計画
{{ plan_table }}
## 銘柄別サマリー
{{ per_stock_summary }}
## 総評
### アクション分布
{{ action_distribution }}
### 全体判断
{{ overall_judgement }}
## 子課題チェックリスト
{{ checklist }}
ゴールデンファイル(tests/golden/summary_issue.md):
# 2026-04-03 総合投資評価
## 現在のポートフォリオ
| 銘柄 | 数量 | 取得単価 | 現在値 |
|------|------|----------|--------|
| 7203 | 100 | 2,500 | 2,680 |
## 銘柄別計画
| 銘柄 | アクション | リンク |
|------|-----------|--------|
| 7203 | HOLD | #101 |
## 銘柄別サマリー
- **7203**: 上昇トレンド継続、HOLD推奨
## 総評
### アクション分布
| BUY | SELL | HOLD | 合計 |
|-----|------|------|------|
| 0 | 0 | 1 | 1 |
### 全体判断
HOLD多数 — 積極エントリー不向き。平均信頼度: 55%
## 子課題チェックリスト
- [ ] #101 7203
テストコード:
from pathlib import Path
def test_summary_matches_golden_file(self, ...):
"""サマリーIssueがゴールデンファイルと一致する."""
# ...フォールバック実行...
actual = summary_issue_call.kwargs["body"]
golden = Path("tests/golden/summary_issue.md").read_text()
assert actual == golden
完全一致が厳しい場合(日付やデータが動的に変わる等)は、必須セクションの見出しだけを抽出して比較する方法もある。
import re
def extract_headings(md: str) -> list[str]:
return re.findall(r"^#{1,3} .+", md, re.MULTILINE)
def test_summary_has_required_headings(self, ...):
actual_headings = extract_headings(summary_issue_call.kwargs["body"])
golden_headings = extract_headings(
Path("tests/golden/summary_issue.md").read_text()
)
assert actual_headings == golden_headings
この方法なら、テンプレートに新しいセクションを追加した際にゴールデンファイルも更新する必要があり、フォールバックパスだけ取り残される事態を防げる。
ゴールデンファイルを Claude Code に自動運用させるには
CLAUDE.md に「ゴールデンファイルを用意すること」と書くのは有効だが、それだけでは不十分な場面がある。実用的には 3 層の仕組みを組み合わせる。
| 層 | 何を書くか | 効果 |
|---|---|---|
| CLAUDE.md | 「外部出力を生成するテンプレートを修正する際は、対応するゴールデンファイルも更新すること」 | プロジェクト全体のルール |
| テストの存在自体 | tests/golden/ にファイルがあれば、テンプレート変更時にテストが落ちる | 仕組みで強制(指示に頼らない) |
| コードコメント | テンプレートに # golden: tests/golden/summary_issue.md と記載 | 対応関係が明示的になる |
一番強いのは 2 層目(テストの存在自体) だ。既にゴールデンファイルとテストがあれば、テンプレートを変更した時点でテストが落ちるので、Claude Code は自動的にゴールデンファイルも更新する。CLAUDE.md のルールは「最初の 1 回」ゴールデンファイルを作らせるために必要であり、2 回目以降はテストの仕組みが守ってくれる。
CLAUDE.md には「用意すること」よりも、いつ用意すべきか の判断基準を書くほうが実用的だ。
## テスト規約
- 外部出力(Issue本文、メール、通知等)を生成する機能には、
出力の期待値をゴールデンファイル(tests/golden/)として用意する
- テンプレートとゴールデンファイルの対応はコードコメントで明示する
まとめ
| 項目 | 内容 |
|---|---|
| 問題 | リファクタリングで新パスを追加した際、既存パスの出力品質がデグレード |
| 原因 | テストが「動作」のみ検証し、出力内容(品質)を検証していなかった |
| 対策 | 出力の必須セクションをアサーションでテスト |
| 教訓 | Claude Code は機能を正しく動かすが、ユーザー体験の同等性は暗黙の要件。明示的にテストで守る |
Claude Code は強力だが万能ではない。特に「正しく動くが情報が足りない」という静かなデグレードは、テストをすり抜けやすい。出力仕様のテストを習慣にすることで、AI 開発でも品質を維持できる。