# v0.3.2: 未追跡ファイル diff の読み込み上限を実用上ほぼ無制限まで引き上げる

この ExecPlan はリビングドキュメントです。`Progress`、`Surprises & Discoveries`、`Decision Log`、`Outcomes & Retrospective` の各セクションは作業の進行に合わせて随時更新してください。

## Purpose / Big Picture

kizu で未追跡 (untracked) ファイルの差分を表示する際、現在は先頭 8 KiB のみ読み込んで `[+N more bytes from new file]` と打ち切っている。このため数 KB を超える新規ファイル (ログ、テストフィクスチャ、少し長めのソースファイル等) の内容がほとんど見えない。

v0.3.2 ではこの上限を **64 MiB** まで引き上げ、ほぼすべての実ソース/テキストファイルを最後まで表示できるようにする。打ち切りマーカーとバイナリ判定ロジックは維持する (真に巨大なファイルや大量の untracked に対する OOM ガードを残すため)。

ユーザ観点の成果:

- AI コーディングエージェントが 30 KB・500 KB 程度の新規ファイルを作成しても、kizu の diff ビューで全行を閲覧・スクロールできる
- `[+... more bytes from new file]` マーカーは極端に大きなファイル (>64 MiB) の場合のみ出る
- 既存の tracked ファイルの diff には元々上限がないので挙動は変わらない

動作確認:

1. 適当なリポジトリで kizu を起動
2. 例えば 100 KB 程度のテキストファイルを新規作成
3. kizu の diff ビューでそのファイルが全行 `Added` として表示され、打ち切りマーカーが**出ない**ことを確認

## Progress

- [x] (2026-04-18 00:00:00Z) 該当箇所の特定完了 (`src/git.rs:9` `UNTRACKED_READ_CAP = 8 * 1024` の 1 箇所のみ)
- [x] (2026-04-18 00:20:00Z) TDD Red: `compute_diff_reads_untracked_file_below_cap_in_full` を追加、旧実装で期待通りの失敗メッセージ `"files well below the cap must not carry a truncation marker"` を確認
- [x] (2026-04-18 00:30:00Z) TDD Green: `UNTRACKED_READ_CAP` を `64 * 1024 * 1024` に更新、ドクコメント改訂、`synthesize_untracked_with_cap` shim を導入
- [x] (2026-04-18 00:32:00Z) 既存テスト `compute_diff_caps_untracked_file_at_read_limit` を `synthesize_untracked_reports_truncation_marker_when_file_exceeds_cap` (cap=4KB での shim 経由) にリネーム + 書き換え
- [x] (2026-04-18 00:35:00Z) `just rust` 全緑 (408 Rust tests)
- [x] (2026-04-18 00:40:00Z) `src/paths.rs` の `KIZU_STATE_DIR` / `KIZU_CONFIG` を操作するテストを `ENV_LOCK` Mutex で直列化 (既存 flake の scope 拡張修正)
- [x] (2026-04-18 00:42:00Z) `Cargo.toml` version を `0.3.1` → `0.3.2` に更新
- [x] (2026-04-18 00:45:00Z) `just ci` 全緑 (fmt-check → clippy → cargo test 408 → release build → e2e 33 tests)
- [ ] コミット + PR 作成 (main への merge はユーザ確認後)

タイムスタンプ形式: `(YYYY-MM-DD HH:MM:SSZ)`

## Surprises & Discoveries

- Observation: ローカル macOS での `just ci` が `paths::tests::session_file_with_override` / `paths::tests::events_dir_is_per_project` の 2 件で fail した
  Evidence: 両テストは `std::env::set_var("KIZU_STATE_DIR", ...)` してから `remove_var` する。cargo test 並列実行中、一方の `remove_var` が他方の assert 前に走るため `path.starts_with("/tmp/kizu-test-state/...")` が false になる。`cargo test -- --test-threads=1` では全 408 件緑、`git show HEAD:src/paths.rs` で v0.3.1 時点 (今回のブランチの base) にも同じコードが存在することを確認済み → v0.3.2 の変更とは独立した既存 flake。`ENV_LOCK: Mutex<()>` を `paths::tests` モジュール内に静的に置き、3 テストの先頭で lock 取得することで直列化した。3 行追加 + `_guard` 変数 3 箇所のみの最小介入。
