# Railway Framework for Python - 仕様書 v2.0

**改訂版: 使いやすさを最重視**
**改訂日: 2026-01-08**
**主な変更: シンプル化、学習曲線の緩和、一貫性の向上**

---

## 設計哲学

1. **シンプルさ第一**: 5分で始められること
2. **段階的な学習**: 初心者から上級者まで対応
3. **一貫性**: 予測可能で直感的なAPI
4. **型安全性**: mypyで完全にチェック可能
5. **実用性**: 実際の運用自動化に即座に使える

---

## 1. システムアーキテクチャ

### 1.1 全体アーキテクチャ（簡略化版）

```
┌──────────────────────────────────────────────────┐
│              CLI Layer (railway)                  │
│  - init: プロジェクト作成                          │
│  - new: エントリー/ノード追加                       │
│  - list: 一覧表示                                  │
└──────────────────────────────────────────────────┘
                      │
                      ▼
┌──────────────────────────────────────────────────┐
│           Template Engine (Jinja2)                │
│  - プロジェクト構造生成                             │
│  - 設定ファイル生成                                 │
│  - コードテンプレート生成                           │
└──────────────────────────────────────────────────┘
                      │
                      ▼
┌──────────────────────────────────────────────────┐
│          User Project (Generated)                 │
│  ┌────────────┐  ┌─────────┐  ┌──────────┐    │
│  │ Entries    │─→│ Nodes   │←─│ Common   │    │
│  │ (実行単位)  │  │(処理単位)│  │(共通機能) │    │
│  └────────────┘  └─────────┘  └──────────┘    │
└──────────────────────────────────────────────────┘
                      │
                      ▼
┌──────────────────────────────────────────────────┐
│              Core Libraries                       │
│  returns | tenacity | pydantic | loguru | typer  │
└──────────────────────────────────────────────────┘
```

### 1.2 シンプル化されたデータフロー

```
.env + config.yaml ──→ Settings Object
         │
         ▼
    Entry Point
         │
         ▼
    Node 1 ──→ Node 2 ──→ Node 3
      │          │          │
   Success    Success    Success ──→ Complete!
      │          │
      └─ Error ─┴─→ Skip remaining ──→ Fail!
```

---

## 2. モジュール構成（簡略化版）

### 2.1 フレームワークコア

```
railway/
├── __init__.py
├── cli/
│   ├── __init__.py
│   ├── main.py          # railway コマンド
│   ├── init.py          # railway init
│   ├── new.py           # railway new (entry/node統合)
│   └── list.py          # railway list
├── templates/
│   ├── project/
│   │   ├── pyproject.toml.jinja
│   │   ├── .env.example.jinja
│   │   ├── config.yaml.jinja        # 統合設定ファイル
│   │   ├── settings.py.jinja
│   │   └── tutorial.md.jinja        # チュートリアル
│   ├── entry/
│   │   └── entry.py.jinja           # シンプル化
│   └── node/
│       ├── node.py.jinja
│       └── test_node.py.jinja
├── core/
│   ├── __init__.py
│   ├── decorators.py    # @railway.entry_point, @railway.node
│   ├── errors.py
│   └── types.py         # 型定義
└── utils/
    ├── __init__.py
    └── file_ops.py
```

### 2.2 ユーザープロジェクト構造（簡略化版）

```
my_automation/
├── src/
│   ├── __init__.py
│   ├── settings.py              # 設定読み込み
│   ├── daily_report.py          # エントリーポイント
│   ├── nodes/
│   │   ├── __init__.py
│   │   ├── fetch_data.py
│   │   └── process_data.py
│   └── common/
│       └── __init__.py
├── tests/
│   ├── conftest.py
│   └── nodes/
│       └── test_fetch_data.py
├── config/
│   ├── development.yaml         # 環境別設定（統合版）
│   └── production.yaml
├── logs/
├── .env
├── .env.example
├── pyproject.toml
├── TUTORIAL.md                  # 段階的チュートリアル
└── README.md
```

---

## 3. CLIコマンド仕様（簡略化版）

### 3.1 railway init

**シグネチャ:**
```bash
railway init <project_name> [OPTIONS]
```

**オプション:**
- `--python-version TEXT`: Minimum Python version (default: "3.10")
- `--with-examples`: Include example entry points and nodes

**動作:**
1. プロジェクトディレクトリ作成
2. 基本構造生成 (src/, tests/, config/, logs/)
3. 設定ファイル生成 (.env.example, config/development.yaml)
4. TUTORIAL.md 生成（段階的学習ガイド）
5. Hello World サンプル生成（--with-examples指定時）

**出力例:**
```
✓ Created project: my_automation

Project structure:
  my_automation/
  ├── src/
  ├── tests/
  ├── config/
  ├── .env.example
  └── TUTORIAL.md

Next steps:
  1. cd my_automation
  2. cp .env.example .env
  3. Open TUTORIAL.md and follow the guide
  4. railway new entry hello --example
```

---

### 3.2 railway new

**シグネチャ:**
```bash
railway new <type> <name> [OPTIONS]

Types:
  entry    Create a new entry point
  node     Create a new node
```

**オプション:**
- `--example`: Generate with example code (not just template)
- `--force`: Overwrite if exists

**例:**
```bash
# エントリーポイント作成
railway new entry daily_report

# サンプルコード付きノード作成
railway new node fetch_data --example

# 既存ファイルを上書き
railway new node fetch_data --force
```

**出力例:**
```
✓ Created entry point: src/daily_report.py
✓ Entry point is ready to use

To run:
  uv run python -m src.daily_report

To add nodes:
  railway new node fetch_data
```

---

### 3.3 railway list

**シグネチャ:**
```bash
railway list [TYPE]

Types (optional):
  entries    List entry points only
  nodes      List nodes only
```

**出力例:**
```
Entry Points:
  • daily_report       Generate and send daily report
  • weekly_batch       Weekly batch processing

Nodes:
  • fetch_data         Fetch data from external API
  • process_data       Process and transform data
  • send_report        Send report via email/Slack

Statistics:
  2 entry points, 3 nodes, 5 tests
```

---

### 3.4 railway run

**シグネチャ:**
```bash
railway run <entry_name> [OPTIONS]
```

**説明:**
プロジェクト内のエントリーポイントを実行します。
`uv run python -m src.entry_name` よりシンプルで、一貫したCLI体験を提供します。

**動作:**
1. カレントディレクトリからプロジェクトルートを検出（pyproject.toml, .env等の存在確認）
2. `src/{entry_name}.py` を動的にインポート
3. `@entry_point`デコレータで定義されたCLI関数を実行
4. オプションはエントリーポイントに直接渡される

**オプション:**
- `--help`: エントリーポイント固有のヘルプを表示
- その他: エントリーポイントで定義されたオプション（typer経由）

**例:**
```bash
# 基本実行
railway run daily_report

# ヘルプ表示
railway run daily_report --help

# オプション付き実行
railway run daily_report --date 2024-01-01 --dry-run

# カレントディレクトリがプロジェクトルート外でもOK
cd /tmp
railway run --project /path/to/my_project daily_report
```

**出力例:**
```
[INFO] Project root: /home/user/my_automation
[INFO] Running entry point: daily_report
[INFO] [daily_report] Entry point started
[INFO] [fetch_data] Starting...
[INFO] [fetch_data] ✓ Completed
[INFO] [process_data] Starting...
[INFO] [process_data] ✓ Completed
[INFO] [send_report] Starting...
[INFO] [send_report] ✓ Completed
[SUCCESS] [daily_report] ✓ Completed successfully
```

**エラー時の出力例:**
```
[ERROR] [fetch_data] ✗ Failed: ConnectionError: Unable to connect to API
[ERROR] 詳細は logs/error.log を確認してください
[ERROR] ヒント: API_BASE_URLが正しく設定されているか確認してください
[INFO] Pipeline: Skipping remaining 2 steps
[ERROR] [daily_report] ✗ Failed
```

**実装方針:**
- `importlib`で動的にエントリーポイントをインポート
- プロジェクトルート検出ロジック: pyproject.toml, .env, READMEの存在確認
- `sys.path`にプロジェクトルートを追加してインポート可能にする

---

## 4. 設定ファイル仕様（統合版）

