局所トレーサビリティ — グループ: CACHE

# 局所トレーサビリティビュー グループ: CACHE

生成日時: 2026-03-11 00:06:09

### 対象アイテム

11

### REQ

1

### ARCH

1

### SPEC

3

### TST

3

### IMPL

3

### レビュー済

11/11

### Suspect

0

### グループ

CACHE

## トレーサビリティマトリクス

✓=レビュー済 ○=未レビュー ⚠=Suspect（複数同時表示あり。IDクリックで詳細へ）

グループ | REQ | ARCH | SPEC | TST | IMPL
--- | --- | --- | --- | --- | ---
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC001 ✓ ## インターフェース ```python @spot.mark( save_blob: Optional[bool] = None, key... | TST001 ✓ ## 目的 `@spot.mark()` はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元... | IMPL001 ✓ ## 実装概要 `Spot.mark()` メソッドが本仕様の中核。二段階デコレータファクトリとして、 オプションを受け取る外側の `mark()` と、関数...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC002 ✓ ## インターフェース ```python @contextmanager def cached_run( self, *funcs: Any... | TST002 ✓ ## 目的 `cached_run` はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーテ... | IMPL002 ✓ ## 実装概要 `Spot.cached_run()` はコンテキストマネージャとして実装。 渡された関数群を内部で `mark()` と同等のラッピングを行...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC003 ✓ ## 概要 `@spot.mark()` デコレータは同期関数と非同期（`async def`）関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャ... | TST003 ✓ ## 目的 beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約（スレッドをまたぐコルーチン実... | IMPL003 ✓ ## 実装概要 `Spot._execute_sync()` と `Spot._execute_async()` が実行エンジンの中核。 両メソッドは同一のフ...

## カバレッジ（局所）

リンク方向 | カバー数 | カバー率 | 未カバー
--- | --- | --- | ---
ARCH → REQ | 1 / 1 | 100.0% | —
SPEC → ARCH | 1 / 1 | 100.0% | —
TST → SPEC | 3 / 3 | 100.0% | —
IMPL → SPEC | 3 / 3 | 100.0% | —

## アイテム詳細

### REQ001 REQ {h(g)} ✓ レビュー済

関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。

関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。

親: —

子: ARCH001

### ARCH001 ARCH {h(g)} ✓ レビュー済

## コンポーネント構成

    graph TD
      A[bs.Spot Factory] -->|DI/Composition| B[core.Spot]
      B --> C[TaskDBBase]
      B --> D[SerializerProtocol]
      B --> E[BlobStorageBase]
      B --> F[StoragePolicyProtocol]
      B --> G[LimiterProtocol]
      B --> H[LifecyclePolicy]

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
core.Spot | キャッシュエンジン。キー生成→検索→実行→保存の制御 | `mark()`, `cached_run()`
TaskDBBase | メタデータ永続化。キャッシュキーによる検索と保存 | `find_by_key()`, `insert()`
SerializerProtocol | データのシリアライズ/デシリアライズ | `pack()`, `unpack()`
BlobStorageBase | 大規模データの外部ストレージ保存 | `save()`, `load()`, `delete()`
StoragePolicyProtocol | Blob保存の判定ポリシー | `should_save_as_blob()`

## データフロー

    sequenceDiagram
      participant User as ユーザーコード
      participant Spot as core.Spot
      participant DB as TaskDB
      participant Ser as Serializer
      participant Blob as BlobStorage

      User->>Spot: fn(args)
      Spot->>DB: find_by_key(cache_key)
      alt キャッシュヒット
        DB-->>Spot: cached_data
        Spot->>Ser: unpack(data)
      else キャッシュミス
        Spot->>Spot: fn(args) 実行
        Spot->>Ser: pack(result)
        Spot->>DB: insert(metadata)
        opt Blob保存
          Spot->>Blob: save(key, data)
        end
      end
      Spot-->>User: result

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
メタデータDB | SQLite | ゼロ設定、組み込み可能、十分な性能
シリアライズ | MessagePack | JSONより高速・コンパクト、バイナリ対応
ストレージ | ローカルファイル / クラウド | 小規模はローカル、大規模は外部ストレージ等で拡張可能

## 非機能要件方針