- Observation: 新 `synthesize_untracked_with_cap` で `Vec::with_capacity(cap + 1)` を素朴に使うと、どんな小さな untracked ファイルでも常に 64 MiB + 1 の allocation が走ってしまう
  Evidence: 実 OS 上の malloc は on-demand paging なので RSS には直接影響しないが、仮想アドレス空間の無駄 + allocator 内部のアカウンティングコストは発生する。修正として `capacity = total_size.saturating_add(1).min(cap.saturating_add(1))` と実ファイルサイズで上限を絞り、`read_to_end` が cap+1 byte まで引き出す挙動はそのまま維持した。

## Decision Log

- Decision: 新しい上限値を 64 MiB (`64 * 1024 * 1024` bytes) にする
  Rationale: 「実用上ほぼ無制限」を満たす必要がある一方、`synthesize_untracked` は `Vec<u8>` に全量を読み込むのでメモリ爆発のガードは残しておきたい。典型的なソースコードファイル (~1 MB)、生成ファイル (数 MB〜数十 MB)、ログダンプの一部まで概ねカバーできる。さらに `git diff --binary` を使う tracked 側でも事実上 64 MiB は「大きすぎる」領域なので対称性もある。無制限 (`usize::MAX`) にすると巨大バイナリを untracked で誤って作成した際に一気に RAM に載ってしまうリスクがある。
  Date/Author: 2026-04-18 / ユーザ + Claude (Opus 4.7)

- Decision: バイナリ判定 (NUL バイト走査) のロジックは変更しない
  Rationale: 既存実装は読み込んだバッファ全体を `buf.contains(&0u8)` で走査しているので、上限を引き上げるだけでより大きなバイナリでも正しく Binary 判定になる。NUL が先頭 8 KiB に入っていない擬似バイナリ (UTF-16 等) も新上限までの範囲で検出できるようになる副作用は望ましい方向。
  Date/Author: 2026-04-18 / Claude

- Decision: 打ち切りマーカー (`[+N more bytes from new file]`) と `truncated` 分岐は残す
  Rationale: 64 MiB 超の真に異常なファイルに当たった際、ユーザに「これは全量ではない」と伝える明示的なシグナルが欲しい。分岐削除のリファクタは範囲外。
  Date/Author: 2026-04-18 / Claude

- Decision: t-wada 流 TDD を遵守する (テスト追従 → 値変更)
  Rationale: 既存テスト `compute_diff_caps_untracked_file_at_read_limit` は「8 KiB で打ち切られる」ことを assert している。先に値を変えるとテストが通ったまま意図せぬ挙動を見逃しうる。まず新上限を期待する形にテストを書き換え (Red)、その後に定数を更新 (Green) する。加えて「上限未満のファイルは全量読める」ことを確認する独立テストも追加する。
  Date/Author: 2026-04-18 / Claude

- Decision: `Vec::with_capacity` を `total_size.saturating_add(1).min(cap.saturating_add(1))` で絞る
  Rationale: cap を 64 MiB に上げた状態で素朴に `Vec::with_capacity(cap + 1)` とすると、untracked ファイル 1 件ごとに 64 MiB 相当の allocation が走る (on-demand paging で RSS は落ち着くが、仮想アドレス空間と allocator accounting の無駄)。ファイルサイズ上限で絞るのが自然。`read_to_end` は実データに応じて `Vec` を拡張するので挙動には影響しない。
  Date/Author: 2026-04-18 / Claude