### 4.1 設定ファイルスキーマ

#### 4.1.1 app セクション

| フィールド | 型 | 必須 | デフォルト値 | 説明 |
|-----------|------|------|------------|------|
| name | string | Yes | - | アプリケーション名 |
| version | string | No | "0.1.0" | バージョン |

#### 4.1.2 api セクション

| フィールド | 型 | 必須 | デフォルト値 | 説明 |
|-----------|------|------|------------|------|
| base_url | string | Yes | - | APIベースURL |
| timeout | integer | No | 30 | タイムアウト（秒） |
| max_retries | integer | No | 3 | 最大リトライ回数 |

#### 4.1.3 database セクション

| フィールド | 型 | 必須 | デフォルト値 | 説明 |
|-----------|------|------|------------|------|
| host | string | No | "localhost" | データベースホスト |
| port | integer | No | 5432 | データベースポート |
| name | string | No | "db" | データベース名 |
| user | string | No | null | ユーザー名（.envで指定推奨） |
| password | string | No | null | パスワード（.envで指定推奨） |

#### 4.1.4 logging セクション

| フィールド | 型 | 必須 | デフォルト値 | 説明 |
|-----------|------|------|------------|------|
| level | string | No | "INFO" | ログレベル (DEBUG/INFO/WARNING/ERROR) |
| format | string | No | "{time:HH:mm:ss} \| {level} \| {message}" | ログフォーマット |
| handlers | array | No | [] | ログハンドラ配列（下記参照） |

**handlers配列の要素:**

| フィールド | 型 | 必須 | デフォルト値 | 説明 |
|-----------|------|------|------------|------|
| type | string | Yes | - | "file" または "console" |
| level | string | No | "INFO" | このハンドラのログレベル |
| path | string | No (fileの場合Yes) | - | ログファイルパス |
| rotation | string | No | null | ローテーション設定（例: "1 day"） |
| retention | string | No | null | 保持期間（例: "7 days"） |

#### 4.1.5 retry セクション

| フィールド | 型 | 必須 | デフォルト値 | 説明 |
|-----------|------|------|------------|------|
| default | object | Yes | - | デフォルトリトライ設定（下記参照） |
| nodes | object | No | {} | ノード別オーバーライド設定 |

**default（およびnodes内の各ノード設定）:**

| フィールド | 型 | 必須 | デフォルト値 | 説明 |
|-----------|------|------|------------|------|
| max_attempts | integer | No | 3 | 最大試行回数 |
| min_wait | integer | No | 2 | 最小待機時間（秒） |
| max_wait | integer | No | 10 | 最大待機時間（秒） |
| multiplier | integer | No | 1 | 指数バックオフの倍率 |

#### 4.1.6 notification セクション（オプション）

| フィールド | 型 | 必須 | デフォルト値 | 説明 |
|-----------|------|------|------------|------|
| email.enabled | boolean | No | false | メール通知の有効化 |
| email.to | string | No | "" | 送信先メールアドレス |
| slack.enabled | boolean | No | false | Slack通知の有効化 |
| slack.webhook | string | No | "" | Slack Webhook URL |

#### 4.1.7 features セクション（オプション）

| フィールド | 型 | 必須 | デフォルト値 | 説明 |
|-----------|------|------|------------|------|
| enable_cache | boolean | No | true | キャッシュ機能の有効化 |
| enable_metrics | boolean | No | false | メトリクス収集の有効化 |

---

### 4.2 統合設定ファイル例: config/development.yaml

```yaml
# Railway Framework Configuration - Development

# Application settings
app:
  name: my_automation
  version: "0.1.0"

# API settings
api:
  base_url: "https://api.example.com"
  timeout: 30
  max_retries: 3

# Database settings (optional)
database:
  host: "localhost"
  port: 5432
  name: "dev_db"

# Logging configuration
logging:
  level: DEBUG

  # Log format (simplified)
  format: "{time:HH:mm:ss} | {level} | {message}"

  # Output handlers
  handlers:
    - type: file
      path: logs/app.log
      rotation: "1 day"
      retention: "7 days"
      level: INFO

    - type: file
      path: logs/error.log
      rotation: "1 day"
      retention: "30 days"
      level: ERROR

    - type: console
      level: DEBUG

# Retry policy
retry:
  # Default retry settings for all nodes
  default:
    max_attempts: 3
    min_wait: 2        # seconds
    max_wait: 10       # seconds
    multiplier: 1

  # Node-specific overrides (optional)
  nodes:
    fetch_data:
      max_attempts: 5
      min_wait: 1
      max_wait: 5

    send_notification:
      max_attempts: 2
      min_wait: 3

# Notification settings (optional)
notification:
  email:
    enabled: false
    to: "dev@example.com"
  slack:
    enabled: false
    webhook: ""

# Feature flags (optional)
features:
  enable_cache: true
  enable_metrics: false
```

### 4.2 .env ファイル

```env
# Environment (development/staging/production)
RAILWAY_ENV=development

# Application
APP_NAME=my_automation

# API Credentials (example)
API_KEY=your_api_key_here
API_SECRET=your_api_secret_here

# Database Credentials (example)
DB_USER=postgres
DB_PASSWORD=secret

# Log Level Override (optional)
LOG_LEVEL=DEBUG
```

---

## 5. エントリーポイント実装仕様（シンプル化版）

### 5.1 レベル1: 超シンプル版（初心者向け）

```python
# src/hello.py
"""Hello World entry point."""
from railway import entry_point, node
from loguru import logger


@node
def say_hello(name: str) -> str:
    """Say hello to someone."""
    logger.info(f"Saying hello to {name}")
    return f"Hello, {name}!"


@entry_point
def main(name: str = "World"):
    """Simple hello world entry point."""
    message = say_hello(name)
    print(message)
    return message


if __name__ == "__main__":
    main()  # Typer appが自動的に起動され、CLIパースが行われる
```

**特徴:**
- デコレータがすべてのエラーハンドリングを処理
- ユーザーは通常のPython関数を書くだけ
- Result型は内部で自動的に処理される

**実行:**
```bash
uv run python -m src.hello
# Output: Hello, World!

uv run python -m src.hello --name Alice
# Output: Hello, Alice!
```

---

### 5.2 レベル2: パイプライン版（中級者向け）

```python
# src/daily_report.py
"""Daily report generation entry point."""
from railway import entry_point, node, pipeline
from loguru import logger
from datetime import datetime


@node
def fetch_data(date: str) -> dict:
    """Fetch data for the given date."""
    logger.info(f"Fetching data for {date}")
    # Simulate API call
    return {"date": date, "records": [1, 2, 3]}


@node
def process_data(data: dict) -> dict:
    """Process the fetched data."""
    logger.info(f"Processing {len(data['records'])} records")
    return {
        "date": data["date"],
        "summary": {
            "total": len(data["records"]),
            "sum": sum(data["records"])
        }
    }


@node
def send_report(data: dict) -> str:
    """Send the report."""
    logger.info("Sending report")
    return f"Report sent for {data['date']}"


@entry_point
def main(date: str = None, dry_run: bool = False):
    """Generate and send daily report.

    Args:
        date: Report date (YYYY-MM-DD), defaults to today
        dry_run: If True, don't actually send the report
    """
    if date is None:
        date = datetime.now().strftime("%Y-%m-%d")

    if dry_run:
        logger.warning("DRY RUN mode - no actual changes")

    # Pipeline execution
    result = pipeline(
        fetch_data(date),
        process_data,
        send_report if not dry_run else lambda x: x
    )

    return result


if __name__ == "__main__":
    main()
```

**特徴:**
- `pipeline()` 関数でパイプライン構築
- エラーは自動的に伝播
- dry-runモードのサポート

**実行:**
```bash
uv run python -m src.daily_report
uv run python -m src.daily_report --date 2024-01-01
uv run python -m src.daily_report --dry-run
```

---

### 5.3 レベル3: 明示的Result型版（上級者向け）