- 透過性: ユーザー関数の入出力に影響を与えないこと。
- 拡張性: 全コンポーネントはProtocol/ABCで抽象化し、DIで差し替え可能とすること。

## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[core.Spot] B --> C[TaskDBBase] B --> D[SerializerProtocol] B --> E[BlobStorageBase] B --> F[StoragePolicyProtocol] B --> G[LimiterProtocol] B --> H[LifecyclePolicy] ``` ## コンポーネント責務 | コンポーネント | 責務 | インターフェース | |---|---|---| | core.Spot | キャッシュエンジン。キー生成→検索→実行→保存の制御 | `mark()`, `cached_run()` | | TaskDBBase | メタデータ永続化。キャッシュキーによる検索と保存 | `find_by_key()`, `insert()` | | SerializerProtocol | データのシリアライズ/デシリアライズ | `pack()`, `unpack()` | | BlobStorageBase | 大規模データの外部ストレージ保存 | `save()`, `load()`, `delete()` | | StoragePolicyProtocol | Blob保存の判定ポリシー | `should_save_as_blob()` | ## データフロー ```mermaid sequenceDiagram participant User as ユーザーコード participant Spot as core.Spot participant DB as TaskDB participant Ser as Serializer participant Blob as BlobStorage User->>Spot: fn(args) Spot->>DB: find_by_key(cache_key) alt キャッシュヒット DB-->>Spot: cached_data Spot->>Ser: unpack(data) else キャッシュミス Spot->>Spot: fn(args) 実行 Spot->>Ser: pack(result) Spot->>DB: insert(metadata) opt Blob保存 Spot->>Blob: save(key, data) end end Spot-->>User: result ``` ## 技術選定 | 技術領域 | 選定 | 理由 | |---|---|---| | メタデータDB | SQLite | ゼロ設定、組み込み可能、十分な性能 | | シリアライズ | MessagePack | JSONより高速・コンパクト、バイナリ対応 | | ストレージ | ローカルファイル / クラウド | 小規模はローカル、大規模は外部ストレージ等で拡張可能 | ## 非機能要件方針 - **透過性**: ユーザー関数の入出力に影響を与えないこと。 - **拡張性**: 全コンポーネントはProtocol/ABCで抽象化し、DIで差し替え可能とすること。

親: REQ001

子: SPEC001, SPEC002, SPEC003

### SPEC001 SPEC {h(g)} ✓ レビュー済

## インターフェース

    @spot.mark(
        save_blob: Optional[bool] = None,
        keygen: Optional[Union[Callable, KeyGenPolicy]] = None,
        input_key_fn: Optional[Union[Callable, KeyGenPolicy]] = None,
        version: str | None = None,
        content_type: Optional[str | ContentType] = None,
        serializer: Optional[SerializerProtocol] = None,
        save_sync: Optional[bool] = None,
        retention: RetentionSpec = None,
        hooks: Optional[Sequence[HookBase]] = None,
    )
    def my_func(x, y): ...

## 振る舞い

### 基本フロー

1. デコレータが対象関数をラップする
2. 呼び出し時にキャッシュキーを生成する
3. DBからキャッシュを検索する
4. ヒット時: デシリアライズして結果を返す（関数は実行しない）
5. ミス時: 関数を実行し、結果をシリアライズしてDB/Blobに保存する

### sync/async 自動判定

- デコレーション時に `inspect.iscoroutinefunction(fn)` で判定する
- sync関数 → `_execute_sync()` に委譲する同期ラッパーを返す
- async関数 → `_execute_async()` に委譲する非同期ラッパーを返す

## パラメータ詳細

パラメータ | デフォルト | 説明
--- | --- | ---
`save_blob` | `None` | `None`: StoragePolicy に委譲、`True`: 強制Blob保存、`False`: DB内保存
`keygen` | `None` | キャッシュキー生成のカスタマイズ。引数やポリシーを指定可能
`input_key_fn` | `None` | （非推奨）`keygen` を使用すること
`version` | `None` | バージョン文字列。変更するとキャッシュキーが変わり無効化される
`content_type` | `None` | 戻り値のMIMEタイプ
`serializer` | `None` | `None`: Spotのデフォルトを使用。関数単位でオーバーライド可能
`save_sync` | `None` | 保存処理を同期的に行うかどうか（`None`の場合はデフォルトに従う）
`retention` | `None` | キャッシュの保持ポリシー（`"30d"` などの文字列や期間オブジェクト等）
`hooks` | `None` | 関数単位のフックリスト（実行前後やキャッシュヒット時に発火）

