# 機能設計書 40-コマンド履歴

## 概要

本ドキュメントは、CoffeeScript REPLのコマンド履歴機能の設計を記述する。この機能により、入力した履歴をファイルに保存し、セッション間で共有できる。また、`.history`コマンドで履歴の表示も可能である。

### 本機能の処理概要

**業務上の目的・背景**：REPLセッション中に入力したコマンドを永続化し、次回セッションでも過去の入力を参照・再利用できるようにする。開発者の生産性向上と、よく使うコードスニペットの再利用を支援する。

**機能の利用シーン**：
- 前回のREPLセッションで入力したコードを再実行したい場合
- 上下キーで過去の入力を参照・編集したい場合
- `.history`コマンドで履歴全体を確認したい場合
- よく使うコードスニペットを履歴から呼び出したい場合

**主要な処理内容**：
1. REPL起動時に履歴ファイルから過去の履歴を読み込み
2. ファイルサイズ制限（デフォルト10KB）の適用
3. 履歴配列をREPLインスタンスに設定
4. 新規入力をlineイベントで監視し、履歴ファイルに追記
5. `.history`コマンドの登録と実装
6. REPL終了時にファイルディスクリプタをクローズ

**関連システム・外部連携**：ファイルシステム（fs）

**権限による制御**：履歴ファイルへの読み書き権限が必要。

## 関連画面

本機能はCLI機能のため、画面との直接的な関連はない。ただし、関連ドキュメント画面がある。

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 20 | Annotated Source - REPL | 補助機能 | addHistoryの解説を含む |
| 35 | Annotated Source - REPL (v2) | 補助機能 | addHistoryの解説を含む（v2） |

## 機能種別

データ永続化 / UI/UX機能

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| repl | Object | Yes | Node.js REPLインスタンス | - |
| filename | String | Yes | 履歴ファイルのパス | - |
| maxSize | Number | Yes | 最大履歴サイズ（バイト） | デフォルト10240 |

### 入力データソース

- 履歴ファイル（`~/.coffee_history`等）
- REPLのlineイベント（新規入力）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| repl.history | Array | 履歴コマンド配列（最新が先頭） |
| repl.historyIndex | Number | 現在の履歴インデックス |

### 出力先

- 履歴ファイル（追記）
- REPLインスタンスのhistoryプロパティ

## 処理フロー

### 処理シーケンス

```
1. addHistory関数の呼び出し
   └─ REPL起動時に呼び出し
2. 履歴ファイルの読み込み
   └─ fs.statSyncでファイル情報取得
   └─ maxSizeと比較して読み込みサイズを決定
3. 履歴データの設定
   └─ 末尾から読み込み、改行で分割、逆順にしてrepl.historyに設定
4. ファイルディスクリプタのオープン
   └─ fs.openSync(filename, 'a')で追記モード
5. lineイベントリスナーの設定
   └─ 新規入力を履歴ファイルに追記
6. SIGINTイベントの監視
   └─ sawSIGINTフラグの設定
7. exitイベントの監視
   └─ ファイルディスクリプタのクローズ
8. .historyコマンドの登録
   └─ 履歴表示機能
```

### フローチャート

```mermaid
flowchart TD
    A[REPL起動] --> B[addHistory repl, filename, maxSize]
    B --> C{履歴ファイル存在?}
    C -->|No| D[空の履歴で開始]
    C -->|Yes| E[fs.statSync でファイル情報取得]
    E --> F[size = min maxSize, stat.size]
    F --> G[fs.openSync filename, 'r']
    G --> H[Buffer.alloc size]
    H --> I[fs.readSync で末尾から読み込み]
    I --> J[fs.closeSync]
    J --> K[buffer.toString.split '\n'.reverse]
    K --> L{stat.size > maxSize?}
    L -->|Yes| M[先頭 pop 部分行除去]
    L -->|No| N[末尾 shift 空行除去]
    M --> N
    N --> O[repl.history = 配列]
    O --> P[repl.historyIndex = -1]
    P --> Q[lastLine = history 0]
    D --> R[fd = fs.openSync filename, 'a']
    Q --> R
    R --> S[repl.addListener 'line']
    S --> T[SIGINT/exitリスナー設定]
    T --> U[.historyコマンド登録]
    U --> V[REPL準備完了]

    V --> W{入力イベント?}
    W -->|line| X{code有効 && 前回と異なる?}
    X -->|Yes| Y[fs.writeSync fd, code + '\n']
    Y --> Z[lastLine = code]
    X -->|No| W
    Z --> W

    W -->|exit| AA[fs.closeSync fd]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-40-01 | ファイルサイズ制限 | 履歴ファイルの読み込みは最大maxSizeバイト | 常時 |
| BR-40-02 | 末尾読み込み | ファイルサイズがmaxSizeを超える場合は末尾から読み込み | stat.size > maxSize |
| BR-40-03 | 部分行除去 | ファイル切り詰め時は不完全な先頭行を除去 | stat.size > maxSize |
| BR-40-04 | 重複排除 | 前回と同じコマンドは履歴に追加しない | lastLine === code |
| BR-40-05 | 特殊コマンド除外 | `.history`と`.exit`は履歴に追加しない | code === '.history' or '.exit' |
| BR-40-06 | 空コマンド除外 | 空のコマンドは履歴に追加しない | !code or !code.length |
| BR-40-07 | 履歴ファイルパス | `$XDG_CACHE_HOME/.coffee_history`または`$HOME/.coffee_history` | デフォルト |

### 計算ロジック

**履歴読み込みロジック**:
```coffeescript
stat = fs.statSync filename
size = Math.min maxSize, stat.size
readFd = fs.openSync filename, 'r'
buffer = Buffer.alloc size
fs.readSync readFd, buffer, 0, size, stat.size - size  # 末尾からsizeバイト読み込み
fs.closeSync readFd