```python
# src/advanced_pipeline.py
"""Advanced pipeline with explicit Result handling."""
from railway import entry_point, node
from returns.result import Result, Success, Failure
from returns.pipeline import flow
from loguru import logger


@node
def fetch_data() -> Result[dict, Exception]:
    """Fetch data with explicit Result type."""
    try:
        # API call
        data = {"records": [1, 2, 3]}
        return Success(data)
    except Exception as e:
        return Failure(e)


@node
def process_data(data: dict) -> Result[dict, Exception]:
    """Process data with explicit Result type."""
    if not data.get("records"):
        return Failure(ValueError("No records to process"))
    return Success({"processed": len(data["records"])})


@entry_point(handle_result=False)  # 明示的Result型を返す
def main() -> Result[str, Exception]:
    """Advanced entry point with explicit Result handling."""
    result = flow(
        fetch_data(),
        process_data,
    )

    # 明示的なエラーハンドリング
    return result.map(lambda x: f"Processed {x['processed']} records")


if __name__ == "__main__":
    main()
```

**特徴:**
- Result型を明示的に扱う
- `flow()` による関数型パイプライン
- 上級者向けの細かい制御

---

## 6. ノード実装仕様（シンプル化版）

### 6.1 基本ノード

```python
# src/nodes/fetch_data.py
"""Fetch data from external API."""
from railway import node
from loguru import logger

from src.settings import settings


@node(retry=True)  # 自動リトライ有効
def fetch_data(url: str = None) -> dict:
    """
    Fetch data from external API.

    Args:
        url: API endpoint URL (optional, uses settings if not provided)

    Returns:
        dict: Fetched data

    Raises:
        Exception: Any exception will be automatically caught and converted to Failure
    """
    url = url or settings.api.base_url + "/data"
    logger.info(f"Fetching data from {url}")

    # Your implementation here
    # If an exception is raised, @node decorator will:
    #   1. Retry according to retry policy
    #   2. Convert to Failure(exception)
    #   3. Log the error

    import requests
    response = requests.get(url, timeout=settings.api.timeout)
    response.raise_for_status()

    return response.json()
```

**特徴:**
- `@node` デコレータが自動的にエラーをキャッチ
- リトライ設定は設定ファイルまたはデコレータで指定可能
- 通常の関数として実装するだけ

---

### 6.2 カスタムリトライ設定

```python
# src/nodes/critical_operation.py
"""Critical operation with custom retry settings."""
from railway import node, Retry


@node(
    retry=Retry(
        max_attempts=5,
        min_wait=1,
        max_wait=10,
        exponential_base=2
    )
)
def critical_operation(data: dict) -> dict:
    """Critical operation with custom retry policy."""
    # Implementation
    return data
```

---

### 6.3 条件付きノード

```python
# src/nodes/conditional_node.py
"""Conditional node execution."""
from railway import node
from typing import Optional


@node
def process_if_valid(data: dict) -> Optional[dict]:
    """Process data only if valid."""
    if not data.get("is_valid"):
        # Return None to skip (treated as Success(None))
        return None

    # Process data
    return {"processed": True}
```

---

## 7. settings.py 実装仕様（簡略化版）

```python
# src/settings.py
"""Application settings."""
from pathlib import Path
from typing import Any, Dict, List
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
import yaml


# 個別設定モデル
class APISettings(BaseModel):
    """API configuration."""
    base_url: str
    timeout: int = 30
    max_retries: int = 3


class DatabaseSettings(BaseModel):
    """Database configuration."""
    host: str = "localhost"
    port: int = 5432
    name: str = "db"
    user: str | None = None
    password: str | None = None


class RetryNodeSettings(BaseModel):
    """Per-node retry settings."""
    max_attempts: int = 3
    min_wait: int = 2
    max_wait: int = 10
    multiplier: int = 1


class RetrySettings(BaseModel):
    """Retry policy configuration."""
    default: RetryNodeSettings
    nodes: Dict[str, RetryNodeSettings] = Field(default_factory=dict)


class LoggingHandlerSettings(BaseModel):
    """Logging handler configuration."""
    type: str  # file, console
    level: str = "INFO"
    path: str | None = None
    rotation: str | None = None
    retention: str | None = None


class LoggingSettings(BaseModel):
    """Logging configuration."""
    level: str = "INFO"
    format: str = "{time:HH:mm:ss} | {level} | {message}"
    handlers: List[LoggingHandlerSettings] = Field(default_factory=list)


# メイン設定クラス
class Settings(BaseSettings):
    """Application settings."""

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
        extra="allow"
    )

    # Environment variables
    railway_env: str = "development"
    app_name: str = "railway_app"
    log_level: str | None = None  # Override from .env

    # Configuration from YAML (loaded in __init__)
    api: APISettings | None = None
    database: DatabaseSettings | None = None
    retry: RetrySettings = Field(
        default_factory=lambda: RetrySettings(
            default=RetryNodeSettings()
        )
    )
    logging: LoggingSettings = Field(
        default_factory=lambda: LoggingSettings()
    )

    def __init__(self, **kwargs):
        """Initialize settings and load YAML config."""
        super().__init__(**kwargs)
        self._load_config()
        self._apply_overrides()

    def _load_config(self) -> None:
        """Load configuration from YAML file."""
        config_file = Path(__file__).parent.parent / "config" / f"{self.railway_env}.yaml"

        if not config_file.exists():
            return

        with open(config_file, "r", encoding="utf-8") as f:
            config = yaml.safe_load(f) or {}

        # Load each section
        if "api" in config:
            self.api = APISettings(**config["api"])

        if "database" in config:
            self.database = DatabaseSettings(**config["database"])

        if "retry" in config:
            retry_config = config["retry"]
            default_retry = RetryNodeSettings(**retry_config.get("default", {}))
            nodes_retry = {
                name: RetryNodeSettings(**settings)
                for name, settings in retry_config.get("nodes", {}).items()
            }
            self.retry = RetrySettings(default=default_retry, nodes=nodes_retry)

        if "logging" in config:
            log_config = config["logging"]
            handlers = [
                LoggingHandlerSettings(**h)
                for h in log_config.get("handlers", [])
            ]
            self.logging = LoggingSettings(
                level=log_config.get("level", "INFO"),
                format=log_config.get("format", "{time:HH:mm:ss} | {level} | {message}"),
                handlers=handlers
            )

    def _apply_overrides(self) -> None:
        """Apply environment variable overrides."""
        if self.log_level:
            self.logging.level = self.log_level

    def get_retry_settings(self, node_name: str) -> RetryNodeSettings:
        """Get retry settings for a specific node."""
        return self.retry.nodes.get(node_name, self.retry.default)


# Global settings instance (lazy initialization)
_settings_instance: Settings | None = None


def get_settings() -> Settings:
    """
    Get or create the global settings singleton.

    Returns:
        Settings: The global settings instance

    Example:
        from src.settings import get_settings

        settings = get_settings()
        print(settings.api.base_url)
    """
    global _settings_instance
    if _settings_instance is None:
        _settings_instance = Settings()
    return _settings_instance


def reset_settings() -> None:
    """
    Reset the global settings instance (for testing).

    This function is primarily used in tests to reset settings
    between test cases or to use different configurations.

    Example:
        from src.settings import reset_settings

        def test_with_custom_config():
            # Reset global settings
            reset_settings()

            # Set custom environment variable
            os.environ['RAILWAY_ENV'] = 'test'

            # Now get_settings() will load test config
            settings = get_settings()
            assert settings.railway_env == 'test'
    """
    global _settings_instance
    _settings_instance = None


class _SettingsProxy:
    """
    Lazy settings proxy.

    Settings are not loaded until first attribute access.
    This allows:
    1. Importing settings without triggering initialization
    2. Setting environment variables before first use
    3. Easier testing with reset_settings()

    Example:
        # Settings are NOT loaded here
        from src.settings import settings

        # Set environment before first access
        os.environ['RAILWAY_ENV'] = 'production'

        # Settings ARE loaded here (first attribute access)
        url = settings.api.base_url
    """

    def __getattr__(self, name: str) -> Any:
        """Delegate attribute access to the actual settings instance."""
        return getattr(get_settings(), name)

    def __repr__(self) -> str:
        """Return string representation."""
        if _settings_instance is None:
            return "<SettingsProxy: not initialized>"
        return f"<SettingsProxy: {_settings_instance}>"


# Lazy proxy for backward compatibility
# Settings are loaded on first attribute access, not on import
# This allows both `from src.settings import settings` and `from src.settings import get_settings`
settings = _SettingsProxy()


# Register settings provider for @node decorator (framework integration)
try:
    from railway.core.config import register_settings_provider
    register_settings_provider(get_settings)
except ImportError:
    # Railway framework not installed, skip registration
    pass
```