## エラーハンドリング

- ジェネレータ関数（`isgeneratorfunction` または `isasyncgenfunction`）を渡した場合 → デコレーション時に `ConfigurationError` を送出する
- `cached_run` 利用時に引数として関数が一つも指定されなかった場合 → `ValidationError` を送出する

## エッジケース

- ラップされた関数の `__name__`, `__doc__` は `functools.wraps` によって保持される
- `@mark` (括弧なし) と `@mark()` (括弧あり) の両方の記法をサポートする

## インターフェース ```python @spot.mark( save_blob: Optional[bool] = None, keygen: Optional[Union[Callable, KeyGenPolicy]] = None, input_key_fn: Optional[Union[Callable, KeyGenPolicy]] = None, version: str | None = None, content_type: Optional[str | ContentType] = None, serializer: Optional[SerializerProtocol] = None, save_sync: Optional[bool] = None, retention: RetentionSpec = None, hooks: Optional[Sequence[HookBase]] = None, ) def my_func(x, y): ... ``` ## 振る舞い ### 基本フロー 1. デコレータが対象関数をラップする 2. 呼び出し時にキャッシュキーを生成する 3. DBからキャッシュを検索する 4. **ヒット時**: デシリアライズして結果を返す（関数は実行しない） 5. **ミス時**: 関数を実行し、結果をシリアライズしてDB/Blobに保存する ### sync/async 自動判定 - デコレーション時に `inspect.iscoroutinefunction(fn)` で判定する - sync関数 → `_execute_sync()` に委譲する同期ラッパーを返す - async関数 → `_execute_async()` に委譲する非同期ラッパーを返す ## パラメータ詳細 | パラメータ | デフォルト | 説明 | |---|---|---| | `save_blob` | `None` | `None`: StoragePolicy に委譲、`True`: 強制Blob保存、`False`: DB内保存 | | `keygen` | `None` | キャッシュキー生成のカスタマイズ。引数やポリシーを指定可能 | | `input_key_fn` | `None` | （非推奨）`keygen` を使用すること | | `version` | `None` | バージョン文字列。変更するとキャッシュキーが変わり無効化される | | `content_type` | `None` | 戻り値のMIMEタイプ | | `serializer` | `None` | `None`: Spotのデフォルトを使用。関数単位でオーバーライド可能 | | `save_sync` | `None` | 保存処理を同期的に行うかどうか（`None`の場合はデフォルトに従う） | | `retention` | `None` | キャッシュの保持ポリシー（`"30d"` などの文字列や期間オブジェクト等） | | `hooks` | `None` | 関数単位のフックリスト（実行前後やキャッシュヒット時に発火） | ## エラーハンドリング - ジェネレータ関数（`isgeneratorfunction` または `isasyncgenfunction`）を渡した場合 → デコレーション時に `ConfigurationError` を送出する - `cached_run` 利用時に引数として関数が一つも指定されなかった場合 → `ValidationError` を送出する ## エッジケース - ラップされた関数の `__name__`, `__doc__` は `functools.wraps` によって保持される - `@mark` (括弧なし) と `@mark()` (括弧あり) の両方の記法をサポートする

親: ARCH001

子: IMPL001, TST001

### SPEC002 SPEC {h(g)} ✓ レビュー済

## インターフェース

    @contextmanager
    def cached_run(
        self,
        *funcs: Any,
        **kwargs: Any
    ) -> Iterator[Any]: ...

## 振る舞い

### 基本フロー

1. `Spot` インスタンスからコンテキストマネージャとして呼び出される。
2. 引数 `funcs` に渡された関数が0個の場合はエラーを送出する。
3. 各関数に対して、`Spot.mark(**kwargs)` で生成したキャッシュデコレータを適用し、ラップされた関数を生成する。
4. ラップされた関数を `yield` してコンテキストブロック内のユーザーコードに提供する。
5. ユーザーコード内での実行は、通常の `@spot.mark` 適用済み関数と同様にキャッシュ機構を経由する。
6. コンテキストブロック終了時のクリーンアップ処理は特に行わない（一時的なラッパーはスコープを抜けると破棄される）。

