# Issue #04: 依存解決とパイプライン改修

## 概要
型ベースの依存解決機構を実装し、pipelineをOutput Modelパターンに対応させる。

## 依存関係
- Issue #01: Output Model基本設計（先行）
- Issue #02: Contractベースクラス実装（先行）
- Issue #03: nodeデコレータ拡張（先行）

## 現在の実装
```python
result = pipeline(initial_value, step1, step2, step3)
```

## 新しい実装

### pipeline関数のシグネチャ
```python
def pipeline(
    *nodes: Callable,
    params: Contract | dict = None,
) -> Contract:
    """
    Execute a pipeline of typed nodes.

    Args:
        *nodes: Node functions to execute
        params: Initial parameters (Params contract or dict)

    Returns:
        The output of the last node
    """
```

### 基本的な使用方法
```python
from railway import pipeline
from contracts.params import ReportParams
from nodes.fetch_users import fetch_users
from nodes.fetch_orders import fetch_orders
from nodes.generate_report import generate_report

# 実行
result = pipeline(
    fetch_users,
    fetch_orders,
    generate_report,
    params=ReportParams(user_id=1),
)

print(result.content)  # IDE補完が効く
```

## 依存解決エンジン

### DependencyResolver クラス
```python
# railway/core/resolver.py
from typing import Type, Callable, Any
from railway.core.contract import Contract

class DependencyResolver:
    """
    Resolves dependencies between nodes based on Contract types.
    """

    def __init__(self):
        self._results: dict[Type[Contract], Contract] = {}
        self._named_results: dict[str, Contract] = {}  # node名でも保存

    def register_result(self, result: Contract, source_name: str = None) -> None:
        """Register a node's result by its type and optionally by source name."""
        result_type = type(result)
        self._results[result_type] = result
        if source_name:
            self._named_results[source_name] = result

    def resolve_inputs(
        self,
        node_func: Callable,
    ) -> dict[str, Contract]:
        """
        Resolve inputs for a node from registered results.

        Args:
            node_func: The node function

        Returns:
            Dictionary of resolved inputs

        Raises:
            DependencyError: If a required input cannot be resolved
        """
        inputs_spec = getattr(node_func, "_node_inputs", {})
        resolved = {}

        for param_name, spec in inputs_spec.items():
            if isinstance(spec, Tagged):
                # Tagged指定: source名で解決
                if spec.source not in self._named_results:
                    raise DependencyError(
                        f"No result from node '{spec.source}'"
                    )
                resolved[param_name] = self._named_results[spec.source]
            else:
                # 型指定: 型で解決
                if spec in self._results:
                    resolved[param_name] = self._results[spec]
                else:
                    raise DependencyError(
                        f"Cannot resolve input '{param_name}' of type "
                        f"{spec.__name__} for node "
                        f"'{node_func._node_name}'"
                    )

        return resolved

    def get_result(self, result_type: Type[Contract]) -> Contract:
        """Get a result by type."""
        if result_type not in self._results:
            raise DependencyError(
                f"No result of type {result_type.__name__} available"
            )
        return self._results[result_type]


class DependencyError(Exception):
    """Raised when a dependency cannot be resolved."""
    pass
```

### pipeline実装
```python
# railway/core/pipeline.py
from typing import Callable, Any
from loguru import logger
from pydantic import create_model
from railway.core.contract import Contract, Params, Tagged
from railway.core.resolver import DependencyResolver, DependencyError

def pipeline(
    *nodes: Callable,
    params: Contract | dict = None,
) -> Contract:
    """
    Execute a pipeline of typed nodes.

    The pipeline:
    1. Creates a DependencyResolver
    2. Registers initial params
    3. Executes each node in order
    4. Resolves inputs for each node from previous results
    5. Returns the output of the last node
    """
    if not nodes:
        raise ValueError("Pipeline requires at least one node")

    resolver = DependencyResolver()

    # 初期パラメータの登録
    if params is not None:
        if isinstance(params, dict):
            # dictの場合、Pydanticのcreate_modelで動的に型を生成
            field_definitions = {k: (type(v), v) for k, v in params.items()}
            DynamicParams = create_model("DynamicParams", __base__=Params, **field_definitions)
            params = DynamicParams(**params)
        resolver.register_result(params, source_name="_params")

    logger.debug(f"Pipeline starting with {len(nodes)} nodes")

    last_result = None

    for node_func in nodes:
        node_name = getattr(node_func, "_node_name", node_func.__name__)

        try:
            # 入力の解決
            inputs = resolver.resolve_inputs(node_func)

            # nodeの実行
            result = node_func(**inputs)

            # 結果の登録（node名も保存）
            if result is not None:
                resolver.register_result(result, source_name=node_name)
                last_result = result

        except DependencyError as e:
            logger.error(f"Dependency error at node '{node_name}': {e}")
            raise
        except Exception as e:
            logger.error(f"Pipeline failed at node '{node_name}': {e}")
            raise

    logger.debug("Pipeline completed successfully")
    return last_result
```

