# Issue #32: パイプライン中間結果へのアクセス

## 優先度: 低

## 概要

デバッグや監査目的で、パイプラインの各ステップの中間結果にアクセスしたいケースがある。現状では最終結果のみが取得可能。

## 現状の問題

```python
result = typed_pipeline(fetch_users, process_users, generate_report)
# result は generate_report の出力のみ
# fetch_users や process_users の出力は取得できない
```

### ユースケース

1. **デバッグ**: どのステップで問題が発生したか特定
2. **監査ログ**: 各ステップの入出力を記録
3. **部分的な再利用**: 途中結果を別のパイプラインで使用

## 採用: on_step コールバック

### 基本設計

```python
def typed_pipeline(
    *steps: Callable,
    on_step: Callable[[str, Any], None] | None = None,
    on_error: Callable[[Exception, str], Any] | None = None,
) -> Any:
    """
    Args:
        *steps: Pipeline steps
        on_step: 各ステップ完了後に呼ばれるコールバック (step_name, output)
        on_error: エラーハンドラー (exception, step_name) -> fallback_value or raise
    """
```

### 使用例

```python
steps = []

def on_step(step_name: str, output: Any) -> None:
    steps.append({"step": step_name, "output": output})

result = typed_pipeline(
    fetch_users, process_users, generate_report,
    on_step=on_step
)
```

### メリット

- 既存の戻り値型を変更しない
- 柔軟（ログ出力、メトリクス送信など自由）
- メモリ効率が良い（必要な情報のみ保持）
- on_error と組み合わせて使用可能

## テスト（TDD準拠）

```python
# tests/unit/core/test_on_step.py
import pytest
from dataclasses import dataclass
from typing import Any

from railway import node, typed_pipeline


@dataclass(frozen=True)
class StepRecord:
    """イミュータブルなステップ記録"""
    name: str
    output: Any


class TestOnStepCallback:
    def test_callback_receives_step_name_and_output(self):
        """on_step コールバックで各ステップの名前と出力を受け取る"""
        captured: list[StepRecord] = []

        def capture(name: str, output: Any) -> None:
            captured.append(StepRecord(name=name, output=output))

        @node
        def step1() -> int:
            return 1

        @node
        def step2(x: int) -> int:
            return x + 1

        result = typed_pipeline(step1, step2, on_step=capture)

        assert result == 2
        assert captured == [
            StepRecord(name="step1", output=1),
            StepRecord(name="step2", output=2),
        ]

    def test_no_callback_when_not_specified(self):
        """on_step 未指定時は通常動作"""
        @node
        def step1() -> int:
            return 1

        result = typed_pipeline(step1)
        assert result == 1

    def test_callback_with_on_error_fallback(self):
        """on_error でフォールバックした場合も on_step は呼ばれる"""
        captured: list[StepRecord] = []

        def capture(name: str, output: Any) -> None:
            captured.append(StepRecord(name=name, output=output))

        def handle_error(error: Exception, step: str) -> int:
            return 99  # フォールバック値

        @node
        def fail_step() -> int:
            raise ConnectionError("network error")

        @node
        def next_step(x: int) -> int:
            return x * 2

        result = typed_pipeline(
            fail_step, next_step,
            on_step=capture,
            on_error=handle_error
        )

        assert result == 198  # 99 * 2
        # フォールバック値が記録される
        assert captured[0] == StepRecord(name="fail_step", output=99)
        assert captured[1] == StepRecord(name="next_step", output=198)

    def test_callback_when_exception_propagates(self):
        """on_error で再送出した場合は途中まで記録される"""
        captured: list[StepRecord] = []

        def capture(name: str, output: Any) -> None:
            captured.append(StepRecord(name=name, output=output))

        def handle_error(error: Exception, step: str):
            raise  # 再送出

        @node
        def step1() -> int:
            return 1

        @node
        def fail_step(x: int) -> int:
            raise ValueError("validation error")

        @node
        def step3(x: int) -> int:
            return x * 3

        with pytest.raises(ValueError, match="validation error"):
            typed_pipeline(
                step1, fail_step, step3,
                on_step=capture,
                on_error=handle_error
            )

        # step1 の結果のみ記録される
        assert len(captured) == 1
        assert captured[0] == StepRecord(name="step1", output=1)

    def test_callback_for_logging(self):
        """実践例: ログ出力"""
        import io
        import sys

        log_output = io.StringIO()

        def log_step(name: str, output: Any) -> None:
            print(f"[{name}] -> {output}", file=log_output)

        @node
        def fetch() -> dict:
            return {"count": 5}

        @node
        def process(data: dict) -> str:
            return f"Processed {data['count']} items"

        typed_pipeline(fetch, process, on_step=log_step)

        log_content = log_output.getvalue()
        assert "[fetch]" in log_content
        assert "[process]" in log_content
```

## 実装計画（TDD）

### Phase 1: Red
上記テストを作成し、失敗することを確認

### Phase 2: Green
```python
# railway/core/pipeline.py
from typing import Callable, Any

def typed_pipeline(
    *steps: Callable,
    on_step: Callable[[str, Any], None] | None = None,
    on_error: Callable[[Exception, str], Any] | None = None,
) -> Any:
    """型安全なパイプライン実行"""
    result = None

    for step in steps:
        try:
            result = step() if result is None else step(result)
        except Exception as e:
            if on_error is None:
                raise  # デフォルト: 例外伝播
            result = on_error(e, step.__name__)

        # ステップ完了後にコールバック
        if on_step:
            on_step(step.__name__, result)

    return result
```

### Phase 3: Refactor
ドキュメント更新

## Issue #28 との関係

on_step と on_error は併用可能:

```python
def log_step(name: str, output: Any) -> None:
    logger.debug(f"[{name}] completed: {output}")

def handle_error(error: Exception, step: str) -> Any:
    match error:
        case ConnectionError():
            return load_from_cache()
        case _:
            raise

result = typed_pipeline(
    fetch_data,
    process_data,
    save_data,
    on_step=log_step,        # 各ステップをログ
    on_error=handle_error    # エラー時のフォールバック
)
```

## 受け入れ条件

- [ ] `on_step` コールバックの実装
- [ ] 各ステップ完了後にコールバックが呼ばれる
- [ ] `on_error` との併用が可能
- [ ] コールバックなしの場合は既存動作を維持
- [ ] テスト追加（TDD Red/Green/Refactor）
- [ ] ドキュメント更新

## 関連

- 外部監査（2026-01-22）: 優先度 低
- Issue #28: 部分的失敗とリカバリー（on_error）