## パラメータ詳細

パラメータ | 必須 | 説明
--- | --- | ---
`*funcs` | はい | キャッシュ機能を適用したい対象の関数。外部ライブラリの関数なども指定可能。複数指定可能。
`**kwargs` | いいえ | `Spot.mark` デコレータに渡すオプション引数（`save_blob`, `keygen`, `version`, `content_type`, `serializer`, `retention`, `save_sync`, `hooks` 等）。

## 戻り値（Yields）の仕様

- 1つの関数を渡した場合: ラップされた単一の callable が返る。
- 複数の関数を渡した場合: ラップされた callable のタプルが返る。

## エラーハンドリング

- 引数エラー: `funcs` が空（0個）の場合、`beautyspot.exceptions.ValidationError` を送出し、コンテキストに入らない。

## エッジケース

- 非同期関数やジェネレータ関数を渡した場合の挙動は、基底となる `mark` デコレータの制約に完全に依存する。
- 適用されるキャッシュキーは元の関数に基づくため、同名・同シグネチャの別関数が混同されないように `KeyGen` のスコープ規則に従う。

## インターフェース ```python @contextmanager def cached_run( self, *funcs: Any, **kwargs: Any ) -> Iterator[Any]: ... ``` ## 振る舞い ### 基本フロー 1. `Spot` インスタンスからコンテキストマネージャとして呼び出される。 2. 引数 `funcs` に渡された関数が0個の場合はエラーを送出する。 3. 各関数に対して、`Spot.mark(**kwargs)` で生成したキャッシュデコレータを適用し、ラップされた関数を生成する。 4. ラップされた関数を `yield` してコンテキストブロック内のユーザーコードに提供する。 5. ユーザーコード内での実行は、通常の `@spot.mark` 適用済み関数と同様にキャッシュ機構を経由する。 6. コンテキストブロック終了時のクリーンアップ処理は特に行わない（一時的なラッパーはスコープを抜けると破棄される）。 ## パラメータ詳細 | パラメータ | 必須 | 説明 | |---|---|---| | `*funcs` | はい | キャッシュ機能を適用したい対象の関数。外部ライブラリの関数なども指定可能。複数指定可能。 | | `**kwargs` | いいえ | `Spot.mark` デコレータに渡すオプション引数（`save_blob`, `keygen`, `version`, `content_type`, `serializer`, `retention`, `save_sync`, `hooks` 等）。 | ## 戻り値（Yields）の仕様 - **1つの関数**を渡した場合: ラップされた単一の callable が返る。 - **複数の関数**を渡した場合: ラップされた callable のタプルが返る。 ## エラーハンドリング - **引数エラー**: `funcs` が空（0個）の場合、`beautyspot.exceptions.ValidationError` を送出し、コンテキストに入らない。 ## エッジケース - 非同期関数やジェネレータ関数を渡した場合の挙動は、基底となる `mark` デコレータの制約に完全に依存する。 - 適用されるキャッシュキーは元の関数に基づくため、同名・同シグネチャの別関数が混同されないように `KeyGen` のスコープ規則に従う。

親: ARCH001

子: IMPL002, TST002

### SPEC003 SPEC {h(g)} ✓ レビュー済

## 概要

`@spot.mark()` デコレータは同期関数と非同期（`async def`）関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャッシュ検索フローを透過的に切り替える。

## 振る舞い

### sync/async の自動判定

- デコレーション時に `inspect.iscoroutinefunction(func)` を用いて判定する。
- ランタイム時の判定オーバーヘッドを削減するため、ラッパー構築時に分岐を行う。

### 実行フロー

- 同期関数の場合:
- `_execute_sync()` に委譲する。
- キャッシュ保存（DBやBlobへのI/O）は、デフォルトでバックグラウンドのスレッドプール (`ThreadPoolExecutor`) にタスクを投げて非同期化される（`save_sync=False` の場合）。
- 非同期関数の場合:
- `_execute_async()` に委譲する。
- キャッシュ検索や保存処理などのI/Oバウンドな操作は、asyncio イベントループ経由でノンブロッキングに処理され、保存自体も非同期でキューに投入される。