**使用例:**
```python
from src.settings import settings

# API settings
print(settings.api.base_url)
print(settings.api.timeout)

# Retry settings
retry_config = settings.get_retry_settings("fetch_data")
print(retry_config.max_attempts)

# Logging
print(settings.logging.level)
```

---

## 8. テストテンプレート仕様（実用的版）

### 8.1 ノードテスト

```python
# tests/nodes/test_fetch_data.py
"""Tests for fetch_data node."""
import pytest
from unittest.mock import patch, MagicMock

from src.nodes.fetch_data import fetch_data


class TestFetchData:
    """Test suite for fetch_data node."""

    def test_success(self):
        """Normal case: successfully fetch data."""
        # Mock the API call
        with patch('requests.get') as mock_get:
            mock_response = MagicMock()
            mock_response.json.return_value = {"records": [1, 2, 3]}
            mock_response.raise_for_status = MagicMock()
            mock_get.return_value = mock_response

            # Execute
            result = fetch_data()

            # Verify
            assert result == {"records": [1, 2, 3]}
            mock_get.assert_called_once()

    def test_api_error(self):
        """Error case: API returns error."""
        with patch('requests.get') as mock_get:
            mock_get.side_effect = Exception("API Error")

            # Execute - should raise exception (will be caught by @node in real usage)
            with pytest.raises(Exception) as exc_info:
                fetch_data()

            assert "API Error" in str(exc_info.value)

    def test_with_custom_url(self):
        """Test with custom URL parameter."""
        custom_url = "https://custom.api.com/data"

        with patch('requests.get') as mock_get:
            mock_response = MagicMock()
            mock_response.json.return_value = {"data": "custom"}
            mock_response.raise_for_status = MagicMock()
            mock_get.return_value = mock_response

            result = fetch_data(url=custom_url)

            assert result == {"data": "custom"}
            mock_get.assert_called_with(custom_url, timeout=30)


@pytest.fixture
def sample_api_response():
    """Sample API response for testing."""
    return {
        "records": [
            {"id": 1, "value": 100},
            {"id": 2, "value": 200},
        ],
        "total": 2
    }


def test_with_fixture(sample_api_response):
    """Test using fixture."""
    assert sample_api_response["total"] == 2
    assert len(sample_api_response["records"]) == 2
```

---

## 9. TUTORIAL.md 仕様（段階的学習ガイド）

プロジェクト初期化時に自動生成される `TUTORIAL.md` の内容:

````markdown
# Railway Framework Tutorial

Welcome to Railway Framework! This tutorial will guide you from zero to hero in about 30 minutes.

## Prerequisites

- Python 3.10+
- uv installed (`curl -LsSf https://astral.sh/uv/install.sh | sh`)

---

## Step 1: Hello World (5 minutes)

Let's create your first Railway entry point.

### 1.1 Create a simple entry point

```bash
railway new entry hello --example
```

This creates `src/hello.py`:

```python
from railway import entry_point, node

@node
def greet(name: str) -> str:
    return f"Hello, {name}!"

@entry_point
def main(name: str = "World"):
    message = greet(name)
    print(message)
    return message
```

### 1.2 Run it

```bash
uv run python -m src.hello
# Output: Hello, World!

uv run python -m src.hello --name Alice
# Output: Hello, Alice!
```

**What you learned:**
- `@node`: Marks a function as a reusable processing unit
- `@entry_point`: Marks a function as the main entry point
- CLI arguments are automatically parsed from function parameters

---

### 1.3 Choosing how to run

Railway Framework provides two ways to run entry points:

**Method 1: `railway run` command (Recommended)**
```bash
railway run hello --name Alice
```

Advantages:
- Shorter command
- Auto-detects project root
- Consistent CLI experience

**Method 2: Direct Python module execution**
```bash
uv run python -m src.hello --name Alice
```

Advantages:
- Works without Railway installed
- Easier debugger integration
- Standard Python approach

**Which should I use?**
- For daily development: Use `railway run` (recommended)
- For CI/CD or environments without Railway: Use `uv run python -m`
- Both produce the same results

---

## Step 2: Error Handling (10 minutes)

Railway handles errors automatically. Let's see how.

### 2.1 Create a node that can fail

```bash
railway new node divide
```

Edit `src/nodes/divide.py`:

```python
from railway import node

@node
def divide(a: float, b: float) -> float:
    """Divide two numbers."""
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
```

### 2.2 Use it in an entry point

Create `src/calculator.py`:

```python
from railway import entry_point
from src.nodes.divide import divide

@entry_point
def main(a: float, b: float):
    result = divide(a, b)
    print(f"{a} / {b} = {result}")
    return result
```

### 2.3 Test error handling

```bash
uv run python -m src.calculator --a 10 --b 2
# Output: 10.0 / 2.0 = 5.0

uv run python -m src.calculator --a 10 --b 0
# Output: [ERROR] Cannot divide by zero
# Exit code: 1
```

**What you learned:**
- Errors are automatically caught and logged
- Exit code is set correctly (1 for failure)
- You don't need try-except blocks!

---

## Step 3: Pipeline Processing (10 minutes)

Let's build a multi-step pipeline.

### 3.1 Create nodes

```bash
railway new node fetch_data --example
railway new node process_data --example
railway new node save_result --example
```

### 3.2 Create a pipeline entry point

```python
from railway import entry_point, pipeline
from src.nodes.fetch_data import fetch_data
from src.nodes.process_data import process_data
from src.nodes.save_result import save_result

@entry_point
def main(source: str):
    """Data processing pipeline."""
    result = pipeline(
        fetch_data(source),
        process_data,
        save_result
    )
    return result
```

### 3.3 Run the pipeline

```bash
uv run python -m src.data_pipeline --source api
```

**What you learned:**
- `pipeline()`: Chain multiple nodes together
- If any node fails, subsequent nodes are skipped
- Data flows automatically from one node to the next

---

## Step 4: Configuration (15 minutes)

### 4.1 Edit config file

Edit `config/development.yaml`:

```yaml
api:
  base_url: "https://api.example.com"
  timeout: 30

retry:
  default:
    max_attempts: 3
  nodes:
    fetch_data:
      max_attempts: 5
```

### 4.2 Use settings in your code

```python
from railway import node
from src.settings import settings

@node
def fetch_data() -> dict:
    url = settings.api.base_url + "/data"
    timeout = settings.api.timeout
    # Use url and timeout...
```

### 4.3 Environment-specific config

Create `config/production.yaml` with production settings.

Switch environment:
```bash
# Edit .env
RAILWAY_ENV=production
```

**What you learned:**
- Settings are centralized in `config/{env}.yaml`
- Access settings via `settings.api.base_url`, etc.
- Easy to switch between environments

---

## Step 5: Testing (20 minutes)

### 5.1 Run existing tests

```bash
pytest tests/
```

### 5.2 Write your own test

When you created nodes with `railway new node`, test files were created automatically.

Edit `tests/nodes/test_divide.py`:

```python
import pytest
from src.nodes.divide import divide

def test_divide_success():
    result = divide(10, 2)
    assert result == 5.0

def test_divide_by_zero():
    with pytest.raises(ValueError):
        divide(10, 0)
```

### 5.3 Run with coverage

```bash
pytest --cov=src --cov-report=html
open htmlcov/index.html
```

**What you learned:**
- Tests are automatically generated
- Use pytest for testing
- Mock external dependencies in tests

---

## Step 6: Troubleshooting (10 minutes)

When things go wrong, here's how to debug.

### 6.1 Common Errors

#### Error: "Module not found"
```
ModuleNotFoundError: No module named 'src.nodes.fetch_data'
```

**Solution:**
- Make sure you're running from the project root
- Check that the file exists at the correct path
- Use `railway run` instead of `python -m`

---

#### Error: "Configuration error"
```
pydantic_core._pydantic_core.ValidationError: 1 validation error for APISettings
base_url
  Field required [type=missing, input_value={}, input_type=dict]
```

