# 機能設計書 40-差分計算

## 概要

本ドキュメントは、OpenCodeにおけるスナップショット間の差分計算機能について記載する。この機能により、2つのスナップショット間でのファイル変更内容（追加/削除行数、変更ファイル、変更前後の内容）を詳細に取得できる。

### 本機能の処理概要

**業務上の目的・背景**：AIアシスタントが行った変更を可視化し、開発者がレビューできるようにするためには、単なる変更ファイル一覧だけでなく、具体的な追加/削除行数や変更内容が必要である。差分計算機能により、セッション内での全変更を詳細に表示し、変更の影響を把握できる。

**機能の利用シーン**：
- セッション画面でAIが行った変更の差分を表示する
- 変更前後のファイル内容を比較する
- 追加/削除行数を集計してセッションの変更量を把握する

**主要な処理内容**：
1. スナップショット間の差分テキスト取得（diff）
2. ファイル別の詳細差分情報取得（diffFull）
3. ファイルステータス判定（added/deleted/modified）
4. 追加/削除行数の計算
5. 変更前後のファイル内容取得

**関連システム・外部連携**：
- Git（差分計算エンジン）
- スナップショット機能（差分計算のベース）

**権限による制御**：スナップショット機能が無効の場合、差分計算も使用不可。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 2 | セッション画面 | 補助機能 | セッション差分（diffs）の計算と表示 |

## 機能種別

データ変換 / 差分計算

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| hash | string | Yes (diff) | 比較元スナップショットのハッシュ | 有効なgitハッシュ |
| from | string | Yes (diffFull) | 比較元スナップショットのハッシュ | 有効なgitハッシュ |
| to | string | Yes (diffFull) | 比較先スナップショットのハッシュ | 有効なgitハッシュ |

### 入力データソース

- スナップショットgitリポジトリ: 差分計算のソース

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| diff結果 | string | 統一diff形式のテキスト |
| FileDiff | object | ファイル別差分情報 |
| FileDiff.file | string | ファイルパス |
| FileDiff.before | string | 変更前の内容 |
| FileDiff.after | string | 変更後の内容 |
| FileDiff.additions | number | 追加行数 |
| FileDiff.deletions | number | 削除行数 |
| FileDiff.status | "added" \| "deleted" \| "modified" | 変更ステータス |

### 出力先

- 関数戻り値: 差分データ

## 処理フロー

### 処理シーケンス

```
1. 単純差分取得（diff）
   └─ 現在の状態をインデックスに追加
   └─ git diff {hash} -- .実行
   └─ 統一diff形式テキスト返却

2. 詳細差分取得（diffFull）
   └─ git diff --name-status {from} {to}でステータス取得
   └─ git diff --numstat {from} {to}で行数取得
   └─ 各ファイルについてgit show {hash}:{file}で内容取得
   └─ FileDiff配列返却
```

### フローチャート

```mermaid
flowchart TD
    A[diffFull呼び出し] --> B[git diff --name-status]
    B --> C[ステータスマップ作成]
    C --> D[git diff --numstat]
    D --> E[各行について処理]
    E --> F{バイナリファイル?}
    F -->|Yes| G[空文字列で設定]
    F -->|No| H[git show from:file]
    G --> I[FileDiff作成]
    H --> J[git show to:file]
    J --> I
    I --> K[結果配列に追加]
    K --> L{次の行あり?}
    L -->|Yes| E
    L -->|No| M[FileDiff配列返却]

    N[diff呼び出し] --> O[add実行]
    O --> P[git diff hash -- .]
    P --> Q{成功?}
    Q -->|No| R[空文字列返却]
    Q -->|Yes| S[diffテキスト返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-40-01 | バイナリファイル判定 | additions="-", deletions="-"の場合はバイナリファイルとして扱う | diffFull |
| BR-40-02 | バイナリファイル内容 | バイナリファイルの場合はbefore/afterを空文字列に設定 | diffFull |
| BR-40-03 | ステータス判定 | A=added, D=deleted, それ以外=modified | diffFull |
| BR-40-04 | 行数無限大対策 | parseInt結果がNumber.isFinite=falseの場合は0に設定 | diffFull |
| BR-40-05 | quotepath無効化 | core.quotepath=falseで非ASCII文字のエスケープを防止 | 全diff操作 |

### 計算ロジック

**ステータス判定**:
```javascript
const kind = code.startsWith("A") ? "added"
           : code.startsWith("D") ? "deleted"
           : "modified"