## エラーハンドリング

- ジェネレータ関数の拒否: `inspect.isgeneratorfunction` または `inspect.isasyncgenfunction` で真となるジェネレータ・非同期ジェネレータが渡された場合、サポート対象外として即座に `ConfigurationError` を送出する。
- 保存タスク中のバックグラウンドエラーは、同期・非同期ともに `on_background_error` コールバックが設定されていればそこに処理が委譲される。

## エッジケース

- デコレーション後に手動で関数の性質（コルーチンかどうか）を変更するような動的な書き換えには対応しない（デコレーション時の静的な判定結果に依存する）。

## 概要 `@spot.mark()` デコレータは同期関数と非同期（`async def`）関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャッシュ検索フローを透過的に切り替える。 ## 振る舞い ### sync/async の自動判定 - デコレーション時に `inspect.iscoroutinefunction(func)` を用いて判定する。 - ランタイム時の判定オーバーヘッドを削減するため、ラッパー構築時に分岐を行う。 ### 実行フロー - **同期関数の場合**: - `_execute_sync()` に委譲する。 - キャッシュ保存（DBやBlobへのI/O）は、デフォルトでバックグラウンドのスレッドプール (`ThreadPoolExecutor`) にタスクを投げて非同期化される（`save_sync=False` の場合）。 - **非同期関数の場合**: - `_execute_async()` に委譲する。 - キャッシュ検索や保存処理などのI/Oバウンドな操作は、asyncio イベントループ経由でノンブロッキングに処理され、保存自体も非同期でキューに投入される。 ## エラーハンドリング - **ジェネレータ関数の拒否**: `inspect.isgeneratorfunction` または `inspect.isasyncgenfunction` で真となるジェネレータ・非同期ジェネレータが渡された場合、サポート対象外として即座に `ConfigurationError` を送出する。 - 保存タスク中のバックグラウンドエラーは、同期・非同期ともに `on_background_error` コールバックが設定されていればそこに処理が委譲される。 ## エッジケース - デコレーション後に手動で関数の性質（コルーチンかどうか）を変更するような動的な書き換えには対応しない（デコレーション時の静的な判定結果に依存する）。

親: ARCH001

子: IMPL003, TST003

### TST001 TST {h(g)} ✓ レビュー済

## 目的

`@spot.mark()` はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元の関数の挙動やメタデータが損なわれると、ユーザーコードのデバッグや型チェックに支障をきたすため、ラッパーの透過性を厳密に検証する。

## 検証観点

- 正常系: 同一引数での2回目呼び出しでキャッシュヒットし、元の関数が再実行されないこと
- Blobモード: `save_blob=True` 指定時にDB直接保存ではなくBlobストレージ経由で保存されること
- ラッパー透過性: デコレーション後も `__name__`, `__doc__`, `inspect.signature` が元の関数と一致すること
- シリアライザ差し替え: `serializer=` パラメータでデフォルトの `MsgpackSerializer` をカスタム実装に置換できること
- キャッシュ無効化: `version=` パラメータを変更するとキャッシュキーが変わり、既存キャッシュがヒットしなくなること

## 目的 `@spot.mark()` はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元の関数の挙動やメタデータが損なわれると、ユーザーコードのデバッグや型チェックに支障をきたすため、ラッパーの透過性を厳密に検証する。 ## 検証観点 - **正常系**: 同一引数での2回目呼び出しでキャッシュヒットし、元の関数が再実行されないこと - **Blobモード**: `save_blob=True` 指定時にDB直接保存ではなくBlobストレージ経由で保存されること - **ラッパー透過性**: デコレーション後も `__name__`, `__doc__`, `inspect.signature` が元の関数と一致すること - **シリアライザ差し替え**: `serializer=` パラメータでデフォルトの `MsgpackSerializer` をカスタム実装に置換できること - **キャッシュ無効化**: `version=` パラメータを変更するとキャッシュキーが変わり、既存キャッシュがヒットしなくなること

references: tests/integration/core/test_basic.py