- Decision: 今回の PR に `src/paths.rs` の既存 flake (`KIZU_STATE_DIR` / `KIZU_CONFIG` 並列レース) 修正を同梱する
  Rationale: 本修正は v0.3.2 の scope から言えば独立だが、ローカル macOS で `just ci` が通らない状態では CLAUDE.md の「コミット / PR を作る前に `just ci` が通ること」規約を満たせない。修正は `static ENV_LOCK: Mutex<()>` 1 箇所 + 3 テストでの lock 取得 3 行、合計 10 行弱の最小介入で、既存挙動を変えずに並列レースだけを防ぐ。別 PR に分離するよりコスト効率が高く、コミットメッセージで scope 拡張を明示する。
  Date/Author: 2026-04-18 / Claude

## Outcomes & Retrospective

(マイルストーン完了時に記入)

## Context and Orientation

### リポジトリ情報

- プロジェクト: `kizu` — AI コーディングエージェント (Claude Code 等) と並走するリアルタイム diff 監視 + inline scar review TUI (Rust 製の単一バイナリ)
- 作業ディレクトリ: `/Users/annenpolka/ghq/github.com/annenpolka/kizu`
- 現バージョン: `0.3.1` (`Cargo.toml:3`)
- ターゲットバージョン: `0.3.2`
- 開発ブランチ: `main` から直接作業(小さな変更のため。PR は切る)

### 用語定義

- **untracked ファイル**: `git` に add されていない新規ファイル。`git diff` のデフォルト出力には現れないので、kizu は `git status --porcelain` から拾って **合成 diff** (synthesize_untracked) を作る。
- **synthesize_untracked**: `src/git.rs:537` の関数。untracked ファイルの中身を読んで、全行を `Added` として扱う人工的な `FileDiff` を生成する。
- **UNTRACKED_READ_CAP**: 上記関数が読み込むバイト数の上限。現在は `8 * 1024`。
- **truncation marker**: 上限を超えた場合に diff 末尾に挿入される `[+N more bytes from new file]` という Context 行。
- **tracked diff**: `git diff --no-renames <baseline> --` の生出力をパースしたもの。こちらには kizu 側のバイト数上限は無い (git 自体が好きなだけ出力する)。
- **t-wada 流 TDD**: Red → Green → Refactor の短いサイクル。「期待通りのメッセージで Red になることを確認してから Green に進む」「1 サイクル数分」等が原則。詳細は `CLAUDE.md` の「開発スタイル」セクション。

### 関連ファイル (フルパス)

- `/Users/annenpolka/ghq/github.com/annenpolka/kizu/src/git.rs` — 唯一の編集対象 (本体コードおよびテスト)
- `/Users/annenpolka/ghq/github.com/annenpolka/kizu/Cargo.toml` — バージョン番号更新
- `/Users/annenpolka/ghq/github.com/annenpolka/kizu/Cargo.lock` — ビルド時に自動再生成
- `/Users/annenpolka/ghq/github.com/annenpolka/kizu/justfile` — `just ci` / `just rust` の定義 (読むだけ、編集不要)

### 現状の該当コード (`src/git.rs:7-9`)

```
/// Maximum number of bytes read from an untracked file when synthesizing its
/// "all-added" diff. See M1.9 / Decision Log for rationale.
pub const UNTRACKED_READ_CAP: usize = 8 * 1024;
```

### 関連するロジック (`src/git.rs:537-609`, `synthesize_untracked`)

1. `std::fs::metadata` で `total_size` を取得
2. `std::fs::File::open` でファイルを開く
3. `file.take(UNTRACKED_READ_CAP + 1)` で上限+1 byte だけ読む (上限超え検出のため)
4. `truncated = buf.len() > UNTRACKED_READ_CAP` を判定
5. 超えていたら `buf.truncate(UNTRACKED_READ_CAP)`
6. NUL 検出で Binary 判定
7. テキストなら `DiffLine` 列に展開
8. `truncated` なら末尾に `[+{remaining} more bytes from new file]` を追加

### 既存テスト (`src/git.rs:1736`)

`compute_diff_caps_untracked_file_at_read_limit` が現状:

- `99 char × 200 行 ≈ 20000 bytes` (= 約 20 KB) のファイルを書く
- `body.len() > UNTRACKED_READ_CAP` を assert (= 8 KiB 超)
- 合成された diff のバイト数が `UNTRACKED_READ_CAP + 100` 以下であることを assert
- `"more bytes from new file"` マーカーが含まれていることを assert
- `added` 行数が 200 未満であることを assert