**Solution:**
- Check `config/development.yaml` has the required field `base_url` under `api:` section
- Make sure `.env` has `RAILWAY_ENV=development`
- Verify the config file is valid YAML (indentation matters!)
- Check the settings schema in the documentation for required fields

---

#### Error: "Connection refused"
```
ConnectionError: Unable to connect to API
```

**Solution:**
- Check your internet connection
- Verify `API_BASE_URL` in config
- Use `--dry-run` to test without API calls
- Check logs: `tail -f logs/error.log`

---

### 6.2 Reading Logs

Logs are in the `logs/` directory:
- `logs/app.log`: All logs (INFO and above)
- `logs/error.log`: Only errors

```bash
# Watch logs in real-time
tail -f logs/app.log

# Search for errors
grep ERROR logs/app.log

# View last 50 lines
tail -50 logs/error.log
```

---

### 6.3 Debug Mode

Enable verbose logging:

```bash
# In .env file
LOG_LEVEL=DEBUG

# Or via command line
LOG_LEVEL=DEBUG railway run my_entry
```

---

### 6.4 Testing Individual Nodes

Test nodes in isolation:

```bash
# Run node tests
pytest tests/nodes/test_fetch_data.py -v

# With print statements
pytest tests/nodes/test_fetch_data.py -v -s
```

---

## Next Steps

### Advanced Features

1. **Retry Configuration**: Customize retry behavior per node
2. **Logging**: Configure log levels and outputs
3. **Type Safety**: Add Pydantic models for type-safe data flow
4. **Async Support**: Use `async def` for async nodes

### Best Practices

1. Keep nodes small and focused
2. Use type hints everywhere
3. Write tests for all nodes
4. Use dry-run mode for testing (`--dry-run`)
5. Configure logging appropriately for each environment
6. Read logs regularly to catch issues early
7. Start with simple pipelines, then add complexity

### Getting Help

- Documentation: https://railway-framework.readthedocs.io
- GitHub: https://github.com/your-org/railway_py
- Issues: https://github.com/your-org/railway_py/issues

---

**Congratulations!** You've completed the Railway Framework tutorial. Start building your automation workflows!
````

---

## 10. pyproject.toml 仕様

```toml
[project]
name = "{project_name}"
version = "0.1.0"
description = "Railway framework automation project"
readme = "README.md"
requires-python = ">={python_version}"
dependencies = [
    "railway-framework>=0.1.0",  # フレームワーク本体
    "returns>=0.22.0",
    "tenacity>=8.2.0",
    "pydantic>=2.5.0",
    "pydantic-settings>=2.1.0",
    "typer>=0.9.0",
    "loguru>=0.7.0",
    "python-dotenv>=1.0.0",
    "PyYAML>=6.0",
]

[project.optional-dependencies]
dev = [
    "ruff>=0.1.0",
    "mypy>=1.7.0",
    "pytest>=7.4.0",
    "pytest-cov>=4.1.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]

[tool.mypy]
python_version = "{python_version}"
strict = true

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--strict-markers --tb=short -v"
```

---

## 11. 変更点まとめ

### 11.1 削除した機能

1. **graph.yaml 機能**: 複雑すぎるため Phase 2 に延期
2. **分散した設定ファイル**: app.yaml, logging.yaml, retry.yaml を統合
3. **複雑なCLIコマンド**: graph validate, graph visualize, check-cycles を削除
4. **非同期専用テンプレート**: 統一的なAPIで対応

### 11.2 簡略化した機能

1. **CLIコマンド**: 10個 → 3個 (init, new, list)
2. **設定ファイル**: 3個 → 1個 (config.yaml)
3. **エントリーポイント**: boilerplate を大幅削減
4. **ノードAPI**: デコレータベースでシンプルに

### 11.3 追加した機能

1. **TUTORIAL.md**: 段階的学習ガイド
2. **--example オプション**: サンプルコード付き生成
3. **レベル別API**: 初心者から上級者まで対応
4. **統合設定ファイル**: 1つのYAMLで完結

---

## 12. 今後の拡張（Phase 2以降）

### Phase 2
- graph.yaml によるグラフベース実行
- WebUI でのグラフ可視化
- 詳細なメトリクス収集

### Phase 3
- 分散実行サポート (Celery/Dask)
- クラウドサービス統合 (AWS/GCP/Azure)

---

## 13. フレームワークコア実装詳細（重要）

このセクションでは、フレームワークの中核となるデコレータとパイプライン関数の実装詳細を定義します。

### 13.1 @node デコレータ実装

#### 13.1.1 設定プロバイダーレジストリ

フレームワークとユーザーコードを分離するため、設定プロバイダーレジストリを使用します。

```python
# railway/core/config.py
"""
Settings provider registry for framework-user code separation.

This module allows the @node decorator to access user settings
without directly importing user code.
"""

from typing import Callable, Any

# Global settings provider
_settings_provider: Callable[[], Any] | None = None


def register_settings_provider(provider: Callable[[], Any]) -> None:
    """
    Register a settings provider function.

    The provider should return a settings object with get_retry_settings() method.

    Args:
        provider: A callable that returns the settings object

    Example:
        from railway.core.config import register_settings_provider
        from src.settings import get_settings

        register_settings_provider(get_settings)
    """
    global _settings_provider
    _settings_provider = provider


def get_settings_provider() -> Callable[[], Any] | None:
    """Get the registered settings provider."""
    return _settings_provider


class DefaultRetrySettings:
    """Default retry settings when no provider is registered."""
    max_attempts: int = 3
    min_wait: int = 2
    max_wait: int = 10
    multiplier: int = 1


def get_retry_config(node_name: str) -> Any:
    """
    Get retry configuration for a specific node.

    If no settings provider is registered, returns default settings.

    Args:
        node_name: Name of the node to get settings for

    Returns:
        Retry configuration object with max_attempts, min_wait, max_wait, multiplier
    """
    if _settings_provider is None:
        return DefaultRetrySettings()

    try:
        settings = _settings_provider()
        return settings.get_retry_settings(node_name)
    except Exception:
        return DefaultRetrySettings()
```

#### 13.1.2 @nodeデコレータ本体