親: SPEC001

子: —

### TST002 TST {h(g)} ✓ レビュー済

## 目的

`cached_run` はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーティ統合シナリオ全体に影響する。戻り値の型が関数の数で変わるスマートリターン仕様は、誤実装すると型安全性を損なうため重点的に検証する。

## 検証観点

- 単一関数: 1つの関数を渡した場合、結果がそのまま（タプルでなく）返ること
- 複数関数: 2つ以上の関数を渡した場合、結果がタプルで返ること
- バリデーション: 関数を0個渡した場合に `ValidationError` が送出されること
- 型推論: mypy / pyright で戻り値の型が正しく推論されること
- キャッシュ動作: コンテキスト内で同一引数の再呼び出しがキャッシュヒットすること

## 目的 `cached_run` はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーティ統合シナリオ全体に影響する。戻り値の型が関数の数で変わるスマートリターン仕様は、誤実装すると型安全性を損なうため重点的に検証する。 ## 検証観点 - **単一関数**: 1つの関数を渡した場合、結果がそのまま（タプルでなく）返ること - **複数関数**: 2つ以上の関数を渡した場合、結果がタプルで返ること - **バリデーション**: 関数を0個渡した場合に `ValidationError` が送出されること - **型推論**: mypy / pyright で戻り値の型が正しく推論されること - **キャッシュ動作**: コンテキスト内で同一引数の再呼び出しがキャッシュヒットすること

references: tests/integration/core/test_cached_run.py

親: SPEC002

子: —

### TST003 TST {h(g)} ✓ レビュー済

## 目的

beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約（スレッドをまたぐコルーチン実行、例外伝播のタイミング差異）により、同期版とは異なる不具合が発生しやすい。非同期パスの堅牢性を独立して検証する。

## 検証観点

- 自動判定: `inspect.iscoroutinefunction()` により同期/非同期が正しく識別され、適切なラッパーが適用されること
- 例外伝播: async 関数内で送出された例外が、await 側に正しく伝播し、キャッシュに保存されないこと
- 非同期 thundering herd: 同一キーへの複数の concurrent な `await` が1回の実行結果を共有すること
- 非同期保存: `save_sync=False` 時に `_BackgroundLoop` 経由でキャッシュ保存が非同期実行されること

## 目的 beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約（スレッドをまたぐコルーチン実行、例外伝播のタイミング差異）により、同期版とは異なる不具合が発生しやすい。非同期パスの堅牢性を独立して検証する。 ## 検証観点 - **自動判定**: `inspect.iscoroutinefunction()` により同期/非同期が正しく識別され、適切なラッパーが適用されること - **例外伝播**: async 関数内で送出された例外が、await 側に正しく伝播し、キャッシュに保存されないこと - **非同期 thundering herd**: 同一キーへの複数の concurrent な `await` が1回の実行結果を共有すること - **非同期保存**: `save_sync=False` 時に `_BackgroundLoop` 経由でキャッシュ保存が非同期実行されること

references: tests/integration/core/test_async_save.py

親: SPEC003

子: —

### IMPL001 IMPL {h(g)} ✓ レビュー済

## 実装概要

`Spot.mark()` メソッドが本仕様の中核。二段階デコレータファクトリとして、 オプションを受け取る外側の `mark()` と、関数をラップする内側の `decorator()` で構成される。 sync/async の判定は `inspect.iscoroutinefunction()` でデコレーション時に行い、 同期関数は `_execute_sync()`、非同期関数は `_execute_async()` に委譲する。 `functools.wraps(fn)` により元関数のメタデータ（`__name__`, `__doc__`, `__module__`, `__qualname__`）を保持する。

## 設計判断

### 二段階デコレータファクトリの採用

`@mark()` （括弧あり）と `@mark`（括弧なし）の両方をサポートするため、 引数の型で分岐するパターンを採用した。第一引数が callable なら括弧なし、 そうでなければ括弧ありと判定する。

### sync/async 分岐のタイミング

デコレーション時に判定する方式を採用。呼び出し時に毎回 `inspect` する方式と比較し、 ランタイムオーバーヘッドがゼロになる利点がある。ただしデコレーション後に 関数の性質が動的に変わるケースには対応できない（実用上問題なし）。