上限を 64 MiB に引き上げると、この 20 KB のファイルでは**もはや truncate されない**。テストは「新上限 (64 MiB) を超えるケースを作り、同じ 4 項目を assert する」方向と、「新上限未満ではフルに読める」ことを別テストに分けて確認する方向の 2 本立てが妥当。

ただし、実 CI で 64 MiB + 1 bytes の一時ファイルを作るのはテスト所要時間・ディスク使用量・tmp dir 汚染の観点から重い。既存テストの精神 (cap 境界を assert する) は活かしつつ、**cap 境界はテスト内で一時的に小さい値を使う「テスト用 helper 関数」には出来ない** (`UNTRACKED_READ_CAP` は `pub const` であり、`synthesize_untracked` が直接参照している)。

### テスト戦略 (詳細)

以下の選択肢を検討した結果、**(A) 小さいファイルで truncate されないことを検証** + **(B) 既存テストを削除し、代わりに境界値を使う truncation テストを残すかどうかを Decision Log で判断**する方針にする:

- (A) 新規テスト `compute_diff_reads_untracked_file_up_to_new_cap`:
  - 例えば 100 KB のファイル (旧上限の 12 倍、新上限の 0.15%) を作成
  - truncation マーカー**不在** + 全 `added` 行数が想定どおり
- (B) 既存 `compute_diff_caps_untracked_file_at_read_limit`:
  - 単純に body を `64 MiB + 1 bytes` に拡大すると、テスト実行時に毎回 64 MiB を tmp に書き込むので CI が遅くなる (macOS / Linux の tmp が tmpfs ならメモリ上だが、ディスクの場合は重い)
  - 代替 1: `UNTRACKED_READ_CAP` 参照箇所をテスト時にモックできる shim (e.g. `read_untracked_capped(path, cap)`) に切り出し、テストだけ小さい cap を渡す
  - 代替 2: 既存テストを「新上限を超えたら truncation される」検証として残し、ファイルサイズを `UNTRACKED_READ_CAP + 1024` で作る (= 64 MiB + 1 KiB)。CI の実行時間と tmp 使用量を増やす
  - 代替 3: 既存テストを削除し、truncation ロジック自体は pure なユニットテスト `synthesize_untracked_reports_truncation_marker_when_buf_filled` に分離
  - **採用: 代替 1 (shim 切り出し)**。`synthesize_untracked_with_cap(root, rel_path, cap)` を private helper として追加し、`synthesize_untracked` はそれを `UNTRACKED_READ_CAP` で呼ぶだけにする。テストは cap に小さな値を渡せる。
  - ただし shim 切り出しは「本来の TDD スコープ外のリファクタ」でもあるため、**必要最小限の構造変更**として Decision Log に記録する。

## Plan of Work

t-wada 流 TDD の 3 サイクルで進める。

### サイクル 1: 現状テストを Red にする

1. `src/git.rs` 末尾のテストモジュールで、既存 `compute_diff_caps_untracked_file_at_read_limit` の直下に、新しいテスト `compute_diff_reads_untracked_file_below_cap_in_full` を追加する。以下を assert:
   - 100 KB のファイルを作成 (新上限 64 MiB 未満、旧上限 8 KiB を大きく超える)
   - `compute_diff` が truncation マーカーを含まない
   - `added` 行数が生成した全行数と一致
2. `just test` を実行 → **Red を期待** (現状実装では 8 KiB で打ち切られるので `added < 200` / マーカー付与)
3. Red メッセージが期待通り (`assertion failed: ..."more bytes from new file"...` 等) であることを確認

### サイクル 2: `UNTRACKED_READ_CAP` を 64 MiB に更新 (Green)

1. `src/git.rs:9` を書き換え:
   - `pub const UNTRACKED_READ_CAP: usize = 8 * 1024;`
   - → `pub const UNTRACKED_READ_CAP: usize = 64 * 1024 * 1024;`