### async_pipeline実装
```python
# railway/core/pipeline.py
import inspect
from pydantic import create_model

async def async_pipeline(
    *nodes: Callable,
    params: Contract | dict = None,
) -> Contract:
    """
    Async version of pipeline.
    """
    if not nodes:
        raise ValueError("Pipeline requires at least one node")

    resolver = DependencyResolver()

    # 初期パラメータの登録
    if params is not None:
        if isinstance(params, dict):
            field_definitions = {k: (type(v), v) for k, v in params.items()}
            DynamicParams = create_model("DynamicParams", __base__=Params, **field_definitions)
            params = DynamicParams(**params)
        resolver.register_result(params, source_name="_params")

    last_result = None

    for node_func in nodes:
        node_name = getattr(node_func, "_node_name", node_func.__name__)
        inputs = resolver.resolve_inputs(node_func)

        # async/sync の判定
        original = getattr(node_func, "_original_func", node_func)
        if inspect.iscoroutinefunction(original):
            result = await node_func(**inputs)
        else:
            result = node_func(**inputs)

        if result is not None:
            resolver.register_result(result, source_name=node_name)
            last_result = result

    return last_result
```

## 同一型の複数出力への対応

### 問題
```python
@node(output=UsersFetchResult)
def fetch_active_users() -> UsersFetchResult: ...

@node(output=UsersFetchResult)
def fetch_inactive_users() -> UsersFetchResult: ...

# どちらのUsersFetchResultを使うべき？
@node(inputs={"users": UsersFetchResult}, output=MergedResult)
def merge_users(users: UsersFetchResult) -> MergedResult: ...
```

### 解決策: タグ付き入力指定
```python
from railway import node, Tagged

@node(
    inputs={
        "active": Tagged(UsersFetchResult, source="fetch_active_users"),
        "inactive": Tagged(UsersFetchResult, source="fetch_inactive_users"),
    },
    output=MergedResult,
)
def merge_users(
    active: UsersFetchResult,
    inactive: UsersFetchResult,
) -> MergedResult:
    ...
```

### Tagged実装
```python
# railway/core/contract.py
from dataclasses import dataclass
from typing import Type

@dataclass(frozen=True)
class Tagged:
    """
    Tagged type specification for disambiguating multiple outputs of same type.

    Example:
        inputs={
            "active": Tagged(UsersFetchResult, source="fetch_active_users"),
        }
    """
    contract_type: Type[Contract]
    source: str  # ソースnode名
```

## 使用例

### 基本的なパイプライン
```python
from railway import pipeline, node
from contracts.user_contracts import UsersFetchResult
from contracts.report_contracts import ReportResult

@node(output=UsersFetchResult)
def fetch_users() -> UsersFetchResult:
    return UsersFetchResult(users=[...], total=10)

@node(inputs={"users": UsersFetchResult}, output=ReportResult)
def generate_report(users: UsersFetchResult) -> ReportResult:
    return ReportResult(content=f"{users.total} users")

# 実行
result = pipeline(fetch_users, generate_report)
print(result.content)  # "10 users"
```

### パラメータ付きパイプライン
```python
from railway import pipeline, Params

class FetchParams(Params):
    user_id: int
    limit: int = 100

@node(inputs={"params": FetchParams}, output=UsersFetchResult)
def fetch_users(params: FetchParams) -> UsersFetchResult:
    users = api.get_users(user_id=params.user_id, limit=params.limit)
    return UsersFetchResult(users=users, total=len(users))

# 実行
result = pipeline(
    fetch_users,
    generate_report,
    params=FetchParams(user_id=1, limit=50),
)
```

## テスト要件
- 基本的なパイプライン実行
- 依存解決の動作
- 複数入力nodeの解決
- Tagged指定の解決
- パラメータの注入
- DependencyErrorの発生
- async_pipeline

## 関連ファイル
- 新規: `railway/core/resolver.py`
- 修正: `railway/core/pipeline.py`
- 新規: `tests/unit/core/test_resolver.py`
- 修正: `tests/unit/core/test_pipeline.py`

## 優先度
最高