### ジェネレータ関数の拒否

`inspect.isgeneratorfunction()` と `inspect.isasyncgenfunction()` の 両方をチェックし、`ConfigurationError` を送出する。 ジェネレータの戻り値はイテレータであり、キャッシュの意味論と矛盾するため。

## 実装メモ

- `inspect.signature` の保持は `functools.wraps` が設定する `__wrapped__` 属性に依存する
- オプションの解決は `_resolve_settings()` で Spot レベルのデフォルト → 関数レベルのオーバーライドの順で行われる
- `keygen` パラメータが指定された場合、`KeyGenPolicy.bind(fn)` でシグネチャにバインドされたキー生成関数に変換される

## TODO / 技術的負債

- デコレータの多重適用（`@mark` を2回付ける）時の検出・警告が未実装

## 実装概要 `Spot.mark()` メソッドが本仕様の中核。二段階デコレータファクトリとして、 オプションを受け取る外側の `mark()` と、関数をラップする内側の `decorator()` で構成される。 sync/async の判定は `inspect.iscoroutinefunction()` で**デコレーション時**に行い、 同期関数は `_execute_sync()`、非同期関数は `_execute_async()` に委譲する。 `functools.wraps(fn)` により元関数のメタデータ（`__name__`, `__doc__`, `__module__`, `__qualname__`）を保持する。 ## 設計判断 ### 二段階デコレータファクトリの採用 `@mark()` （括弧あり）と `@mark`（括弧なし）の両方をサポートするため、 引数の型で分岐するパターンを採用した。第一引数が callable なら括弧なし、 そうでなければ括弧ありと判定する。 ### sync/async 分岐のタイミング デコレーション時に判定する方式を採用。呼び出し時に毎回 `inspect` する方式と比較し、 ランタイムオーバーヘッドがゼロになる利点がある。ただしデコレーション後に 関数の性質が動的に変わるケースには対応できない（実用上問題なし）。 ### ジェネレータ関数の拒否 `inspect.isgeneratorfunction()` と `inspect.isasyncgenfunction()` の 両方をチェックし、`ConfigurationError` を送出する。 ジェネレータの戻り値はイテレータであり、キャッシュの意味論と矛盾するため。 ## 実装メモ - `inspect.signature` の保持は `functools.wraps` が設定する `__wrapped__` 属性に依存する - オプションの解決は `_resolve_settings()` で Spot レベルのデフォルト → 関数レベルのオーバーライドの順で行われる - `keygen` パラメータが指定された場合、`KeyGenPolicy.bind(fn)` でシグネチャにバインドされたキー生成関数に変換される ## TODO / 技術的負債 - デコレータの多重適用（`@mark` を2回付ける）時の検出・警告が未実装

references: src/beautyspot/core.py

親: SPEC001

子: —

### IMPL002 IMPL {h(g)} ✓ レビュー済

## 実装概要

`Spot.cached_run()` はコンテキストマネージャとして実装。 渡された関数群を内部で `mark()` と同等のラッピングを行い、 コンテキスト内で呼び出すとキャッシュが効く一時的なラッパーを返す。

## 設計判断

### スマートリターンの型分岐

単一関数の場合は結果を直接返し、複数関数の場合はタプルで返す。 これにより `wrapped_fn = spot.cached_run(fn)` のように自然に書ける。 0個の場合は `ValidationError` で早期失敗させる。

### コンテキストマネージャパターン

`@contextmanager` デコレータを使用。`__enter__` でラップ済み関数を返し、 `__exit__` で後処理（drain 等）は `with spot:` に委譲する設計。

## 実装メモ

- 型推論（pyright/mypy）で戻り値型が正しく推論されるよう、`@overload` を使用している
- 外部ライブラリ関数のラッピングでは `functools.wraps` が元関数に適用される

