# 修正ループ実験（2026-10-23）

AIのレビュー役と修正役を、毎回新しい会話で交互に回すと何が起きるかを測った素材です。

## 中身

| ファイル | 内容 |
|---|---|
| `modules/` | 実験に使った4つの小さなPythonモジュール。`invoice.py`・`rate_limiter.py`は隠しテストに全部通る。`ranges.py`・`access.py`は仕様と食い違う本物のバグを1つずつ仕込んである |
| `tests/` | 隠しテスト（要件から先に書いた。請求書17・レート制限15・範囲10・アクセス10件）。レビュー役・修正役には見せない |
| `run_loop.py` | ループ本体。`claude -p`を道具なし・履歴なしで呼ぶ |
| `analyze.py` / `summarize.py` | 集計 |
| `results.jsonl` | 48ランの全ラウンドの記録（指摘・テスト結果・修正後のコード） |
| `summary.json` | 条件別の集計値 |

## 条件

- モデル：`claude-sonnet-5-5`、`claude-haiku-4-5-20251001`
- レビュー役の指示：`open`（「全部の問題を挙げて」）、`evidence`（「仕様違反だけ。具体的な入力と期待値つき。0件でよい」）
- 修正役：指摘を全部直し、ファイル全体を書き直す
- 4モジュール × 2モデル × 2指示 × 3回 = 48ラン。修正は最大6回、指摘が0件になったら止める
- 毎ラウンドの直前に隠しテストを実行して記録

## 素材を取得する

空のフォルダで次を実行すると、必要な14ファイルだけを階層を保って取得します。URLは公開後のサイトです。

```bash
base=https://aachat.io/blog/ai-code-review-fix-loop-stopping/experiment
for file in README.md run_loop.py results.jsonl summary.json summarize.py analyze.py modules/invoice.py modules/rate_limiter.py modules/ranges.py modules/access.py tests/test_invoice.py tests/test_rate_limiter.py tests/test_ranges.py tests/test_access.py; do
  mkdir -p "$(dirname "$file")"
  curl --fail --show-error --location "$base/$file" --output "$file" || exit 1
done
```

## 保存データをオフラインで集計する

```bash
python3 summarize.py results.jsonl
python3 analyze.py results.jsonl
# pytest導入済みの環境で。seededのranges/accessは意図的なバグで失敗する
PYTHONPATH=modules python3 -m pytest -q tests
```

## 再現

```bash
uv venv venv && uv pip install --python venv/bin/python pytest
PYTHON=venv/bin/python REPS=3 python3 run_loop.py out.jsonl
python3 summarize.py out.jsonl
```

累計編集行数は各roundの`step_changed`の和です。追加行＋削除行なので、1行の置換は2行と数え、再編集や取り消しも数えます。`cum_changed`は元コードから各round直前までのnet diffという歴史的fieldであり、累計ではありません。初回green後は、最初に`tests_failed=0`となったrow自身から末尾までのstepの和です（greenになる直前のfixは含めません）。green未到達runはこの平均の分母から除外します。

reviewのJSONやfindingsのshapeが不正なら、`parse_ok=false`、`stopped="parse_error"`で停止し、fixは呼びません。有効な空listだけが`no_findings`です。集計はparse失敗を収束・初回ゼロ・指摘数平均に含めず、失敗数を別に表示します。有効なparseがない平均はnull（未測定）です。保存済み143rowsは全てparse成功しており、この分離で歴史的な収束数は変わりません。

LLMの出力は毎回変わります。同じ数字にはなりません。
`total_cost_usd`はAPI料金に換算した額で、実際の請求額ではありません。