repl.history = buffer.toString().split('\n').reverse()  # 改行分割して逆順
repl.history.pop() if stat.size > maxSize               # 不完全な先頭行を除去
repl.history.shift() if repl.history[0] is ''           # 末尾の空行を除去
repl.historyIndex = -1
```

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

本機能はデータベース操作を行わない（ファイルシステム操作のみ）。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| ENOENT | ファイルエラー | 履歴ファイルが存在しない | 新規作成される |
| EACCES | 権限エラー | ファイルへのアクセス権限がない | ファイル権限を確認 |
| - | I/Oエラー | ディスク書き込みエラー | ディスク空き容量を確認 |

### リトライ仕様

リトライは行わない。ファイル操作エラーは無視されることが多い（REPLの動作継続を優先）。

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

該当なし。

## パフォーマンス要件

- 履歴読み込みは起動時に1回のみ
- 書き込みは各コマンド入力時に同期的に実行（遅延なし）
- 読み込みサイズ制限（デフォルト10KB）でメモリ使用量を制限

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

- 履歴ファイルには入力したコードがそのまま保存される
- 機密情報（パスワード、APIキー等）が履歴に残る可能性がある
- 適切なファイル権限の設定を推奨（0600等）

## 備考

**.historyコマンドの使用例**:
```
coffee> x = 1
1
coffee> y = 2
2
coffee> .history
x = 1
y = 2
coffee>
```

**履歴ファイルの場所**:
- Linux/macOS: `$HOME/.coffee_history`
- XDG対応システム: `$XDG_CACHE_HOME/.coffee_history`

---

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

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

### 推奨読解順序

#### Step 1: replDefaultsでの履歴設定を理解する

まず、履歴ファイルパスのデフォルト設定を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | repl.coffee | `src/repl.coffee` | replDefaultsでのhistoryFile設定 |

**主要処理フロー**:
- **13-15行目**: historyFile設定
  ```coffeescript
  historyFile: do ->
    historyPath = process.env.XDG_CACHE_HOME or process.env.HOME
    path.join historyPath, '.coffee_history' if historyPath
  ```
- **16行目**: `historyMaxInputSize: 10240` - デフォルト最大サイズ

**読解のコツ**: `do ->`は即時実行関数で、変数をスコープ内に閉じ込めている。

#### Step 2: addHistory関数の構造を理解する

履歴機能の本体を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | repl.coffee | `src/repl.coffee` | addHistory関数定義 |

**主要処理フロー**:
- **130-167行目**: addHistory関数全体
- **131行目**: `lastLine = null` - 重複検出用変数

#### Step 3: 履歴読み込み処理を理解する

ファイルからの履歴読み込みを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | repl.coffee | `src/repl.coffee` | 履歴読み込み処理 |

**主要処理フロー**:
- **132-148行目**: 履歴読み込み（try ブロック内）
  ```coffeescript
  try
    stat = fs.statSync filename
    size = Math.min maxSize, stat.size
    readFd = fs.openSync filename, 'r'
    buffer = Buffer.alloc size
    fs.readSync readFd, buffer, 0, size, stat.size - size
    fs.closeSync readFd
    repl.history = buffer.toString().split('\n').reverse()
    repl.history.pop() if stat.size > maxSize
    repl.history.shift() if repl.history[0] is ''
    repl.historyIndex = -1
    lastLine = repl.history[0]
  ```

**読解のコツ**:
- `stat.size - size`で末尾からの読み込み位置を計算
- `reverse()`で最新の履歴が配列先頭に来るようにする
- `stat.size > maxSize`の場合、不完全な先頭行をpopで除去

#### Step 4: 履歴書き込み処理を理解する

新規入力の履歴ファイルへの追記を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | repl.coffee | `src/repl.coffee` | 履歴書き込み処理 |

**主要処理フロー**:
- **150行目**: `fd = fs.openSync filename, 'a'` - 追記モードでオープン
- **152-156行目**: lineイベントリスナー
  ```coffeescript
  repl.addListener 'line', (code) ->
    if code and code.length and code isnt '.history' and code isnt '.exit' and lastLine isnt code
      fs.writeSync fd, "#{code}\n"
      lastLine = code
  ```

**読解のコツ**:
- `.history`と`.exit`は履歴に追加しない
- `lastLine isnt code`で連続する同じコマンドを除外

#### Step 5: イベントリスナーと.historyコマンドを理解する

終了処理とコマンド登録を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | repl.coffee | `src/repl.coffee` | イベントリスナーと.historyコマンド |

**主要処理フロー**:
- **158-159行目**: SIGINTとexitイベント
  ```coffeescript
  repl.on 'SIGINT', -> sawSIGINT = yes
  repl.on 'exit', -> fs.closeSync fd
  ```
- **162-167行目**: .historyコマンド登録
  ```coffeescript
  repl.commands[getCommandId(repl, 'history')] =
    help: 'Show command history'
    action: ->
      repl.outputStream.write "#{repl.history[..].reverse().join '\n'}\n"
      repl.displayPrompt()
  ```

**読解のコツ**:
- `repl.history[..]`はhistory配列のコピーを作成
- `reverse().join('\n')`で古い順に表示

#### Step 6: getCommandId関数を理解する

Node.jsバージョン間の互換性処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 6-1 | repl.coffee | `src/repl.coffee` | getCommandId関数 |

**主要処理フロー**:
- **169-172行目**: getCommandId関数
  ```coffeescript
  getCommandId = (repl, commandName) ->
    commandsHaveLeadingDot = repl.commands['.help']?
    if commandsHaveLeadingDot then ".#{commandName}" else commandName
  ```

**読解のコツ**: Node.js 0.11以降でコマンド名の形式が変更されたため、`.help`の存在で判定している。

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

```
start(opts) (repl.coffee:175)
    │
    └─ addHistory(repl, opts.historyFile, opts.historyMaxInputSize)
           │
           ├─ fs.statSync(filename)
           │      └─ ファイル情報取得
           │
           ├─ fs.openSync(filename, 'r')
           │      └─ 読み込み用オープン
           │
           ├─ fs.readSync(fd, buffer, 0, size, offset)
           │      └─ 末尾からsize バイト読み込み
           │
           ├─ buffer.toString().split('\n').reverse()
           │      └─ repl.historyに設定
           │
           ├─ fs.openSync(filename, 'a')
           │      └─ 追記用オープン
           │
           ├─ repl.addListener('line', ...)
           │      │
           │      └─ fs.writeSync(fd, code + '\n')
           │             └─ 履歴ファイルに追記
           │
           ├─ repl.on('SIGINT', ...)
           │
           ├─ repl.on('exit', ...)
           │      └─ fs.closeSync(fd)
           │
           └─ repl.commands['history'] = {...}
                  │
                  └─ action: repl.history[..].reverse().join('\n')
