# 機能設計書 6-記事リビジョン

## 概要

本ドキュメントは、Ghost CMSにおける記事リビジョン機能について、その設計仕様を記載する。記事リビジョン機能は、記事の編集履歴を保存し、過去のバージョンを参照・復元するための機能を提供する。

### 本機能の処理概要

記事リビジョン機能は、記事が編集されるたびに自動的に編集履歴（リビジョン）を保存する機能である。ユーザーが明示的に保存した場合、一定時間経過後のバックグラウンド保存、公開・非公開の状態変更時など、様々なタイミングでリビジョンが作成される。これにより、誤った編集からの復元や、編集履歴の追跡が可能になる。

**業務上の目的・背景**：コンテンツ編集において、誤って重要な内容を削除したり、以前のバージョンに戻したいケースが頻繁に発生する。リビジョン機能により、編集履歴を自動保存し、いつでも過去の状態を参照・復元できるため、コンテンツの安全性と編集作業の効率が向上する。

**機能の利用シーン**：
- ライターが誤って段落を削除し、以前のバージョンから復元したい
- 編集者が記事の変更履歴を確認し、誰がいつどのような変更を行ったかを追跡する
- 管理者が公開前の下書きバージョンと公開後のバージョンを比較する
- チームで共同編集を行い、各メンバーの変更を把握する

**主要な処理内容**：
1. リビジョンの自動保存（記事保存時）
2. リビジョン一覧の取得（記事詳細取得時にinclude指定）
3. リビジョンの制限管理（最大25件）
4. 古いリビジョンの自動削除

**関連システム・外部連携**：
- 記事管理機能との連携（Postモデルのフック経由）
- ユーザー管理機能との連携（author_idの記録）

**権限による制御**：
- リビジョンの閲覧: 記事の閲覧権限を持つユーザー
- リビジョンの作成: 記事の編集権限を持つユーザー（自動的に作成）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 13 | エディタ画面 | 補助機能 | 編集履歴の自動保存 |
| 14 | 投稿復元画面 | 主画面 | 削除された投稿の復元処理 |

## 機能種別

データ保存 / 履歴管理

## 入力仕様

### 入力パラメータ

リビジョンは自動的に作成されるため、直接的な入力パラメータはない。以下は記事保存時に参照される情報である。

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| lexical | string | Yes | 記事のLexical形式コンテンツ | - |
| title | string | Yes | 記事タイトル | - |
| author_id | string | Yes | 編集者のユーザーID | - |
| feature_image | string | No | アイキャッチ画像URL | - |
| feature_image_alt | string | No | アイキャッチ画像のalt属性 | - |
| feature_image_caption | string | No | アイキャッチ画像のキャプション | - |
| custom_excerpt | string | No | カスタム抜粋 | - |
| post_status | string | Yes | 記事の公開状態 | - |

### 入力データソース

- 記事の保存処理（PostモデルのonSavingフック経由）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | リビジョンの一意識別子 |
| post_id | string | 関連する記事のID |
| lexical | string | Lexical形式のコンテンツ |
| title | string | その時点のタイトル |
| author | object | 編集者情報（リレーション） |
| feature_image | string | アイキャッチ画像URL |
| feature_image_alt | string | アイキャッチ画像のalt属性 |
| feature_image_caption | string | アイキャッチ画像のキャプション |
| custom_excerpt | string | カスタム抜粋 |
| post_status | string | 記事の公開状態 |
| reason | string | リビジョン作成理由 |
| created_at_ts | number | 作成タイムスタンプ（ミリ秒） |
| created_at | datetime | 作成日時 |

### 出力先

- データベース（post_revisionsテーブル）
- APIレスポンス（記事取得時にinclude=post_revisionsで取得）

## 処理フロー

### 処理シーケンス

```
1. 記事保存処理開始
   └─ PostモデルのonSavingフック呼び出し
2. リビジョン生成判定
   └─ shouldGenerateRevision()で判定
3. 判定条件のチェック
   └─ 初回保存: initial_revision
   └─ 公開: published
   └─ 非公開化: unpublished
   └─ 明示的保存（Cmd+S）: explicit_save
   └─ バックグラウンド保存（10分経過）: background_save
4. リビジョン作成
   └─ convertPostLikeToRevision()で変換
5. リビジョン数の制限
   └─ 最大25件を超える場合は古いものを削除
6. データベース保存
   └─ post_revisionsテーブルに挿入
```

### フローチャート