```python
# railway/core/decorators.py

from functools import wraps
from typing import Callable, TypeVar, ParamSpec, Any
from returns.result import Result, Success, Failure, safe
from tenacity import retry, stop_after_attempt, wait_exponential, RetryError
from loguru import logger
import inspect

P = ParamSpec('P')
T = TypeVar('T')


class Retry:
    """Retry configuration."""

    def __init__(
        self,
        max_attempts: int = 3,
        min_wait: float = 2.0,
        max_wait: float = 10.0,
        exponential_base: int = 2,
    ):
        self.max_attempts = max_attempts
        self.min_wait = min_wait
        self.max_wait = max_wait
        self.exponential_base = exponential_base


def node(
    func: Callable[P, T] | None = None,
    *,
    retry: bool | Retry = True,
    log_input: bool = False,
    log_output: bool = False,
    name: str | None = None,
) -> Callable[P, T]:
    """
    Node decorator that provides:
    1. Automatic exception handling
    2. Optional retry with exponential backoff
    3. Structured logging
    4. Metadata storage

    Args:
        func: Function to decorate
        retry: Enable retry (bool) or provide Retry config
        log_input: Log input parameters (caution: may log sensitive data)
        log_output: Log output data (caution: may log sensitive data)
        name: Override node name (default: function name)

    Returns:
        Decorated function with automatic error handling

    Example:
        @node
        def fetch_data() -> dict:
            return api.get("/data")

        @node(retry=Retry(max_attempts=5, min_wait=1))
        def critical_api_call() -> dict:
            return api.post("/critical")
    """

    def decorator(f: Callable[P, T]) -> Callable[P, T]:
        node_name = name or f.__name__

        # Prepare retry decorator if needed
        if retry is True:
            # Load retry settings from config via provider registry
            from railway.core.config import get_retry_config
            retry_config = get_retry_config(node_name)
            max_attempts = retry_config.max_attempts

            # Define retry logging callback
            def log_retry(retry_state):
                attempt = retry_state.attempt_number
                logger.warning(f"[{node_name}] リトライ中... (試行 {attempt}/{max_attempts})")

            retry_decorator = retry(
                stop=stop_after_attempt(max_attempts),
                wait=wait_exponential(
                    multiplier=retry_config.multiplier,
                    min=retry_config.min_wait,
                    max=retry_config.max_wait,
                ),
                reraise=True,  # Re-raise for @safe to catch
                before=log_retry,
            )
        elif isinstance(retry, Retry):
            retry_decorator = retry(
                stop=stop_after_attempt(retry.max_attempts),
                wait=wait_exponential(
                    multiplier=retry.exponential_base,
                    min=retry.min_wait,
                    max=retry.max_wait,
                ),
                reraise=True,
                before=lambda _: logger.debug(f"[{node_name}] Retrying..."),
            )
        else:
            retry_decorator = lambda x: x  # No retry

        # Create wrapper
        @wraps(f)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            # Log input
            if log_input:
                logger.debug(f"[{node_name}] Input: args={args}, kwargs={kwargs}")

            logger.info(f"[{node_name}] Starting...")

            try:
                # Apply retry decorator dynamically
                retryable_func = retry_decorator(f)
                result = retryable_func(*args, **kwargs)

                # Log output
                if log_output:
                    logger.debug(f"[{node_name}] Output: {result}")

                logger.info(f"[{node_name}] ✓ Completed")
                return result

            except RetryError as e:
                # Retry exhausted
                original_exception = e.last_attempt.exception()
                attempt_count = e.last_attempt.attempt_number

                # Enhanced error message with hints
                logger.error(
                    f"[{node_name}] ✗ Failed after {attempt_count} attempts: "
                    f"{type(original_exception).__name__}: {original_exception}"
                )
                logger.error(f"詳細は logs/error.log を確認してください")

                # Add hints for common errors
                if isinstance(original_exception, ConnectionError):
                    logger.error("ヒント: ネットワーク接続を確認してください。APIエンドポイントが正しいか確認してください。")
                elif isinstance(original_exception, TimeoutError):
                    logger.error("ヒント: タイムアウト値を増やすか、APIサーバーの状態を確認してください。")
                elif isinstance(original_exception, ValueError):
                    logger.error("ヒント: 入力データの形式や値を確認してください。")

                raise original_exception

            except Exception as e:
                # Enhanced error message
                logger.error(f"[{node_name}] ✗ Failed: {type(e).__name__}: {e}")
                logger.error(f"詳細は logs/error.log を確認してください")

                # Add hints for common errors
                if isinstance(e, ConnectionError):
                    logger.error("ヒント: ネットワーク接続を確認してください。APIエンドポイントが正しいか確認してください。")
                elif isinstance(e, TimeoutError):
                    logger.error("ヒント: タイムアウト値を増やすか、APIサーバーの状態を確認してください。")
                elif isinstance(e, ValueError):
                    logger.error("ヒント: 入力データの形式や値を確認してください。")
                elif "API_KEY" in str(e).upper() or "API_SECRET" in str(e).upper():
                    logger.error("ヒント: .envファイルでAPI認証情報が正しく設定されているか確認してください。")

                raise

        # Store metadata
        wrapper._is_railway_node = True
        wrapper._node_name = node_name
        wrapper._original_func = f

        return wrapper

    # Handle decorator usage with and without parentheses
    if func is None:
        return decorator
    return decorator(func)
```

---

### 13.2 pipeline() 関数実装

```python
# railway/core/pipeline.py

from typing import TypeVar, Callable, Any, get_type_hints
from returns.result import Result, Success, Failure
from loguru import logger
import inspect

T = TypeVar('T')


def pipeline(
    initial: Any,
    *steps: Callable[[Any], Any],
    type_check: bool = True,
    strict: bool = False,
) -> Any:
    """
    Execute a pipeline of processing steps.

    Features:
    1. Sequential execution of steps
    2. Automatic error propagation (skip remaining steps on error)
    3. Runtime type checking between steps (enabled by default)
    4. Detailed logging of execution flow

    IMPORTANT: Understanding the 'initial' argument
    -----------------------------------------------
    The 'initial' argument is the STARTING VALUE for the pipeline.
    It is NOT a function, but a value (or the result of a function call).

    Example:
        # Correct: fetch_data() is called FIRST, then its result enters pipeline
        result = pipeline(
            fetch_data(date),   # ← This is evaluated IMMEDIATELY before pipeline starts
            process_data,       # ← This receives the result of fetch_data()
            save_data,          # ← This receives the result of process_data()
        )

        # Equivalent to:
        step1_result = fetch_data(date)  # Executed first, OUTSIDE pipeline
        step2_result = process_data(step1_result)  # First step IN pipeline
        step3_result = save_data(step2_result)     # Second step IN pipeline

    Error handling note:
        If fetch_data() raises an exception, the pipeline is never entered.
        The exception propagates immediately. For error handling within the
        pipeline, wrap the function with @node decorator.

    Args:
        initial: Initial value or Result[T, E]
        *steps: Processing functions to apply sequentially
        type_check: Enable runtime type checking (default: True, recommended)
        strict: Require type hints on all steps (default: False)

    Returns:
        Final result (unwrapped if successful)

    Raises:
        Exception: If any step fails
        TypeError: If an async function is passed (use await directly or wait for Phase 2)

    Example:
        result = pipeline(
            fetch_data(),
            validate_data,
            transform_data,
            save_data,
        )

    Internal behavior:
        - If a step raises an exception, remaining steps are skipped
        - Each step receives the output of the previous step
        - Type annotations are optionally validated at runtime
        - Async functions are rejected with helpful error message
    """
    import asyncio

    # Check for async functions (not supported in Phase 1)
    for step in steps:
        if asyncio.iscoroutinefunction(step):
            step_name = getattr(step, '_node_name', step.__name__)
            raise TypeError(
                f"Async function '{step_name}' cannot be used in pipeline(). "
                "Phase 1 supports only synchronous nodes in pipeline(). "
                "Options:\n"
                f"  1. Use 'await {step_name}(value)' directly\n"
                "  2. Wait for pipeline_async() in Phase 2\n"
                "  3. Convert to synchronous function if possible"
            )

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

    current_value = initial
    current_step = 0

    try:
        for i, step in enumerate(steps, 1):
            current_step = i
            step_name = getattr(step, '_node_name', step.__name__)

            # Type checking (if enabled)
            if type_check:
                _validate_step_input_type(step, current_value, step_name)

            # Execute step
            logger.debug(f"Pipeline step {i}/{len(steps)}: {step_name}")

            try:
                result = step(current_value)
                current_value = result
                logger.debug(f"Pipeline step {i}/{len(steps)}: ✓ Success")

            except Exception as e:
                logger.error(
                    f"Pipeline step {i}/{len(steps)} ({step_name}): ✗ Failed with {type(e).__name__}: {e}"
                )
                logger.info(f"Pipeline: Skipping remaining {len(steps) - i} steps")
                raise

        logger.debug(f"Pipeline completed successfully")
        return current_value

    except Exception as e:
        logger.error(f"Pipeline failed at step {current_step}/{len(steps)}")
        raise


def _validate_step_input_type(step: Callable, value: Any, step_name: str) -> None:
    """Validate that the input value matches the expected type annotation."""
    try:
        hints = get_type_hints(step)
        # Get first parameter (usually 'data' or similar)
        params = list(inspect.signature(step).parameters.keys())
        if not params:
            return

        first_param = params[0]
        expected_type = hints.get(first_param)

        if expected_type and not isinstance(value, expected_type):
            raise TypeError(
                f"Type mismatch in pipeline step '{step_name}': "
                f"expected {expected_type}, got {type(value)}"
            )
    except Exception:
        # Skip validation if type hints are not available
        pass
```

---

### 13.3 @entry_point デコレータ実装