```

### データフロー図

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

~/.coffee_history        fs.readSync                     repl.history
     │                        │                               │
     │ "x = 1\n"              │                               │
     │ "y = 2\n"   ───▶   末尾から読み込み    ───▶      ['y = 2', 'x = 1']
     │ "z = 3\n"              │                               │
     │                        ▼                               │
                         split('\n').reverse()                │
                              │                               │
                              ▼                               ▼
                         repl.history = [...]          repl.historyIndex = -1
                              │
                              │
ユーザー入力              lineイベント                  履歴ファイル更新
"a = 4"                       │                               │
     │                        │                               │
     ▼                        ▼                               ▼
repl.emit('line')      code !== lastLine ?         fs.writeSync(fd, "a = 4\n")
     │                        │
     ▼                        ▼
                         lastLine = code


.history コマンド         action実行                   標準出力
     │                        │                               │
     ▼                        ▼                               ▼
                         history[..].reverse()         "x = 1\n
                              │                         y = 2\n
                              ▼                         z = 3\n
                         join('\n')                     a = 4"
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| repl.coffee | `src/repl.coffee` | ソース | addHistory関数、getCommandId関数 |
| fs | Node.js組み込み | モジュール | ファイル操作（statSync、openSync、readSync、writeSync、closeSync） |
