# 終端ノード型安全化 実装計画

**目的**: `DefaultExitContract` を廃止し、`railway sync transition` 時に終端ノードの型安全性を保証する

**バージョン**: v0.12.3（非破壊的）→ v0.12.3（破壊的）

---

## 背景

v0.12.2 で導入した `DefaultExitContract` は「ランタイムでの後方互換パッチ」であり、型安全性を損なう:

```python
# 現状: 実行時にラップ（Any型で型安全性なし）
if not isinstance(result, ExitContract):
    return DefaultExitContract(context=result, ...)
```

**問題点**:
- 型安全性がない（`context: Any`）
- IDE 補完が効かない
- 問題の検出が実行時まで遅延

## 解決策

「コンパイル時（sync時）に解決する」アプローチを **段階的に** 導入:

1. **スケルトン自動生成** (#44) - 未実装ノードに型安全なコードを生成
2. **返り値型チェック** (#45) - sync時に不正な返り値型を警告
3. **DefaultExitContract 削除** (#46) - ランタイムパッチを廃止、強制

---

## Issue 一覧と依存関係

```
         #39 (async_dag_runner エラーメッセージ) ─┐
         #40 (例外伝播テスト)                    │
         #41 (ADR-005 更新)                      │
                                                 ↓
         #44 (スケルトン生成) ←─────────────────────
           ↓
         #45 (型チェック警告)
           ↓
         #46 (DefaultExitContract 削除) ← v0.12.3
           ↓
         #47 (TUTORIAL.md 更新) ← 新規追加
```

### コアIssue（実装順）

| Issue | 優先度 | 内容 | 依存 | バージョン |
|-------|--------|------|------|------------|
| #44 | P0 | 終端ノードスケルトン自動生成 | #39, #40, #41 | v0.12.3 |
| #45 | P0 | 終端ノード返り値型チェック | #44 | v0.12.3 |
| #46 | P1 | DefaultExitContract 削除（強制） | #45 | v0.12.3 |
| #47 | P1 | TUTORIAL.md 更新 | #46 | v0.12.3 |

### 前提Issue（先に完了が必要）

| Issue | 優先度 | 内容 | 状態 |
|-------|--------|------|------|
| #39 | P0 | async_dag_runner エラーメッセージ修正 | 未着手 |
| #40 | P1 | 終端ノード例外伝播テスト追加 | 未着手 |
| #41 | P2 | ADR-005 ステータス更新 | 未着手 |

**順序の理由**:
- #39-41 で既存コードの品質を確保
- #44 で正しい形式のスケルトンを提供
- #45 で既存コードの問題を警告し、修正の機会を与える
- #46 で最終的に強制（破壊的変更）
- #47 でユーザー向けドキュメントを更新

---

## 設計方針

### 終端ノードスケルトン生成例

`railway sync transition --entry my_workflow` 実行時、未実装の終端ノードに対して:

```python
# 自動生成: src/nodes/exit/success/done.py
"""終端ノード: 正常終了

Auto-generated by `railway sync transition`.
"""
from railway import ExitContract, node


class SuccessDoneResult(ExitContract):
    """exit.success.done の結果。

    TODO: カスタムフィールドを追加してください。
    """
    exit_state: str = "success.done"


@node(name="exit.success.done")
def done(ctx: ExitContract) -> SuccessDoneResult:
    """正常終了処理。

    Args:
        ctx: 直前のノードからのコンテキスト（ExitContract 型）

    TODO: 実装してください。
    """
    return SuccessDoneResult()
```

### 命名規則

| YAML パス | Contract 名 | 関数名 |
|-----------|-------------|--------|
| `exit.success.done` | `SuccessDoneResult` | `done` |
| `exit.failure.timeout` | `FailureTimeoutResult` | `timeout` |
| `exit.failure.ssh.handshake` | `FailureSshHandshakeResult` | `handshake` |

### 移行パス（ユーザー向け）

```
v0.12.2 (現在)
    ↓ railway sync transition 実行
v0.12.3 (#44 適用後)
    - 未実装の終端ノードにスケルトン生成
    - 既存コードは引き続き動作（DefaultExitContract で互換）
    ↓ 警告に従って修正
v0.12.3 (#45 適用後)
    - 不正な返り値型に警告表示
    - 既存コードは引き続き動作
    ↓ コード修正
v0.12.3 (#46 適用後)
    - DefaultExitContract 削除
    - 不正な返り値型は TypeError
    ↓ ドキュメント更新
v0.12.3 (#47 適用後)
    - TUTORIAL.md に終端ノードの書き方を追記
```

---

## 関数型パラダイム遵守ポイント

### 純粋関数と副作用の分離

```python
# 純粋関数（テスト容易）
def generate_exit_node_skeleton(node: NodeDefinition) -> str: ...
def check_return_type(source: str, function_name: str) -> TypeCheckResult: ...

# 副作用を含む関数（最小限に分離）
def write_skeleton_file(path: Path, content: str) -> None: ...
def read_source_file(path: Path) -> str | None: ...
```

### イミュータブルデータ

```python
@dataclass(frozen=True)
class TypeCheckResult:
    is_valid: bool
    warnings: tuple[str, ...]  # list ではなく tuple

@dataclass(frozen=True)
class SyncResult:
    generated: tuple[Path, ...]
    skipped: tuple[Path, ...]
    warnings: tuple[str, ...]
```

**dataclass vs BaseModel の使い分け**:

| 用途 | パターン | 例 |
|------|----------|-----|
| ユーザー向け / シリアライズ必要 | `BaseModel` / `Contract` | `MigrationResult` |
| 内部処理用 / シリアライズ不要 | `@dataclass(frozen=True)` | `ValidationResult`, `SyncResult` |

`SyncResult`, `TypeCheckResult` は内部処理用のため `dataclass` を採用。

---

## 受け入れ条件

- [ ] 未実装終端ノードに対してスケルトンが生成される
- [ ] 生成されたスケルトンは型安全（`ctx: ExitContract` 型ヒント）
- [ ] 既存の終端ノードが ExitContract を返さない場合、警告を表示
- [ ] 最終的に DefaultExitContract が完全に削除されている
- [ ] TUTORIAL.md に終端ノードの実装例が追記されている
- [ ] 全テスト通過