```python
# railway/core/decorators.py (continued)

import sys
import typer
from typing import Callable, TypeVar, ParamSpec, get_type_hints
from returns.result import Result, Success, Failure
from loguru import logger

P = ParamSpec('P')
T = TypeVar('T')


def entry_point(
    func: Callable[P, T] | None = None,
    *,
    handle_result: bool = True,
) -> Callable[P, None]:
    """
    Entry point decorator that provides:
    1. Automatic CLI argument parsing via Typer
    2. Error handling and logging
    3. Exit code management (0 for success, 1 for failure)
    4. Result type unwrapping

    Args:
        func: Function to decorate
        handle_result: Automatically handle Result types (default: True)

    Returns:
        Decorated function with CLI integration

    Example:
        @entry_point
        def main(name: str = "World", verbose: bool = False):
            print(f"Hello, {name}!")
            return "Success"

    CLI usage:
        python -m src.entry --name Alice --verbose
    """

    def decorator(f: Callable[P, T]) -> Callable[P, None]:
        entry_name = f.__name__

        # Create Typer app for this entry point
        app = typer.Typer(
            help=f.__doc__ or f"Execute {entry_name} entry point",
            add_completion=False,
        )

        @app.command()
        @wraps(f)
        def cli_wrapper(**kwargs) -> None:
            """CLI wrapper for the entry point."""
            logger.info(f"[{entry_name}] Entry point started")
            logger.debug(f"[{entry_name}] Arguments: {kwargs}")

            try:
                # Execute the main function
                result = f(**kwargs)

                # Handle result based on type
                if handle_result:
                    if isinstance(result, Success):
                        value = result.unwrap()
                        logger.success(f"[{entry_name}] ✓ Completed successfully: {value}")
                        sys.exit(0)
                    elif isinstance(result, Failure):
                        error = result.failure()
                        logger.error(f"[{entry_name}] ✗ Failed: {error}")
                        sys.exit(1)
                    else:
                        # Plain value
                        logger.success(f"[{entry_name}] ✓ Completed successfully")
                        sys.exit(0)
                else:
                    # User handles Result explicitly
                    logger.success(f"[{entry_name}] ✓ Completed")
                    sys.exit(0)

            except KeyboardInterrupt:
                logger.warning(f"[{entry_name}] Interrupted by user")
                sys.exit(130)

            except Exception as e:
                logger.exception(f"[{entry_name}] ✗ Unhandled exception")
                sys.exit(1)

        # Store Typer app and metadata for programmatic access
        cli_wrapper._typer_app = app
        cli_wrapper._original_func = f
        cli_wrapper._is_railway_entry_point = True

        # The wrapper itself becomes the callable that Typer will invoke
        return cli_wrapper

    if func is None:
        return decorator
    return decorator(func)
```

---

### 13.4 型安全なノード定義（Pydantic モデル使用）

```python
# Example: Type-safe nodes with Pydantic models

from pydantic import BaseModel, Field, validator
from railway import node
from datetime import datetime


# Input/Output models
class FetchDataInput(BaseModel):
    """Input model for fetch_data node."""
    date: str
    source: str = "api"

    @validator('date')
    def validate_date_format(cls, v):
        """Validate date format."""
        try:
            datetime.strptime(v, "%Y-%m-%d")
            return v
        except ValueError:
            raise ValueError("Date must be in YYYY-MM-DD format")


class FetchDataOutput(BaseModel):
    """Output model for fetch_data node."""
    records: list[dict]
    count: int = Field(ge=0)
    fetched_at: datetime


class ProcessDataInput(BaseModel):
    """Input model for process_data node."""
    records: list[dict]
    count: int


class ProcessDataOutput(BaseModel):
    """Output model for process_data node."""
    processed_records: list[dict]
    summary: dict


# Type-safe nodes
@node
def fetch_data(input: FetchDataInput) -> FetchDataOutput:
    """
    Fetch data with type-safe input/output.

    Pydantic automatically validates:
    - Input data structure
    - Field types
    - Custom validators
    """
    # Fetch data from API
    records = api.get(f"/data?date={input.date}&source={input.source}")

    return FetchDataOutput(
        records=records,
        count=len(records),
        fetched_at=datetime.now(),
    )


@node
def process_data(input: ProcessDataInput) -> ProcessDataOutput:
    """Process data with type-safe input/output."""
    processed = [
        {**record, "processed": True}
        for record in input.records
    ]

    return ProcessDataOutput(
        processed_records=processed,
        summary={"total": input.count, "processed": len(processed)},
    )


# Usage with type-safe pipeline
@entry_point
def main(date: str, source: str = "api"):
    """Type-safe pipeline execution."""
    input_data = FetchDataInput(date=date, source=source)

    result = pipeline(
        input_data,
        fetch_data,
        lambda x: ProcessDataInput(records=x.records, count=x.count),
        process_data,
    )

    return result
```

---

### 13.5 エラー型の実装詳細

```python
# railway/core/errors.py

from typing import Any, Optional
from enum import Enum


class ErrorCode(str, Enum):
    """Standard error codes."""

    # Configuration errors
    CONFIG_NOT_FOUND = "CONFIG_001"
    CONFIG_INVALID = "CONFIG_002"

    # Validation errors
    VALIDATION_REQUIRED = "VALIDATION_001"
    VALIDATION_TYPE = "VALIDATION_002"

    # API errors
    API_SERVER_ERROR = "API_001"
    API_CLIENT_ERROR = "API_002"
    API_TIMEOUT = "API_003"

    # Network errors
    NETWORK_UNREACHABLE = "NETWORK_001"
    NETWORK_TIMEOUT = "NETWORK_002"

    # Database errors
    DB_CONNECTION_FAILED = "DB_001"
    DB_QUERY_FAILED = "DB_002"


class RailwayError(Exception):
    """
    Base Railway error with structured information.

    Provides:
    1. Error code for programmatic handling
    2. Context dict for debugging
    3. User-friendly hint for resolution
    """

    def __init__(
        self,
        message: str,
        *,
        code: ErrorCode | str | None = None,
        context: dict[str, Any] | None = None,
        hint: str | None = None,
    ):
        super().__init__(message)
        self.message = message
        self.code = code
        self.context = context or {}
        self.hint = hint

    def __str__(self) -> str:
        """Format error message with code, context, and hint."""
        parts = []

        if self.code:
            parts.append(f"[{self.code}]")

        parts.append(self.message)

        if self.hint:
            parts.append(f"\nHint: {self.hint}")

        if self.context:
            context_str = ", ".join(f"{k}={v}" for k, v in self.context.items())
            parts.append(f"\nContext: {context_str}")

        return " ".join(parts)


class RetryableError(RailwayError):
    """Error that should be retried."""
    pass


class FatalError(RailwayError):
    """Error that should not be retried."""
    pass


# Domain-specific errors
class ConfigurationError(FatalError):
    """Configuration related error."""
    pass


class ValidationError(FatalError):
    """Data validation error."""
    pass


class APIError(RetryableError):
    """External API error."""
    pass


class NetworkError(RetryableError):
    """Network connectivity error."""
    pass


class DatabaseError(RetryableError):
    """Database operation error."""
    pass
```

---

### 13.6 ロギング初期化

```python
# src/settings.py (updated with logging initialization)

from loguru import logger
import sys


class Settings(BaseSettings):
    """Settings with automatic logging initialization."""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._load_config()
        self._apply_overrides()
        self._initialize_logging()

    def _initialize_logging(self) -> None:
        """
        Initialize loguru based on configuration.

        Steps:
        1. Remove default handler
        2. Add configured handlers (console, file)
        3. Apply log level and format
        """
        # Remove default handler
        logger.remove()

        # Add handlers from config
        for handler_config in self.logging.handlers:
            if handler_config.type == "console":
                logger.add(
                    sys.stderr,
                    level=handler_config.level,
                    format=self.logging.format,
                    colorize=True,
                )
            elif handler_config.type == "file":
                logger.add(
                    handler_config.path,
                    level=handler_config.level,
                    format=self.logging.format,
                    rotation=handler_config.rotation,
                    retention=handler_config.retention,
                    encoding="utf-8",
                )

        logger.info(
            f"Logging initialized "
            f"(env={self.railway_env}, level={self.logging.level})"
        )


# Global settings instance (initializes logging on import)
settings = Settings()
```

---

### 13.7 非同期ノードのサポート