```mermaid
flowchart TD
    A[記事保存処理開始] --> B{リビジョン存在?}
    B -->|No| C[initial_revision]
    B -->|Yes| D{状態変更?}
    D -->|公開| E[published]
    D -->|非公開化| F[unpublished]
    D -->|変更なし| G{内容変更あり?}
    G -->|No| H[リビジョン作成スキップ]
    G -->|Yes| I{明示的保存?}
    I -->|Yes| J[explicit_save]
    I -->|No| K{10分経過?}
    K -->|Yes| L[background_save]
    K -->|No| H
    C --> M[リビジョン作成]
    E --> M
    F --> M
    J --> M
    L --> M
    M --> N{25件超過?}
    N -->|Yes| O[古いリビジョン削除]
    N -->|No| P[DB保存]
    O --> P
    P --> Q[完了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 最大リビジョン数 | 各記事に対して最大25件のリビジョンを保持 | 常時 |
| BR-002 | バックグラウンド保存間隔 | 内容変更がある場合、前回のリビジョンから10分経過後に自動保存 | 自動保存時 |
| BR-003 | 初回リビジョン | 新規記事の最初の保存時は必ずリビジョンを作成 | 新規記事保存時 |
| BR-004 | 公開時リビジョン | 記事を公開する際は必ずリビジョンを作成 | 公開処理時 |
| BR-005 | 非公開時リビジョン | 記事を非公開（draft）にする際は必ずリビジョンを作成 | 非公開処理時 |
| BR-006 | 明示的保存 | ユーザーがCmd+Sで保存した場合は内容変更があればリビジョンを作成 | 明示的保存時 |

### 計算ロジック

- **リビジョン生成判定**: shouldGenerateRevision()メソッドで以下を確認
  - リビジョンが0件の場合: true（初回）
  - 状態がpublishedに変更: true
  - 状態がdraftに変更（元がpublished）: true
  - forceRevision指定 + 内容変更あり: true
  - 10分経過 + 内容変更あり: true
  - それ以外: false

- **内容変更の判定**: 以下のフィールドを比較
  - lexical（本文）
  - title（タイトル）
  - feature_image（アイキャッチ画像）
  - custom_excerpt（カスタム抜粋）

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| リビジョン作成 | post_revisions | INSERT | 新規リビジョンレコードの挿入 |
| リビジョン取得 | post_revisions | SELECT | 記事に関連するリビジョン一覧取得 |
| 超過リビジョン削除 | post_revisions | DELETE | 最大件数を超える古いリビジョンの削除 |
| 著者削除時の更新 | post_revisions | UPDATE | 削除された著者のauthor_idをnullに設定 |

### テーブル別操作詳細

#### post_revisions

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | 自動生成 | 24文字の文字列 |
| INSERT | post_id | 対象記事のID | |
| INSERT | lexical | 現在のLexical JSON | |
| INSERT | title | 現在のタイトル | 最大255文字 |
| INSERT | author_id | 編集者のユーザーID | |
| INSERT | feature_image | 現在のアイキャッチ画像URL | |
| INSERT | feature_image_alt | アイキャッチ画像のalt | |
| INSERT | feature_image_caption | アイキャッチ画像のキャプション | |
| INSERT | custom_excerpt | カスタム抜粋 | |
| INSERT | post_status | 記事の公開状態 | draft/published/scheduled/sent |
| INSERT | reason | 作成理由 | initial_revision/published/unpublished/explicit_save/background_save |
| INSERT | created_at_ts | 現在のタイムスタンプ（ミリ秒） | |
| INSERT | created_at | 現在日時 | |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | - | リビジョン機能自体はエラーを発生させない | 記事保存に失敗した場合はリビジョンも保存されない |

### リトライ仕様

リビジョンの保存は記事保存のトランザクション内で実行されるため、個別のリトライ処理はない。

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

リビジョンの作成は記事保存のトランザクション内で実行される。記事保存が失敗した場合、リビジョンも保存されない。

## パフォーマンス要件

- リビジョン取得: 各記事につき最大25件のため、パフォーマンス影響は限定的
- リビジョン作成: 記事保存処理の一部として実行、追加の遅延は最小限

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

- 認可: リビジョンの閲覧は記事の閲覧権限に準ずる
- データ保護: リビジョンに保存されるコンテンツは記事と同等のセキュリティで保護
- 著者追跡: 各リビジョンに編集者のIDを記録し、編集履歴を追跡可能

## 備考

- MobiledocからLexicalへの移行に伴い、新しいリビジョンはLexical形式で保存される
- 古いMobiledocリビジョンはmobiledoc_revisionsテーブルに別途保存されている（最大10件）
- リビジョンの復元機能はエディタUI側で実装されている

---

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

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

### 推奨読解順序

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

リビジョンの型定義とスキーマを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | post-revisions.ts | `ghost/core/core/server/lib/post-revisions.ts` | TypeScript型定義（1-27行目） |
| 1-2 | schema.js | `ghost/core/core/server/data/schema/schema.js` | post_revisionsテーブル（402-416行目） |

**読解のコツ**:
- PostLike型: 現在の記事データの形式
- Revision型: 保存されるリビジョンの形式
- created_at_ts: ミリ秒単位のタイムスタンプで保存間隔を計算

#### Step 2: リビジョン生成ロジックを理解する

リビジョン生成判定と作成処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | post-revisions.ts | `ghost/core/core/server/lib/post-revisions.ts` | PostRevisionsクラス全体（45-155行目） |

**主要処理フロー**:
- **55-89行目**: shouldGenerateRevision() - リビジョン生成判定
  - **58-60行目**: リビジョン0件の場合はinitial_revision
  - **62-65行目**: 非公開化の場合はunpublished
  - **67-70行目**: 公開の場合はpublished
  - **72-86行目**: 内容変更あり + forceRevisionまたは10分経過
- **91-113行目**: getRevisions() - リビジョン配列の更新
  - **108-109行目**: max_revisionsを超える場合は古いものを削除
- **115-129行目**: convertPostLikeToRevision() - 現在の記事をリビジョン形式に変換

#### Step 3: Postモデルでの呼び出しを理解する

PostモデルのonSavingフックでの呼び出しを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | post.js | `ghost/core/core/server/models/post.js` | onSavingフック内のリビジョン処理（906-951行目） |

**主要処理フロー**:
- **906-913行目**: PostRevisionsインスタンス化（max_revisions=25, revision_interval_ms=10分）
- **919-924行目**: 既存リビジョンの取得
- **928-939行目**: currentオブジェクトの構築（現在の記事データ）
- **942-948行目**: getRevisions()の呼び出しとモデルへのセット

#### Step 4: リビジョンモデルを理解する

PostRevisionモデルの定義を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | post-revision.js | `ghost/core/core/server/models/post-revision.js` | PostRevisionモデル定義 |

**主要処理フロー**:
- **7-8行目**: authorリレーション定義
- **32-34行目**: orderDefaultRaw - created_at_ts DESCでソート
- **36-42行目**: toJSON - author_idを削除（authorオブジェクトに含まれるため）

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

```
Post Model (post.js)
    │
    └─ onSaving()
           │
           └─ PostRevisions (post-revisions.ts)
                  │
                  ├─ shouldGenerateRevision()
                  │      ├─ 初回判定
                  │      ├─ 公開/非公開判定
                  │      ├─ 内容変更判定
                  │      └─ 時間経過判定
                  │
                  ├─ getRevisions()
                  │      ├─ convertPostLikeToRevision()
                  │      └─ max_revisions制限
                  │
                  └─ removeAuthorFromRevisions()
                         └─ 著者削除時のクリーンアップ

PostRevision Model (post-revision.js)
    │
    ├─ author() - Userとのリレーション
    └─ orderDefaultRaw() - 作成日時降順
```

### データフロー図

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

Post.edit() / Post.add()
       │
       ▼
┌─────────────────┐
│ onSaving()     │
│ Postモデルフック │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ PostRevisions   │
│ shouldGenerate  │
│ Revision()      │
└────────┬────────┘
         │
    ┌────┴────┐
    │ true?   │
    └────┬────┘
         │
         ▼
┌─────────────────┐
│ getRevisions()  │
│ - 現在の記事から │
│   リビジョン作成 │
│ - 25件制限適用  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ model.set()     │
│ 'post_revisions'│
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Database        │
│ post_revisions  │
└─────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| post-revisions.ts | `ghost/core/core/server/lib/post-revisions.ts` | ライブラリ | リビジョン生成ロジック |
| post-revision.js | `ghost/core/core/server/models/post-revision.js` | モデル | PostRevisionモデル定義 |
| post.js | `ghost/core/core/server/models/post.js` | モデル | Postモデル（リビジョン呼び出し元） |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | スキーマ | DBスキーマ定義（post_revisionsテーブル） |