```

**行数計算**:
```javascript
const added = isBinaryFile ? 0 : parseInt(additions)
const deleted = isBinaryFile ? 0 : parseInt(deletions)
// Number.isFinite でガード
const safeAdded = Number.isFinite(added) ? added : 0
```

## データベース操作仕様

### 操作別データベース影響一覧

本機能はgitリポジトリをデータソースとして使用。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| DiffError | 差分エラー | git diffが失敗 | 空文字列を返却 |
| ShowError | 表示エラー | git showが失敗 | 空文字列を設定 |

### リトライ仕様

- 各操作の失敗時に自動リトライは行わない

## トランザクション仕様

該当なし

## パフォーマンス要件

- diffFullは各ファイルについてgit showを2回呼び出すため、変更ファイル数に比例した時間がかかる
- 大規模な変更では応答時間が長くなる可能性あり

## セキュリティ考慮事項

- 差分計算はスナップショットgitリポジトリ内でのみ実行
- ファイル内容の取得もスナップショット内に限定

## 備考

FileDiff型の構造:
```typescript
type FileDiff = {
  file: string          // 相対ファイルパス
  before: string        // 変更前の内容（全文）
  after: string         // 変更後の内容（全文）
  additions: number     // 追加行数
  deletions: number     // 削除行数
  status?: "added" | "deleted" | "modified"
}
```

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

FileDiff型の定義を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | index.ts | `packages/opencode/src/snapshot/index.ts` | FileDiff型定義（185-197行目） |

**読解のコツ**: FileDiffはzodスキーマで定義され、statusはオプショナル。metaでrefを設定してOpenAPI用の参照名を定義。

#### Step 2: 単純差分（diff）を理解する

統一diff形式での差分取得。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.ts | `packages/opencode/src/snapshot/index.ts` | diff関数（163-183行目） |

**主要処理フロー**:
- **165行目**: add関数で現在状態をインデックスに追加
- **166-170行目**: git diff --no-ext-diff {hash} -- .実行
- **172-179行目**: エラー時のログ出力と空文字列返却
- **182行目**: 結果をtrim()して返却

#### Step 3: 詳細差分（diffFull）を理解する

ファイル別の詳細差分情報取得。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | index.ts | `packages/opencode/src/snapshot/index.ts` | diffFull関数（198-250行目） |

**主要処理フロー**:
- **203-208行目**: git diff --name-status --no-renames {from} {to}でステータス取得
- **210-216行目**: ステータスのパース（A/D/それ以外）
- **218-222行目**: git diff --numstat {from} {to}で行数取得
- **224-225行目**: バイナリファイル判定
- **226-237行目**: git show {hash}:{file}で内容取得
- **238-247行目**: FileDiff オブジェクト生成

#### Step 4: ヘルパー関数を理解する

gitdir取得とadd関数。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | index.ts | `packages/opencode/src/snapshot/index.ts` | gitdir関数（252-255行目） |
| 4-2 | index.ts | `packages/opencode/src/snapshot/index.ts` | add関数（257-259行目） |

**主要処理フロー**:
- **253-254行目**: Global.Path.data + "snapshot" + project.idでパス生成
- **258-259行目**: syncExclude + git add .

### プログラム呼び出し階層図

```
Snapshot.diff(hash)
    │
    ├─ gitdir() - スナップショットリポジトリパス取得
    │
    ├─ add(git)
    │      │
    │      ├─ syncExclude(git)
    │      │
    │      └─ git add .
    │
    └─ git diff --no-ext-diff {hash} -- .
           │
           └─ 統一diff形式テキスト

Snapshot.diffFull(from, to)
    │
    ├─ gitdir()
    │
    ├─ git diff --name-status --no-renames {from} {to}
    │      │
    │      └─ ステータスマップ作成
    │
    └─ git diff --numstat {from} {to}
           │
           └─ [各行について]
                  │
                  ├─ [バイナリ判定]
                  │
                  ├─ git show {from}:{file} → before
                  │
                  ├─ git show {to}:{file} → after
                  │
                  └─ FileDiff {file, before, after, additions, deletions, status}
```

### データフロー図

```
[入力]                    [処理]                         [出力]

hash ───────────────▶ git diff hash ───────────────▶ 統一diff文字列

from, to ───────────▶ git diff --name-status ──────▶ ステータスマップ
                            │
                            ▼
                    git diff --numstat ────────────▶ 行数情報
                            │
                            ▼
                    [各ファイルについて]
                            │
                            ├─ git show from:file ───▶ before
                            │
                            └─ git show to:file ────▶ after
                                   │
                                   ▼
                            FileDiff {
                              file,
                              before,
                              after,
                              additions,
                              deletions,
                              status
                            }
                                   │
                                   ▼
                            FileDiff[] ─────────────▶ 結果配列
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.ts | `packages/opencode/src/snapshot/index.ts` | ソース | diff、diffFull関数、FileDiff型定義 |
| instance.ts | `packages/opencode/src/project/instance.ts` | ソース | プロジェクト情報（worktree） |
| global.ts | `packages/opencode/src/global.ts` | ソース | Global.Path.data（スナップショット保存先） |