2. 直上のドクコメント (`src/git.rs:7-8`) を更新:
   - 「See M1.9 / Decision Log」の参照は古い文脈なので、「See v0.3.2 / ADR...」または「Rationale: 実用上ほぼ無制限の上限。メモリ爆発ガードのため有限値を維持」に差し替え。**ADR は書かない** (ADR 運用ルールは「後から変えると痛い設計判断」のときのみ。今回は値調整 + 将来値を変える可能性ありのため不要。plans/v0.3.2.md の Decision Log で十分)。
3. 既存テスト `compute_diff_caps_untracked_file_at_read_limit` を cap shim 経由に書き換える:
   - まず `synthesize_untracked` を `synthesize_untracked_with_cap(root, rel_path, cap: usize)` 経由に切り出す
   - 元の `synthesize_untracked` は `synthesize_untracked_with_cap(root, rel_path, UNTRACKED_READ_CAP)` を呼ぶだけのラッパーにする
   - テスト側は `synthesize_untracked_with_cap` を cap=1024 等で直接叩いて truncation ロジックを検証する (この関数は `#[cfg(test)]` では pub(crate) 露出で十分)
   - テスト名を `synthesize_untracked_reports_truncation_marker_when_file_exceeds_cap` にリネーム。元の `compute_diff_caps_untracked_file_at_read_limit` 相当のエンドツーエンド assertion はサイクル 1 で追加した「cap 未満なら truncate しない」テストで十分カバーされる
4. `just test` を実行 → **Green を期待**
5. `just rust` (fmt + lint + test) を実行して全体緑を確認

### サイクル 3: バージョン番号とリリース準備

1. `Cargo.toml` の `version = "0.3.1"` → `"0.3.2"` に更新
2. `cargo build --release` を実行して `Cargo.lock` を再生成 (version フィールドが反映される)
3. `just ci` を実行 (fmt-check → clippy → cargo test → release → e2e) で全 CI gate が緑を確認
4. コミット作成 (prefix: `release: kizu v0.3.2 — raise untracked diff cap to 64 MiB`)
5. `gh pr create` で PR 作成 (main への merge はユーザ確認後)

## Concrete Steps

すべて作業ディレクトリ `/Users/annenpolka/ghq/github.com/annenpolka/kizu` で実行。

### Step 1: 現状確認

```
git status
git log --oneline -5
grep -n "UNTRACKED_READ_CAP" src/git.rs
```

期待:
- `git status` が clean
- `git log` に `a1e6422 docs: refresh demo.gif and retune tape for v0.3.1` を含む直近コミットが並ぶ
- `grep` で定数定義 1 行 + 参照 5-6 行が出る

### Step 2: サイクル 1 (Red) — 新規テスト追加

`src/git.rs` の既存 `compute_diff_caps_untracked_file_at_read_limit` の直後に以下を追加:

```
#[test]
fn compute_diff_reads_untracked_file_below_cap_in_full() {
    let repo = init_repo();
    fs::write(repo.path().join("seed.txt"), "seed").expect("write seed");
    run_git(repo.path(), &["add", "seed.txt"]);
    run_git(repo.path(), &["commit", "--quiet", "-m", "initial"]);
    let baseline = head_sha(repo.path()).expect("head_sha");

    // 100 KiB ファイル: 新上限 (64 MiB) 未満、旧上限 (8 KiB) を大きく超過。
    // truncation は発生してはならない。
    let line: String = "y".repeat(99);
    let body = (0..1024)
        .map(|_| line.as_str())
        .collect::<Vec<_>>()
        .join("\n");
    assert!(body.len() > 8 * 1024, "test body must exceed the old 8 KiB cap");
    fs::write(repo.path().join("big.txt"), &body).expect("write big");

    let files = compute_diff(repo.path(), &baseline).expect("compute_diff");
    let big = files
        .iter()
        .find(|f| f.path == Path::new("big.txt"))
        .expect("untracked big present");
    let hunks = match &big.content {
        DiffContent::Text(h) => h,
        DiffContent::Binary => panic!("unexpected binary classification"),
    };
    assert!(
        hunks[0]
            .lines
            .iter()
            .all(|line| !line.content.contains("more bytes from new file")),
        "files well below the cap must not be truncated"
    );
    assert_eq!(big.added, 1024, "all 1024 lines must be present");
}
```