## 実装概要 `Spot.cached_run()` はコンテキストマネージャとして実装。 渡された関数群を内部で `mark()` と同等のラッピングを行い、 コンテキスト内で呼び出すとキャッシュが効く一時的なラッパーを返す。 ## 設計判断 ### スマートリターンの型分岐 単一関数の場合は結果を直接返し、複数関数の場合はタプルで返す。 これにより `wrapped_fn = spot.cached_run(fn)` のように自然に書ける。 0個の場合は `ValidationError` で早期失敗させる。 ### コンテキストマネージャパターン `@contextmanager` デコレータを使用。`__enter__` でラップ済み関数を返し、 `__exit__` で後処理（drain 等）は `with spot:` に委譲する設計。 ## 実装メモ - 型推論（pyright/mypy）で戻り値型が正しく推論されるよう、`@overload` を使用している - 外部ライブラリ関数のラッピングでは `functools.wraps` が元関数に適用される

references: src/beautyspot/core.py

親: SPEC002

子: —

### IMPL003 IMPL {h(g)} ✓ レビュー済

## 実装概要

`Spot._execute_sync()` と `Spot._execute_async()` が実行エンジンの中核。 両メソッドは同一のフロー（キー生成→キャッシュ検索→Herd待機→関数実行→保存）を それぞれ同期/非同期のセマンティクスで実装する。 `mark()` がデコレーション時に `inspect.iscoroutinefunction()` で判定し、 適切な方を選択する。

## 設計判断

### 同期・非同期の並行実装

`_execute_sync` と `_execute_async` は処理フローが同一だが、 `await` の有無やロック機構（`threading.Event` vs `asyncio.Future`）が異なるため、 共通化せず並行して実装している。DRY 原則よりも明瞭性・デバッグ容易性を優先した。

### Herd 結果の保存前共有

Herd protection の結果ボックスへの結果格納は、DB/Blob 保存の前に行う。 これにより、保存が失敗しても待機中のスレッド/タスクは結果を受け取れる。 「保存の失敗で実行結果が失われる」ことを防ぐ意図的な設計。

### バックグラウンド保存のエラー隔離

`save_sync=False` 時の保存エラーは `on_background_error` コールバックに 通知し、ERROR ログを出力するが、例外を再送出しない。 関数の実行自体は成功しているため、ユーザーに例外を見せるのは不適切と判断した。

## 実装メモ

- フック呼び出し（`pre_execute`, `on_cache_hit`, `on_cache_miss`）は try/except で囲み、フック内の例外が実行結果に影響しないようにしている
- 自動エビクションは `_eviction_guard_lock` で非ブロッキングにガードし、 並行エビクションの重複実行を防止している
- `_execute_async` 内の保存処理は `_BackgroundLoop.submit()` でコルーチンとして投入される

## 実装概要 `Spot._execute_sync()` と `Spot._execute_async()` が実行エンジンの中核。 両メソッドは同一のフロー（キー生成→キャッシュ検索→Herd待機→関数実行→保存）を それぞれ同期/非同期のセマンティクスで実装する。 `mark()` がデコレーション時に `inspect.iscoroutinefunction()` で判定し、 適切な方を選択する。 ## 設計判断 ### 同期・非同期の並行実装 `_execute_sync` と `_execute_async` は処理フローが同一だが、 `await` の有無やロック機構（`threading.Event` vs `asyncio.Future`）が異なるため、 共通化せず並行して実装している。DRY 原則よりも明瞭性・デバッグ容易性を優先した。 ### Herd 結果の保存前共有 Herd protection の結果ボックスへの結果格納は、DB/Blob 保存の**前**に行う。 これにより、保存が失敗しても待機中のスレッド/タスクは結果を受け取れる。 「保存の失敗で実行結果が失われる」ことを防ぐ意図的な設計。 ### バックグラウンド保存のエラー隔離 `save_sync=False` 時の保存エラーは `on_background_error` コールバックに 通知し、ERROR ログを出力するが、例外を再送出しない。 関数の実行自体は成功しているため、ユーザーに例外を見せるのは不適切と判断した。 ## 実装メモ - フック呼び出し（`pre_execute`, `on_cache_hit`, `on_cache_miss`）は try/except で囲み、フック内の例外が実行結果に影響しないようにしている - 自動エビクションは `_eviction_guard_lock` で非ブロッキングにガードし、 並行エビクションの重複実行を防止している - `_execute_async` 内の保存処理は `_BackgroundLoop.submit()` でコルーチンとして投入される

references: src/beautyspot/core.py

親: SPEC003

子: —