```python
# railway/core/decorators.py (async support)

import asyncio
from typing import Awaitable


def node(func=None, *, retry=True, log_input=False, log_output=False, name=None):
    """Node decorator with async support."""

    def decorator(f):
        node_name = name or f.__name__
        is_async = asyncio.iscoroutinefunction(f)

        if is_async:
            # Async wrapper
            @wraps(f)
            async def async_wrapper(*args, **kwargs):
                logger.info(f"[{node_name}] Starting (async)...")

                try:
                    result = await f(*args, **kwargs)
                    logger.info(f"[{node_name}] ✓ Completed")
                    return result
                except Exception as e:
                    logger.error(f"[{node_name}] ✗ Failed: {e}")
                    raise

            async_wrapper._is_railway_node = True
            async_wrapper._node_name = node_name
            async_wrapper._is_async = True
            return async_wrapper

        else:
            # Sync wrapper (as before)
            @wraps(f)
            def sync_wrapper(*args, **kwargs):
                logger.info(f"[{node_name}] Starting...")

                try:
                    result = f(*args, **kwargs)
                    logger.info(f"[{node_name}] ✓ Completed")
                    return result
                except Exception as e:
                    logger.error(f"[{node_name}] ✗ Failed: {e}")
                    raise

            sync_wrapper._is_railway_node = True
            sync_wrapper._node_name = node_name
            sync_wrapper._is_async = False
            return sync_wrapper

    if func is None:
        return decorator
    return decorator(func)


# Async pipeline
async def pipeline_async(initial, *steps):
    """Async pipeline that supports both sync and async nodes."""
    current_value = initial

    # Await initial if it's a coroutine
    if asyncio.iscoroutine(current_value):
        current_value = await current_value

    for i, step in enumerate(steps, 1):
        step_name = getattr(step, '_node_name', step.__name__)

        try:
            # Check if step is async
            if getattr(step, '_is_async', False) or asyncio.iscoroutinefunction(step):
                current_value = await step(current_value)
            else:
                current_value = step(current_value)

        except Exception as e:
            logger.error(f"Pipeline step {i} ({step_name}): Failed")
            raise

    return current_value
```

---

## 14. 実装ガイドライン

### 14.1 実装の優先順位

**Phase 1a: 基本機能 (Week 1-2)**
1. `@node` デコレータ (基本版、リトライなし)
2. `pipeline()` 関数 (基本版)
3. `@entry_point` デコレータ (基本版)
4. CLI: `railway init`, `railway new`

**Phase 1b: 拡張機能 (Week 3-4)**
5. リトライ機能
6. 設定ファイル読み込み
7. ロギング初期化
8. CLI: `railway list`

**Phase 1c: 高度な機能 (Week 5-6)**
9. 型チェック (オプション)
10. Pydantic モデルサポート
11. 非同期ノードサポート
12. エラー型階層

### 14.2 テスト戦略

```python
# tests/core/test_decorators.py

import pytest
from railway.core.decorators import node, entry_point
from railway.core.pipeline import pipeline


class TestNodeDecorator:
    """Test suite for @node decorator."""

    def test_node_basic(self):
        """Basic node execution."""
        @node
        def add_one(x: int) -> int:
            return x + 1

        result = add_one(5)
        assert result == 6

    def test_node_error_handling(self):
        """Node should propagate exceptions."""
        @node
        def failing_node() -> int:
            raise ValueError("Test error")

        with pytest.raises(ValueError):
            failing_node()

    def test_node_metadata(self):
        """Node should store metadata."""
        @node
        def my_node() -> int:
            return 42

        assert my_node._is_railway_node is True
        assert my_node._node_name == "my_node"


class TestPipeline:
    """Test suite for pipeline function."""

    def test_pipeline_success(self):
        """Pipeline should execute all steps."""
        @node
        def step1(x: int) -> int:
            return x + 1

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

        result = pipeline(5, step1, step2)
        assert result == 12  # (5 + 1) * 2

    def test_pipeline_error_propagation(self):
        """Pipeline should stop on error."""
        @node
        def step1(x: int) -> int:
            return x + 1

        @node
        def step2(x: int) -> int:
            raise ValueError("Error in step2")

        @node
        def step3(x: int) -> int:
            return x * 2  # Should not be executed

        with pytest.raises(ValueError):
            pipeline(5, step1, step2, step3)
```

---

## 15. Phase 1とPhase 2の機能境界

このセクションでは、Phase 1で実装する機能とPhase 2に延期する機能の境界を明確に定義します。

### 15.1 非同期処理のサポート範囲

#### Phase 1: 基本サポート

**サポート内容:**
- `@node`デコレータで`async def`関数を自動検出
- 非同期ノードの実行とログ記録
- 非同期ノードを個別に`await`で実行可能

**制限事項:**
- `pipeline()`関数は**同期ノードのみ**サポート
- 非同期ノードを`pipeline()`に渡すと実行時エラー
- 同期・非同期の混在パイプラインは不可

**使用例（Phase 1）:**
```python
from railway import node, entry_point
import asyncio

@node
async def async_fetch_data() -> dict:
    """Async node - Phase 1 support."""
    await asyncio.sleep(1)
    return {"data": "fetched"}

@node
def sync_process_data(data: dict) -> dict:
    """Sync node."""
    return {"processed": data}

@entry_point
async def main():
    """Entry point can be async."""
    # ✓ OK: Call async node directly
    data = await async_fetch_data()

    # ✓ OK: Call sync node
    result = sync_process_data(data)

    # ✗ NG: Cannot mix async/sync in pipeline (Phase 1)
    # result = pipeline(
    #     async_fetch_data(),  # ← Error!
    #     sync_process_data,
    # )

    return result
```

#### Phase 2: 完全サポート

**追加機能:**
- `pipeline_async()`関数の実装
- 同期・非同期混在パイプライン
- 並列実行（`asyncio.gather`）のサポート

**使用例（Phase 2）:**
```python
@entry_point
async def main():
    """Entry point with async pipeline."""
    # ✓ OK: Mix async/sync nodes in Phase 2
    result = await pipeline_async(
        async_fetch_data(),
        sync_process_data,  # ← Sync node in async pipeline
        async_save_data,
    )
    return result
```

**Phase 2実装時期:** 第2回リリース（Phase 1リリースから3-6ヶ月後）

---

### 15.2 グラフベース実行

#### Phase 1: パイプライン実行のみ

**サポート内容:**
- `pipeline()`関数による明示的なパイプライン構築
- 線形の依存関係のみサポート

**制限事項:**
- グラフ定義ファイル（graph.yaml）は未サポート
- 複数の依存関係を持つノードは未サポート
- 並列実行は未サポート
- 動的な実行順序解決は未サポート

#### Phase 2: グラフベース実行

**追加機能:**
- `graph.yaml`による依存関係の定義
- 複数の依存関係を持つノードのサポート
- 並列実行可能なノードの自動検出
- 動的な実行順序解決
- Mermaid形式でのグラフ可視化
- WebUIでのインタラクティブな可視化

**Phase 2実装時期:** 第2回リリース（Phase 1リリースから3-6ヶ月後）

---

### 15.3 実装優先順位（Phase 1）

#### Phase 1a: 基本機能（Week 1-2）
1. CLIコマンド: `railway init`, `railway new`, `railway list`, `railway run`
2. `@node`デコレータ（同期版、リトライなし）
3. `@entry_point`デコレータ（同期版）
4. `pipeline()`関数（同期版のみ、型チェック有効）
5. 統合設定ファイルの読み込み（pydantic-settings）
6. ロギング設定（loguru）

#### Phase 1b: 拡張機能（Week 3-4）
7. リトライ機能（tenacity統合）
8. エラー表示の改善（ヒント、ログファイル場所の明示）
9. TUTORIAL.mdの自動生成
10. テストテンプレートの自動生成
11. `railway run`コマンドの実装

#### Phase 1c: 高度な機能（Week 5-6）
12. 型チェック（デフォルト有効、strictモード）
13. 非同期ノードの基本サポート（`async def`の検出とログ記録）
14. カスタムエラー型階層（RailwayError, RetryableError, FatalError）
15. テスト用設定注入機能（遅延ロード）

#### Phase 1で実装しない機能（Phase 2以降）
- `pipeline_async()`関数
- グラフベース実行（graph.yaml）
- WebUIでの可視化
- 分散実行
- クラウドサービス統合
- スケジューラー統合

---

**仕様書バージョン:** 2.3.0
**最終更新日:** 2026-01-08
**レビューステータス:** 第4回改善完了