```
just test 2>&1 | tail -40
```

期待: 新規テストが失敗 (truncation マーカー検出 or `big.added < 1024`)。

### Step 3: サイクル 2 (Green) — 定数更新 + shim 切り出し

1. `src/git.rs:7-9` の編集:

```
/// Maximum number of bytes read from an untracked file when synthesizing its
/// "all-added" diff. 64 MiB is effectively unlimited for source / text files
/// while keeping an OOM guard against pathological large binaries that an
/// agent might accidentally drop into the worktree. See plans/v0.3.2.md.
pub const UNTRACKED_READ_CAP: usize = 64 * 1024 * 1024;
```

2. `synthesize_untracked` の本体を `synthesize_untracked_with_cap` に切り出し、`synthesize_untracked` はそれを呼ぶラッパーに:

```
fn synthesize_untracked(root: &Path, rel_path: &Path) -> Result<FileDiff> {
    synthesize_untracked_with_cap(root, rel_path, UNTRACKED_READ_CAP)
}

fn synthesize_untracked_with_cap(
    root: &Path,
    rel_path: &Path,
    cap: usize,
) -> Result<FileDiff> {
    // ... 既存の本体を cap 引数で動くように置換 ...
}
```

3. 既存テスト `compute_diff_caps_untracked_file_at_read_limit` をリネーム + shim 経由化:

```
#[test]
fn synthesize_untracked_reports_truncation_marker_when_file_exceeds_cap() {
    let repo = init_repo();
    // 小さい cap でロジックだけ検証: 実 CI で 64 MiB を書かないため。
    let cap = 4 * 1024usize;
    let line: String = "x".repeat(99);
    let body = (0..200).map(|_| line.as_str()).collect::<Vec<_>>().join("\n");
    assert!(body.len() > cap);
    fs::write(repo.path().join("big.txt"), &body).expect("write big");

    let synth = synthesize_untracked_with_cap(repo.path(), Path::new("big.txt"), cap)
        .expect("synthesize_untracked_with_cap");
    let hunks = match &synth.content {
        DiffContent::Text(h) => h,
        DiffContent::Binary => panic!("unexpected binary classification"),
    };
    let total_bytes: usize = hunks[0].lines.iter().map(|l| l.content.len() + 1).sum();
    assert!(total_bytes <= cap + 100);
    assert!(
        hunks[0]
            .lines
            .iter()
            .any(|line| line.content.contains("more bytes from new file"))
    );
    assert!(synth.added < 200);
}
```

4. テスト実行:

```
just test 2>&1 | tail -40
```

期待: 全テスト緑。

### Step 4: lint / format / 全体 CI

```
just rust
just ci
```

期待: どちらも exit code 0 で終了。`just ci` では release build と bun e2e (tests/e2e/) も通る。

### Step 5: リリース準備

1. `Cargo.toml:3` の編集:

```
version = "0.3.2"
```

2. Cargo.lock 再生成:

```
cargo build --release
```

期待: `kizu v0.3.2` でビルド成功、`Cargo.lock` 内の kizu エントリが `version = "0.3.2"` に更新。

3. 最終 CI:

```
just ci
```

期待: 緑。

### Step 6: コミット + PR

```
git status
git diff --stat
git add src/git.rs Cargo.toml Cargo.lock plans/v0.3.2.md
git commit -m "release: kizu v0.3.2 — raise untracked diff cap to 64 MiB"
git log --oneline -3
```

PR 作成はユーザ確認後。

## Validation and Acceptance

### 自動テスト

- `cargo test --all-targets --all-features` で **全テスト緑**
- 以下 2 本のテストが新たに加わる / 変わる:
  - 新規: `compute_diff_reads_untracked_file_below_cap_in_full` — 100 KiB のファイルが truncation なしで読まれる
  - リネーム: `synthesize_untracked_reports_truncation_marker_when_file_exceeds_cap` — shim 経由で小さい cap を渡した時に truncation マーカーと `added < 全行数` を確認

### CI gate (ローカル)

- `just ci` が exit 0 (fmt-check → clippy -D warnings → cargo test → release build → e2e)
- e2e (`tests/e2e/` bun test) は既存シナリオがそのまま通る。untracked file を直接扱うシナリオはないので新規シナリオ追加は不要

### 手動確認 (Acceptance)

1. `cargo build --release` 後、テスト用 worktree で `target/release/kizu` を起動
2. 例えば `seq 1 5000 | awk '{printf "line %d: the quick brown fox\n", $1}' > big_new_file.txt` を実行 (≈ 150 KB)
3. kizu の diff ビューで `big_new_file.txt` にカーソルを合わせる
4. **期待**: 右ペインに全 5000 行が `Added` で表示され、末尾に `[+N more bytes from new file]` マーカーが**出ない**こと
5. スクロール (`j`/`k`/`J`/`K`) で末尾まで到達できること

### 回帰確認 (打ち切りが依然として動作すること)

64 MiB を超えるテキストファイルを手元で作るのは現実的でないため、手動 acceptance では省略。自動テスト `synthesize_untracked_reports_truncation_marker_when_file_exceeds_cap` で shim 経由で打ち切りロジックが健在であることを検証済みとみなす。

## Idempotence and Recovery

- すべての編集は 1 ファイル (`src/git.rs` + `Cargo.toml`) への変更なので、問題があれば `git restore <path>` で簡単に戻せる
- `Cargo.lock` の再生成は `cargo build --release` 一発。失敗時は `git checkout Cargo.lock && cargo build --release` で再実行可能
- ExecPlan 自体 (`plans/v0.3.2.md`) はコミット前なら自由に編集可能
- PR 作成後に問題が見つかった場合、force push を避け、追加コミットで修正する
- コミット前に破壊的操作を避ける: `git reset --hard` や `git clean -f` は使わない

## Artifacts and Notes

(実装中に有用な出力を追記)

### 該当コード (変更前の参考)

    src/git.rs:7-9
    /// Maximum number of bytes read from an untracked file when synthesizing its
    /// "all-added" diff. See M1.9 / Decision Log for rationale.
    pub const UNTRACKED_READ_CAP: usize = 8 * 1024;

### 該当コード (変更後の目標)

    src/git.rs:7-13
    /// Maximum number of bytes read from an untracked file when synthesizing its
    /// "all-added" diff. 64 MiB is effectively unlimited for source / text files
    /// while keeping an OOM guard against pathological large binaries that an
    /// agent might accidentally drop into the worktree. See plans/v0.3.2.md.
    pub const UNTRACKED_READ_CAP: usize = 64 * 1024 * 1024;

## Interfaces and Dependencies

### 外部ライブラリ

- 変更なし。`anyhow`, `std::fs`, `std::io::Read` のみ使用

### Public API の変更

- `pub const UNTRACKED_READ_CAP: usize` — 値のみ変更、シグネチャ維持。外部 crate はこれを参照していないので破壊的変更ではない (kizu はバイナリ crate)
- `src/git.rs` 内の private helper `synthesize_untracked` は維持
- 新規 private helper `synthesize_untracked_with_cap(root: &Path, rel_path: &Path, cap: usize) -> Result<FileDiff>` を追加。テストからのみ直接呼ばれる想定

### 影響する呼び出し元

- `compute_diff` (`src/git.rs:263` 付近) から `synthesize_untracked` を呼んでいる。シグネチャが変わらないので影響なし
- `synthesize_untracked_diff_text` (`src/git.rs:187`) も同関数を呼ぶ。影響なし

### リリース成果物

- `Cargo.toml` version: `0.3.1` → `0.3.2`
- `Cargo.lock` の kizu エントリも自動追従
- git tag / GitHub Release は本 ExecPlan の範囲外 (ユーザが後続で実施)
