Specification

# Doorstop Specifications

Generated at 2026-03-10 20:55:32

# REQ Specification

## 1.0 序文 REQ014

本ドキュメントは、Python関数キャッシュおよびレート制限ライブラリ『beautyspot』の要件を定義する。

## 2.0 背景・動機 REQ015

データパイプラインや機械学習の実験、外部API呼び出し等において、同一入力での不要な再実行を防ぐことで、実行時間の短縮とリソースの節約を実現する。開発者が最小限のコード変更で強力なキャッシュ・レート制限機能を導入できることを目指す。

## 3.0 用語定義 REQ016

- Spot: 本ライブラリにおけるキャッシュ・レート制限のコアエンジンの名称。
- キャッシュヒット: 保存された結果を再利用し、元の関数を実行しないこと。
- キャッシュミス: 結果が未保存または無効のため、元の関数を実行すること。
- Thundering Herd: 複数のクライアントが同時に同じ期限切れキャッシュ（または未キャッシュの重い処理）を要求し、システムに過負荷をかける問題。

3.1 用語集 REQ018
COMMON Non-normative

本プロジェクト（Beautyspot）で使用される主要な用語の定義です。

- Beautyspot: 本キャッシュライブラリの名称。
- キャッシュキー (Cache Key): 関数の入力引数等から一意に生成される識別子。
- メタデータDB (Metadata DB): キャッシュの有効期限、サイズ、関数名などの付帯情報を保存するデータストア。
- Blobストレージ (Blob Storage): キャッシュの実際のペイロード（戻り値のシリアライズ結果）を保存するストレージ。
- シリアライザ (Serializer): 関数の戻り値をストレージに保存可能なバイト列等に変換、および復元するコンポーネント。
- バックグラウンドIO (Background IO): メインスレッド（またはタスク）の実行をブロックせず、非同期にキャッシュ保存などを行う仕組み。
- レート制限 (Rate Limiting): 外部API呼び出し等の頻度を制御する仕組み。トークンバケット方式を採用。
- Thundering Herd: キャッシュミス時に、多数の並行リクエストが同時に同一関数を実行しようとして過負荷になる現象。
- フック (Hook): キャッシュのライフサイクル（ヒット、ミス、保存前後等）に介入できるコールバック機構。
- 依存注入 (Dependency Injection / DI): 各コンポーネント（DB、ストレージ等）を外部から差し替え可能にする設計パターン。

### 仕様書の機能グループ (Groups)

仕様書アイテムに付与される `groups` 属性の意味は以下の通りです。

グループ名 | 意味 / 対象領域
--- | ---
`CACHE` | 関数キャッシュの基本機能（同期・非同期のデコレータなど）
`KEY` | キャッシュキーの生成ロジック（引数のハッシュ化など）
`DB` | メタデータDB（有効期限、メタ情報の管理）
`STORE` | Blobストレージ（ペイロードの保存）
`SERIAL` | シリアライゼーション（データの変換処理）
`BGIO` | バックグラウンドIO（非同期書き込み処理）
`LIFE` | ライフサイクル管理（有効期限の判定、無効化ルールなど）
`LIMIT` | レート制限（トークンバケット方式等）
`HOOK` | コールバックフックの仕組み
`MAINT` | メンテナンス（キャッシュの削除、ガベージコレクション）
`CLI` | コマンドラインインターフェース
`DI` | 依存注入の仕組みとデフォルトファクトリ
`HERD` | Thundering Herd対策（直列化、重複実行防止）
`COMMON` | 共通定義（用語集など、特定の機能に縛られないもの）

## 4.0 機能要件 REQ017

本章ではシステムが提供すべき具体的な機能要件について定義する。

4.1 関数キャッシュ REQ001
CACHE

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

Children: ARCH001
4.2 キャッシュキー生成 REQ002
KEY

関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。

Children: ARCH002
4.3 メタデータDB REQ003
DB

キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。

抽象インターフェースにより、DBやストレージなどのバックエンド実装を差し替え可能であること。

Children: ARCH003
4.4 Blobストレージ REQ004
STORE

大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。

DB直接保存とBlob保存を自動判定できること。

Children: ARCH004
4.5 シリアライゼーション REQ005
SERIAL

関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。

Children: ARCH005
4.6 バックグラウンドIO REQ006
BGIO

キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。

Children: ARCH006
4.7 ライフサイクル管理 REQ007
LIFE

キャッシュの有効期限を関数名パターンに基づくルールで設定できること。個別の関数に対して保持期間を指定でき、期限切れのキャッシュを自動的に無効化できること。

Children: ARCH007
4.8 レート制限 REQ008
LIMIT

関数の実行頻度をトークンバケット方式で制限し、外部APIの呼び出しレートを制御できること。同期・非同期の両方に対応すること。

Children: ARCH008
4.9 フック機能 REQ009
HOOK

関数の実行前、キャッシュヒット時、キャッシュミス時にカスタムコールバック（フック）を実行できること。スレッドセーフなフック実装を提供すること。

Children: ARCH009
4.10 メンテナンス REQ010
MAINT

期限切れキャッシュの削除、孤立Blobファイルのガベージコレクション、時間ベースの一括削除（prune）等のメンテナンス操作ができること。

Children: ARCH010
4.11 CLI REQ011
CLI

コマンドラインインターフェースからキャッシュの一覧表示、詳細確認、統計表示、削除、GC等の管理操作ができること。

Children: ARCH011
4.12 依存注入設計 REQ012
DI

全コンポーネント（DB、ストレージ、シリアライザ、リミッター等）が `Protocol/ABC` に基づく依存注入により差し替え可能であること。ファクトリ関数がデフォルトの組み立てを提供すること。

Children: ARCH012
4.13 Thundering Herd対策 REQ013
HERD

同一キャッシュキーに対する並行リクエストを直列化し、1つのリクエストのみが関数を実行し、他のリクエストはその結果を共有することで重複実行を防止できること。

Children: ARCH013

# ARCH Specification

## 1.0 序文 ARCH014

本ドキュメントは、『beautyspot』のアーキテクチャ設計を定義する。

## 2.0 構成要素 ARCH015

システムを構成する主要コンポーネントとその責務、およびコンポーネント間の相互作用を定義する。

2.1 関数キャッシュアーキテクチャ ARCH001
CACHE

## コンポーネント構成

    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で差し替え可能とすること。

Parents: REQ001
Children: SPEC001, SPEC002, SPEC003
2.2 キャッシュキー生成アーキテクチャ ARCH002
KEY

## コンポーネント構成

    graph TD
      A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy]
      B -->|バインド| C[bound_keygen]
      C -->|引数別戦略| D[Strategy]
      C -->|正規化| E[canonicalize]
      C -->|ハッシュ計算| F[KeyGen.hash_items]
      E --> F

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
`KeyGenPolicy` | 関数の引数に対するハッシュ化戦略（無視、ファイル内容等）の宣言的定義と保持。 | `bind(func)`
`Strategy` | 引数ごとの処理方法（DEFAULT, IGNORE, FILE_CONTENT, PATH_STAT）の定義。 | -
`canonicalize` | Pythonオブジェクトを再帰的に正規化し、Msgpackで安定してシリアライズ可能な状態に変換。 | `@singledispatch canonicalize(obj)`
`KeyGen` | 正規化されたリストからSHA-256ハッシュを生成。レガシーデフォルトのハッシュ生成も担う。 | `hash_items(items)`, `from_file_content(path)`

## データフロー

    sequenceDiagram
      participant Spot as core.Spot
      participant KP as KeyGenPolicy
      participant Sig as inspect.signature
      participant Can as canonicalize
      participant KG as KeyGen

      Spot->>KP: bind(func)
      KP->>Sig: signature(func)
      Sig-->>KP: bound_keygen 生成
      KP-->>Spot: bound_keygen
      Spot->>Spot: bound_keygen(*args, **kwargs)
      loop 各引数
        alt Strategy.IGNORE
          Spot->>Spot: スキップ
        else Strategy.FILE_CONTENT
          Spot->>KG: from_file_content(path)
        else Strategy.PATH_STAT
          Spot->>KG: from_path_stat(path)
        else Strategy.DEFAULT
          Spot->>Can: canonicalize(val)
          Can-->>Spot: 正規化済みデータ
        end
      end
      Spot->>KG: hash_items(items_to_hash)
      KG-->>Spot: SHA-256 ハッシュ文字列 (キャッシュキー)

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
引数のバインド | `inspect.signature` | argsとkwargsを定義順に正確にマッピングし、デフォルト値も適用するため
正規化 | `functools.singledispatch` | 型ごとの正規化ロジックを拡張可能かつクリーンに実装するため
シリアライズ | `msgpack` | 高速でバイナリセーフであり、一貫したバイト列表現を得るため
ハッシュ関数 | SHA-256 | 衝突確率が極めて低く、暗号学的に安全かつ広くサポートされているため

## 非機能要件方針

- 安定性: プロセスの再起動やPythonのバージョン（辞書の順序等）に依存せず、同一入力に対して常に同一のハッシュを生成すること。
- 性能: 複雑なオブジェクト構造でも高速に正規化とハッシュ計算を行えること。ファイル内容のハッシュ化はチャンク読み込みでメモリ効率を確保する。
- 拡張性: ユーザーが独自の `KeyGenPolicy` を定義可能にし、柔軟なキャッシュ戦略をサポートする。

Parents: REQ002
Children: SPEC004, SPEC005, SPEC006
2.3 メタデータDBアーキテクチャ ARCH003
DB

## コンポーネント構成

    classDiagram
      class TaskDBCore {
        <<Protocol>>
        +init_schema() void
        +get(cache_key) TaskRecord
        +save(cache_key, ...) void
        +delete(cache_key) bool
      }
      class Maintenable {
        <<Protocol>>
        +delete_expired() int
        +prune(older_than) int
        +delete_all() int
      }
      class TaskDBMaintenable {
        <<Protocol>>
      }
      TaskDBCore <|-- TaskDBMaintenable
      Maintenable <|-- TaskDBMaintenable

      class SQLiteTaskDB {
        -db_path: Path
        -_write_queue: Queue
        +init_schema() void
        +get(cache_key) TaskRecord
        +save(cache_key, ...) void
      }
      TaskDBMaintenable <|-- SQLiteTaskDB

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
`TaskDBCore` | キャッシュ実行時に必要な最小限のメタデータDBアクセス | `init_schema()`, `get()`, `save()`, `delete()`
`Maintenable` | GCやCLI等、運用・保守に必要な拡張操作 | `delete_expired()`, `prune()`, `delete_all()`
`TaskDBMaintenable` | 実行用と保守用の両方を備えた上位Protocol | `TaskDBCore` + `Maintenable`
`SQLiteTaskDB` | SQLiteを用いたデフォルト実装。非同期書き込みキューを内包 | `TaskDBMaintenable` 実装

## データフロー

    sequenceDiagram
      participant Spot as core.Spot
      participant DB as SQLiteTaskDB
      participant Q as WriteQueue
      participant Thread as WriterThread
      participant SQLite as SQLiteDB

      Spot->>DB: get(cache_key)
      DB->>SQLite: SELECT
      SQLite-->>DB: TaskRecord
      DB-->>Spot: TaskRecord

      Spot->>DB: save(metadata)
      DB->>Q: put(WriteTask)
      DB-->>Spot: void (Non-blocking)

      Thread->>Q: get()
      Thread->>SQLite: INSERT / UPDATE

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
デフォルトメタデータDB | SQLite | ゼロ設定、組み込み可能、ローカルキャッシュとして十分な性能
バックグラウンド書き込み | キュー + 専用スレッド | SQLiteの排他制御によるメインスレッドのブロックを防ぐため
抽象化 | `Protocol`ベースのDI | `TaskDBCore`を満たせばPostgreSQL等に容易に差し替え可能

## 非機能要件方針

- 性能: 読み込みはメインスレッドで高速に処理（WALモード）、書き込みは専用スレッドによる非同期化。
- スレッド安全性: SQLiteへの書き込みは1つのスレッドに直列化し、データベースロックを回避する。
- 拡張性: すべての操作はProtocol (`TaskDBCore`, `Maintenable`) で定義され、利用側（`core.Spot`や`_CacheManager`）は実装に依存しない。

Parents: REQ003
Children: SPEC007, SPEC008
2.4 Blobストレージ構成 ARCH004
STORE

## コンポーネント構成

    graph TD
      A[core.Spot] -->|判定| B[StoragePolicyProtocol];
      A -->|保存/読込| C[BlobStorageBase];
      D[LocalStorage] --> C;
      E[S3Storage] --> C;
      A -->|メタデータ| F[TaskDBBase];

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
StoragePolicyProtocol | データのサイズ等に基づき、DB保存かBlob保存かを判定する | `should_save_as_blob()`
BlobStorageBase | 大規模データの外部ストレージ保存を抽象化する | `save()`, `load()`, `delete()`
LocalStorage | ローカルファイルシステムへのアトミックな保存と検証 | `base_dir`, `os.replace`
S3Storage | AWS S3互換オブジェクトストレージへの保存 | `s3://` URI, `boto3`

## データフロー

    sequenceDiagram
      participant Spot as core.Spot
      participant Policy as StoragePolicy
      participant DB as TaskDB
      participant Blob as BlobStorage

      Spot->>Policy: should_save_as_blob(data)
      alt Policy: True (Blob保存)
        Spot->>Blob: save(key, data)
        Blob-->>Spot: location (path/URI)
        Spot->>DB: insert(metadata, storage_type=FILE, blob_key=location)
      else Policy: False (Inline保存)
        Spot->>DB: insert(metadata, storage_type=DIRECT_BLOB, result_data=data)
      end

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
保存判定 | 閾値ベース (Threshold) | DBの肥大化を防ぎつつ、小容量データのI/Oを最適化
アトミック性 | 一時ファイル + Rename | 保存中のクラッシュや並行書き込みによる破損を完全に防止
クラウド対応 | Boto3 (S3) | 業界標準のプロトコルによる高い互換性とスケーラビリティ

## 非機能要件方針

- 安全性: LocalStorage では `is_relative_to` によるパストラバーサル防止を徹底

- 信頼性: 保存時は `fsync` を実行し、OSレベルでのデータ到達を確認

- 効率性: S3 保存時は `upload_fileobj` を使用し、5GB超のマルチパートアップロードに自動対応

Parents: REQ004
Children: SPEC009, SPEC010, SPEC011, SPEC024
2.5 シリアライゼーション構成 ARCH005
SERIAL

## コンポーネント構成

    graph TD
      A[MsgpackSerializer] -->|Registry| B[TypeRegistryProtocol]
      A -->|LRU Cache| C[Thread-Local Storage]
      A -->|Core| D[msgpack-python]

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
MsgpackSerializer | MessagePackベースの高速・コンパクトなバイナリ変換 | `dumps()`, `loads()`
TypeRegistryProtocol | ユーザー定義型（ExtType）のエンコーダ・デコーダ登録 | `register()`
Thread-Local Cache | free-threading環境でのロック競合を回避するMRO解決キャッシュ | `OrderedDict` (LRU)

## データフロー

    sequenceDiagram
      participant User as ユーザーコード
      participant Ser as MsgpackSerializer
      participant Reg as TypeRegistry
      participant MP as msgpack

      User->>Ser: dumps(obj)
      Ser->>Ser: 型チェック & MRO解決
      opt カスタム型
        Ser->>Reg: エンコーダ取得
        Reg-->>Ser: encoder_fn
        Ser->>MP: ExtType(code, encoder_fn(obj))
      end
      MP-->>Ser: binary
      Ser-->>User: bytes

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
フォーマット | MessagePack | JSONより高速・コンパクト。バイナリをネイティブにサポート
スレッド安全 | Copy-on-Write (CoW) | 読み取りパスからロックを排除し、マルチスレッド性能を最大化
型拡張 | ExtType (0-127) | カスタム型を1バイトのオーバーヘッドで表現可能

## 非機能要件方針

- 性能: MRO（継承関係）解決結果をLRUキャッシュし、深層木探索のコストを最小化
- 柔軟性: ユーザー定義のエンコーダがプリミティブを返せば、再帰的にシリアライズ可能
- 堅牢性: デコード時の `ExtCode` 未登録エラーを検知し、互換性問題を明示的に報告

Parents: REQ005
Children: SPEC012, SPEC013
2.6 バックグラウンドIO構成 ARCH006
BGIO

## コンポーネント構成

    graph TD
      A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop]
      B -->|Daemon Thread| C[asyncio.AbstractEventLoop]
      C -->|I/O| D[TaskDB / BlobStorage]
      A -->|ライフサイクル制御| E[atexit / ContextManager]

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
_BackgroundLoop | デーモンスレッドでのイベントループ管理とタスク投入 | `submit()`, `run_forever()`
core.Spot | 非同期保存のトリガーとエラーハンドリング | `_save_metadata_async()`
ContextManager | 処理終了時のタスク完了待機（Flush） | `__exit__`, `flush()`

## データフロー

    sequenceDiagram
      participant Main as メインスレッド
      participant BGLoop as _BackgroundLoop
      participant Store as ストレージ

      Main->>BGLoop: submit(save_coro)
      Note over Main: 関数は即座に結果を返す (save_sync=False)
      BGLoop->>Store: 書き込み実行 (I/O)
      alt 成功
          Store-->>BGLoop: OK
      else 失敗
          BGLoop->>Main: on_background_error(error)
      end

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
並行処理 | asyncio in Thread | GILを解放しつつ、多数のI/Oタスクを効率的に多重化
スレッド種類 | Daemon Thread | アプリケーション終了時にプロセスをブロックしない
終了制御 | atexit / drain | 強制終了時も、可能な限り保留中の書き込みを完了させる

## 非機能要件方針

- 透過性: 保存失敗時もユーザーコードの例外にはせず、コールバック経由で通知

- 一貫性: `flush()` により、クリティカルな処理の直後での保存完了を保証

- 安全性: シャットダウンシーケンス中に新規タスクを受け付けない排他制御を導入

Parents: REQ006
Children: SPEC014, SPEC015
2.7 ライフサイクル管理構成 ARCH007
LIFE

## コンポーネント構成

    graph TD
      A[core.Spot] -->|マッチング要求| B[LifecyclePolicy]
      B -->|ルールリスト| C[Rule]
      C -->|パターン照合| D[fnmatch]
      B -->|パース| E[Retention]

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
LifecyclePolicy | 複数のルールを管理し、関数名に最適な有効期限を決定する | `resolve_with_fallback()`
Rule | 1つのパターンとそれに対応する保持期間のペア | `match(func_name)`
Retention | 多様な時間表現（文字列/数値）を秒数に正規化する | `parse_retention()`

## データフロー

    sequenceDiagram
      participant Spot as core.Spot
      participant Policy as LifecyclePolicy
      participant DB as TaskDB

      Spot->>Policy: resolve_with_fallback("module.func")
      Policy->>Policy: ルール順次照合 (fnmatch)
      Policy-->>Spot: expires_at (timestamp)
      Spot->>DB: save(..., expires_at)
      Note over DB: get() 時に現在時刻と比較判定

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
パターンマッチ | fnmatch (Glob) | 正規表現よりも直感的で、開発者にとって馴染みのある指定方法
保持期間指定 | 文字列パーサー ("30d"等) | 設定ファイルやデコレータでの可読性を向上
判定タイミング | 遅延判定 (Lazy) | 書き込み・読み込み時のオーバーヘッドを最小化

## 非機能要件方針

- 優先順位: 完全修飾名（モジュール名込み）が短縮名より優先される二段構えのマッチング
- 堅牢性: ポリシーにマッチしない場合は「無期限」とするフェイルセーフな設計
- 拡張性: `Retention.FOREVER` 等の定数により、グローバルポリシーの個別上書きに対応

Parents: REQ007
Children: SPEC016, SPEC017
2.8 レート制限構成 ARCH008
LIMIT

## コンポーネント構成

    graph TD
      A["@spot.consume"] -->|宣言的適用| B[LimiterProtocol]
      C[core.Spot] -->|DI| B
      D[TokenBucket] --> B
      D -->|アルゴリズム| E[GCRA]
      D -->|時刻同期| F[monotonic clock]

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
TokenBucket | GCRAアルゴリズムによるスムースなトラフィック制御 | `consume()`, `consume_async()`
@spot.consume | 関数実行前のトークン消費を透過的に行うデコレータ | `cost` (int or callable)
LimiterProtocol | レートリミッターの差し替えを可能にするインターフェース | `LimiterProtocol`

## データフロー

    sequenceDiagram
      participant User as ユーザーコード
      participant Dec as @spot.consume
      participant Bucket as TokenBucket

      User->>Dec: call func()
      Dec->>Bucket: consume(cost)
      Bucket->>Bucket: 次回実行許可時刻の計算 (GCRA)
      alt 許可
          Bucket-->>Dec: OK
          Dec->>User: execute func()
      else 拒否 (Over rate)
          Bucket-->>Dec: raise ValueError/RateLimitError
      end

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
アルゴリズム | GCRA (Generic Cell Rate Algorithm) | 固定ウィンドウと違い「スムースな」流量制御が可能
待機制御 | asyncio.sleep / time.sleep | 同期・非同期の両方のコンテキストで正確なスロットリングを実現
コスト計算 | ダイナミックコスト | 引数に応じて消費トークン数を動的に変更可能

## 非機能要件方針

- 正確性: `time.monotonic()` を使用し、システム時刻の変更（NTP同期等）の影響を受けない

- バースト制御: `max_burst` 設定により、アイドル後の過度なバーストを防止

- スレッド安全: `threading.Lock` により、マルチスレッド環境での二重消費を防止

Parents: REQ008
Children: SPEC018
2.9 フック機能構成 ARCH009
HOOK

## コンポーネント構成

    graph TD
      A[core.Spot] -->|イベント通知| B[HookBase]
      C[ThreadSafeHookBase]　--> B
      B -->|コンテキスト| D[Context Objects]
      E[PreExecuteContext] --> D
      F[CacheHitContext] --> D
      G[CacheMissContext] --> D

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
HookBase | 実行ライフサイクルの各段階で呼び出される抽象基底クラス | `pre_execute`, `on_cache_hit`, `on_cache_miss`
ThreadSafeHookBase | RLockを用いて各フックメソッドの実行を直列化する | `__init_subclass__` による自動ラップ
Context Objects | フックに渡される実行時の引数、結果、例外、メタデータのコンテナ | `ctx.args`, `ctx.result` 等

## データフロー

    sequenceDiagram
      participant Spot as core.Spot
      participant Hook as HookBase
      participant Fn as Target Function

      Spot->>Hook: pre_execute(ctx)
      alt キャッシュヒット
          Spot->>Hook: on_cache_hit(ctx)
      else キャッシュミス
          Spot->>Fn: execute()
          Fn-->>Spot: result
          Spot->>Hook: on_cache_miss(ctx)
      end

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
パターン | オブザーバー | コアロジックを汚さずに、ログ出力や統計取得等の横断的関心を分離
スレッド安全 | 自動装飾 (Decorator) | ユーザーが意識せずに、サブクラス化するだけで安全なフックを実装可能
実行制御 | 準同期実行 | フック内の例外をキャッチしログ出力に留めることで、主処理を保護

## 非機能要件方針

- 透過性: フックの実行失敗（例外）は主処理の結果に影響を与えない

- 柔軟性: 関数ごとに異なるフックリストを `hooks=[...]` で指定可能

- パフォーマンス: フック未登録時のオーバーヘッドを最小化するガード条件を実装

Parents: REQ009
Children: SPEC019
2.10 メンテナンス構成 ARCH010
MAINT

## コンポーネント構成

    graph TD
      A[MaintenanceService] -->|操作集約| B[TaskDBBase]
      A -->|操作集約| C[BlobStorageBase]
      A -->|自動実行| D[Probabilistic Eviction]

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
MaintenanceService | DBとストレージを跨ぐクリーンアップ操作のファサード | `clean_garbage()`, `prune()`
TaskDBBase | 参照カウントや期限切れレコードの提供 | `get_blob_refs()`, `delete_expired()`
BlobStorageBase | 物理ファイルの列挙と削除 | `list_keys()`, `delete()`

## データフロー (GCプロセス)

    sequenceDiagram
      participant Service as MaintenanceService
      participant DB as TaskDB
      participant Storage as BlobStorage

      Service->>DB: get_blob_refs()
      DB-->>Service: 有効な参照リスト (Whitelist)
      Service->>Storage: list_keys()
      Storage-->>Service: 全ファイルリスト
      Service->>Service: 差分抽出 (孤立ファイルの特定)
      Service->>Storage: delete(orphan_keys)
      Note over Service: 猶予期間 (grace_period) を考慮して削除

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
削除ポリシー | ホワイトリスト方式 | DBに記録がないBlobを削除対象とすることで、データの整合性を担保
競合防止 | 更新時刻確認 (mtime) | 保存中のファイルを誤って消さないよう、一定時間経過した孤立のみ削除
実行頻度 | 確率的オートエビクション | 書き込み時に低確率でGCをキックし、手動メンテなしでの健康度を維持

## 非機能要件方針

- 安全性: DBレコードの削除 → Blobの削除の順序を徹底し、参照エラーを防止
- 可用性: メンテナンス中も通常のキャッシュ読み取りを妨げないロック粒度
- 観測性: 削除されたタスク数や解放された容量を統計として返却

Parents: REQ010
Children: SPEC020
2.11 CLI構成 ARCH011
CLI

## コンポーネント構成

    graph TD
      A[beautyspot CLI] -->|Command| B[Typer App]
      B -->|Business Logic| C[MaintenanceService]
      B -->|Output| D[Rich Console]
      B -->|Dashboard| E[Dashboard App]

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
Typer App | コマンドライン引数のパースとサブコマンドのディスパッチ | `main()`, `gc()`, `list()`
MaintenanceService | 実際のキャッシュ管理ロジックの実行 | `clean_garbage()`, `clear()`
Rich Console | ターミナルでの視覚的に分かりやすいテーブルやパネルの描画 | `Console`, `Table`
Dashboard App | キャッシュ状態をインタラクティブに閲覧するためのTUI | `dashboard.py`

## データフロー

    sequenceDiagram
      participant User as ユーザー
      participant CLI as CLI App
      participant Service as MaintenanceService
      participant Output as Rich Console

      User->>CLI: beautyspot gc --name my-project
      CLI->>Service: clean_garbage()
      Service-->>CLI: result (removed count, etc.)
      CLI->>Output: print summary table
      Output-->>User: formatted output

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
フレームワーク | Typer | 型ヒントに基づいた堅牢なコマンドラインインターフェースを迅速に構築
出力装飾 | Rich | プログレスバーやステータス表示により、長時間処理の進捗を可視化
連携 | Factory DI | カレントディレクトリや設定から自動的に `Spot` インスタンスを組み立て

## 非機能要件方針

- UX: 破壊的な操作（`clear`等）には対話的な確認プロンプトを表示
- 互換性: 標準出力のパイプ連携を妨げないよう、色の自動無効化に対応
- 利便性: 特定のプロジェクトだけでなく、全DBを横断的にスキャンする機能を提供

Parents: REQ011
Children: SPEC021
2.12 依存注入構成 ARCH012
DI

## コンポーネント構成

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

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
bs.Spot (Factory) | 環境（名前、パス等）に応じたデフォルトコンポーネントの選定と組み立て | `bs.Spot()`
core.Spot | 注入された依存関係を使用して、キャッシュロジックをオーケストレーションする | `mark()`, `cached_run()`
Protocols / ABCs | 各コンポーネントが満たすべき契約（インターフェース）の定義 | `SerializerProtocol` 等

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
パターン | Constructor Injection | 依存関係を明示的に渡し、テスト時のMock差し替えを容易にする
インターフェース | Protocol (Duck Typing) | 厳密な継承を強制せず、構造的部分型によりサードパーティ実装を受け入れ
デフォルト設定 | 規約より構成 (CoC) | `.beautyspot/` ディレクトリを基点とした標準パスを自動生成

## 非機能要件方針

- 結合度: `core.Spot` は具体的なクラス名（SQLite等）を知らず、Protocolのみに依存する
- テスト容易性: メモリ上のDBや仮装ストレージを注入することで、高速な単体テストが可能
- 拡張性: ユーザーが独自のシリアライザやストレージを自作して注入可能

Parents: REQ012
Children: SPEC022
2.13 Thundering Herd対策構成 ARCH013
HERD

## コンポーネント構成

    graph TD
      A[core.Spot] -->|管理要求| B[CacheManager]
      B -->|In-flight追跡| C[_inflight Dict]
      C -->|同期待機| D[threading.Event]
      C -->|非同期待機| E[asyncio.Future]

## コンポーネント責務

コンポーネント責務インターフェースCacheManagerキャッシュキーごとの実行状態管理と実行のシリアライズ`get_or_create_inflight()`_inflight実行中のタスクを保持し、後続リクエストをイベントで待機させる`ExecutionState`core.Spot待機結果の受け取りとエラーの伝播`cached_run()`

## データフロー

    sequenceDiagram
      participant T1 as Thread 1 (First)
      participant T2 as Thread 2 (Second)
      participant CM as CacheManager
      participant Fn as Target Function

      T1->>CM: get_or_create(key)
      Note over CM: 新規作成 (Executor)
      T2->>CM: get_or_create(key)
      Note over CM: 既存あり (Waiter)

      T1->>Fn: execute()
      Fn-->>T1: result
      T1->>CM: set_result(key, result)
      Note over CM: Eventをセット
      CM-->>T2: notify result
      T1-->>T1: return result
      T2-->>T2: return result

## 技術選定

技術領域選定理由待機機構Event / FutureOS/ランタイムレベルの待機を使用し、CPU負荷（ビジーループ）を回避スコープキャッシュキー単位異なる関数の実行は妨げず、同一入力のみを直列化安全策タイムアウト & リトライ実行者がハングした場合に、待機側がデッドロックしないよう保護

## 非機能要件方針

- 一貫性: 実行者が成功すれば全員に結果を、失敗すれば全員に同じ例外を伝播

- 効率性: メモリリークを防ぐため、結果配布後に `_inflight` エントリを即座に削除

- 信頼性: 強参照 (`StrongReference`) で実行中のタスクを保持し、GCによる消失を防止

Parents: REQ013
Children: SPEC023

# SPEC Specification

## 1.0 序文 SPEC025

本ドキュメントは、『beautyspot』の各機能における詳細な仕様、インターフェース、およびアルゴリズムを定義する。

## 2.0 詳細仕様 SPEC026

各コンポーネントが提供するAPI、内部状態遷移、および例外ハンドリングの詳細を定義する。

2.1 mark デコレータ SPEC001
CACHE

## インターフェース

    @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()` (括弧あり) の両方の記法をサポートする

Parents: ARCH001
Children: IMPL001, TST001
2.2 cached_run コンテキストマネージャ SPEC002
CACHE

## インターフェース

    @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` のスコープ規則に従う。

Parents: ARCH001
Children: IMPL002, TST002
2.3 同期・非同期関数サポート SPEC003
CACHE

## 概要

`@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` コールバックが設定されていればそこに処理が委譲される。

## エッジケース

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

Parents: ARCH001
Children: IMPL003, TST003
2.4 SHA-256キャッシュキー SPEC004
KEY

## インターフェース

    class KeyGen:
        @staticmethod
        def _default(args: tuple, kwargs: dict) -> str: ...

        @staticmethod
        def hash_items(items: list) -> str: ...

## 振る舞い

### 基本フロー (`_default`)

1. 正規化: `args` と `kwargs` を `canonicalize()` により再帰的に正規化する
2. パック: 正規化された構造 `[args, kwargs]` を MessagePack でバイナリにシリアライズする
3. ハッシュ: バイナリデータに対して SHA-256 ハッシュを計算し、16進数文字列を返す

### リストハッシュ (`hash_items`)

- 任意個の正規化済みアイテムを含むリストを受け取り、一括でパックしてハッシュ化する。`KeyGenPolicy` で使用される。

## パラメータ詳細

パラメータ | 型 | 説明
--- | --- | ---
`args` | `tuple` | 関数の位置引数
`kwargs` | `dict` | 関数のキーワード引数
`items` | `list` | バインド済み引数の正規化値リスト

## エラーハンドリング

- 再帰制限超過: `RecursionError` 発生時は、安全のため `str((args, kwargs))` のハッシュをフォールバックとして使用する
- パック失敗: MessagePack シリアライズに失敗した場合は、警告をログ出力した上で `str()` ベースのハッシュを返す

## エッジケース

- 空の引数: 引数なしの場合も `[(), {}]` の構造として一定のハッシュ値を生成する
- 非決定的な repr: フォールバック時の `str()` はオブジェクトによっては実行ごとに変わる可能性があるため、明示的な正規化が推奨される

Parents: ARCH002
Children: IMPL004, TST004
2.5 引数の再帰的正規化 SPEC005
KEY

## インターフェース

    @singledispatch
    def canonicalize(obj: Any) -> Any: ...

## 振る舞い

`singledispatch` を使用し、型に応じた最適な正規化形式へ再帰的に変換する。

### 主要な型別の処理

- 基本型 (int, float, str, bytes): そのまま返す
- bool: `("__bool__", value)` のタプルに変換（`1` と `True` の衝突を防止）
- dict: キーでソートし、`[[k1, v1], [k2, v2], ...]` のリストに変換
- list / tuple / set / frozenset / deque: それぞれ専用の型タグ（例: `"__list__"`) を付与し、要素を再帰的に正規化。集合型はソートを行う
- Enum: `("__enum__", module, qualname, value)` に変換
- カスタムオブジェクト: `__dict__` または `__slots__` から属性を取得し、型名と共に正規化

## パラメータ詳細

オブジェクト型 | 正規化後の形式 | 備考
--- | --- | ---
`numpy.ndarray` | `("__numpy__", shape, dtype, bytes)` | 高速かつ正確なハッシュを保証
`OrderedDict` | `("__ordered_dict__", items)` | 挿入順序を保持したまま正規化
`Pydantic Model` | `("__pydantic_v2__", schema)` | スキーマ情報をベースに判定

## エラーハンドリング

- 未対応型: 警告をログ出力し、`str(obj)` にフォールバックする
- 循環参照: `RecursionError` をキャッチし、その階層で `str()` による正規化に切り替える

## エッジケース

- 異なる型の比較: `_safe_sort_key` により、Python 3 で比較不可能な型同士（例: `int` と `str`）が辞書のキーに含まれていても安定したソート順を保証する

Parents: ARCH002
Children: IMPL005, TST005
2.6 KeyGenPolicy 戦略 SPEC006
KEY

## インターフェース

    class Strategy(Enum):
        DEFAULT = auto()      # 再帰的正規化
        IGNORE = auto()       # キー計算から除外
        FILE_CONTENT = auto() # ファイルの内容をハッシュ
        PATH_STAT = auto()    # パス+サイズ+更新時刻をハッシュ

    class KeyGenPolicy:
        def bind(self, func: Callable) -> Callable[..., str]: ...

## 振る舞い

1. バインド: `bind(func)` 時に `inspect.signature` を解析し、引数名とデフォルト値を把握する
2. 適用: 呼び出し時に引数を実値にバインドし、デフォルト値を補填する
3. 戦略実行: 引数名ごとに指定された `Strategy` を適用し、正規化された値のリストを作成する
4. 集約: 全ての値を `KeyGen.hash_items()` で一つのハッシュにまとめる

## パラメータ詳細

戦略 | 内容 | 用途
--- | --- | ---
`IGNORE` | キャッシュキーに含めない | `verbose` や `logger` 等、結果に影響しない引数
`FILE_CONTENT` | ファイルを全読込してハッシュ化 | 入力ファイルの厳密な変更検知
`PATH_STAT` | `os.stat` 情報を使用 | 大容量ファイルの高速な変更検知

## エラーハンドリング

- ファイル不在: `FILE_CONTENT` / `PATH_STAT` 指定時にファイルがない場合、`"MISSING_{path}"` という固定文字列をハッシュ対象とする
- アクセスエラー: ファイル読み込み失敗時は `"ERROR_{path}"` を返す

## エッジケース

- シグネチャ不一致: 引数が関数のシグネチャと合わない場合は `TypeError` を送出する（実行前バリデーション）
- デフォルト値の変更: 関数のデフォルト値が変わると、同じ引数でもハッシュ値が変わる（意図的な挙動）

Parents: ARCH002
Children: IMPL006, TST006
2.7 SQLiteTaskDB SPEC007
DB

## インターフェース

    class SQLiteTaskDB(TaskDBBase):
        def __init__(self, db_path: str | Path, timeout: float = 30.0): ...
        def save(self, task: TaskRecord) -> None: ...
        def get(self, input_key: str) -> TaskRecord | None: ...

## 振る舞い

### ライタースレッド方式

- 書き込み操作は全て `_WriteTask` キューに投入され、専用のバックグラウンドスレッドで直列に実行される。これにより SQLite の `database is locked` エラーを回避する。

### 読み取り並行性

- 読み取りはスレッドローカルな接続を使用し、`PRAGMA query_only = ON` 状態で並行して実行される。

### スキーマ管理

- 起動時に `tasks` テーブルの存在を確認し、必要に応じて自動的に `ALTER TABLE` によるカラム追加（マイグレーション）を行う。

## パラメータ詳細

設定 | デフォルト | 説明
--- | --- | ---
`journal_mode` | `WAL` | 書き込みと読み取りの並行性を高める設定
`synchronous` | `NORMAL` | パフォーマンスと耐久性のバランス設定
`timeout` | `30.0` | データベースロック待機時間の閾値

## エラーハンドリング

- 接続失敗: `sqlite3.Error` をキャッチし、適切にラップして再送出する
- シャットダウン時: ライタースレッドへの新規タスク投入を停止し、保留中のタスクを完了させてから接続を閉じる

## エッジケース

- 破損DB: DBファイルが壊れている場合は、初期化時に検知しエラーとする
- インメモリDB: `:memory:` 指定時は、プロセスの生存期間中のみキャッシュが保持される

Parents: ARCH003
Children: IMPL007, TST007
2.8 DB プロトコル階層 SPEC008
DB

## インターフェース

    @runtime_checkable
    class TaskDBCore(Protocol):
        def get(self, input_key: str) -> TaskRecord | None: ...
        def save(self, record: TaskRecord) -> None: ...

    class TaskDBBase(ABC):
        # TaskDBCore を満たしつつ、メンテナンスのデフォルト実装を提供

## 振る舞い

### 階層構造

1. TaskDBCore: キャッシュの実行に必要な最小限のメソッド定義
2. Maintenable: GCや統計取得に必要な管理用メソッドの定義
3. TaskDBBase: 共通のバリデーションや例外処理を実装する基底クラス

## パラメータ詳細 (主要メソッド)

メソッド | 役割
--- | ---
`get_blob_refs()` | 全レコードの `blob_key` を列挙する（GC用）
`delete_expired()` | `expires_at` を経過したレコードを一括削除する
`get_history()` | 実行履歴（統計）を取得する

## エラーハンドリング

- プロトコルに未実装のメソッドを呼び出した場合、実行時に `NotImplementedError` を送出する代わりに、`runtime_checkable` による事前チェックを推奨する。

## エッジケース

- 複合実装: 複数のプロトコルを一つのクラスで実装する場合、`TaskDBMaintenable` 構成プロトコルとして扱う。

Parents: ARCH003
Children: IMPL008, TST008
2.9 LocalStorage SPEC009
STORE

## インターフェース

    class LocalStorage(BlobStorageMaintenable):
        def __init__(self, base_dir: str | Path): ...
        def save(self, key: str, data: bytes) -> str: ...
        def load(self, location: str) -> bytes: ...

## 振る舞い

### アトミック書き込み (`save`)

1. 指定ディレクトリ内に `tempfile.mkstemp` で一時ファイルを作成
2. データを書き込み、`flush()` および `os.fsync()` でディスク到達を保証
3. `os.replace` で最終的なパスへリネーム（アトミックな置換）

### パストラバーサル対策 (`load` / `delete`)

- 解決後の絶対パスが `base_dir` の配下にあることを `is_relative_to` で厳密にチェックし、範囲外へのアクセスを遮断する。

## パラメータ詳細

パラメータ | 説明 | 制限
--- | --- | ---
`key` | 保存ファイル名のベース | `..` や `/` を含む場合は `ValidationError`
`location` | `save` が返した識別子 | 相対パス形式を推奨

## エラーハンドリング

- ファイル不在: `load` 時にファイルがない場合 `CacheCorruptedError` を送出
- 権限エラー: `PermissionError` 発生時は、GCでの回収に委ねるため警告ログのみ出力

## エッジケース

- ディスクフル: 書き込み中に容量不足になった場合、一時ファイルを削除して `OSError` を送出
- Windowsパス: パス区切り文字として `/` と `\` の両方を検証対象とする

Parents: ARCH004
Children: IMPL009, TST009
2.10 S3Storage SPEC010
STORE

## インターフェース

    class S3Storage(BlobStorageMaintenable):
        def __init__(self, s3_uri: str, s3_opts: dict | None = None): ...

## 振る舞い

### URIベースの管理

- `s3://bucket/prefix/key.bin` 形式のURIをロケーション識別子として使用する。

### 大容量対応 (`save`)

- `boto3.upload_fileobj` を使用。5GBを超えるデータに対しては、自動的にマルチパートアップロードに切り替えられ、PutObjectの制限を回避する。

### 高速なメタデータ取得 (`get_mtime`)

- `head_object` を使用し、データ本体をダウンロードせずに最終更新時刻（LastModified）を取得する。

## パラメータ詳細

設定 | 内容
--- | ---
`s3_uri` | `s3://my-bucket/my-prefix` 形式
`s3_opts` | `boto3.client('s3', **s3_opts)` に渡される設定

## エラーハンドリング

- ライブラリ不在: `boto3` がインポートできない環境でインスタンス化を試みると `ImportError` を送出
- ネットワークエラー: `botocore.exceptions.ClientError` をキャッチし、状況に応じて `CacheCorruptedError` に変換

## エッジケース

- リージョン不一致: S3クライアントの設定とバケットのリージョンが異なる場合の動作は Boto3 のデフォルト設定に依存
- 削除の冪等性: S3 は存在しないオブジェクトの削除要求に対してエラーを返さないため、常に成功として扱う

Parents: ARCH004
Children: IMPL010, TST010
2.11 ストレージポリシー SPEC011
STORE

## インターフェース

    class StoragePolicyProtocol(Protocol):
        def should_save_as_blob(self, data: bytes) -> bool: ...

## 振る舞い

### 判定ロジック

- ThresholdStoragePolicy: `len(data)` が `threshold` を超えた場合に `True` を返す。
- WarningOnlyPolicy: 常に `False` を返すが、閾値を超えた場合にログ出力のみ行う（互換モード）。
- AlwaysBlobPolicy: 常に `True` を返す。

## パラメータ詳細

ポリシー型 | パラメータ | デフォルト | 説明
--- | --- | --- | ---
`Threshold` | `threshold` | なし | バイト単位の閾値。10MB等を推奨
`Warning` | `warning_threshold` | なし | 警告を出すサイズ閾値

## エラーハンドリング

- ポリシー内の判定で例外が発生した場合は、安全のため `False` (DB保存) とみなし、エラーをログに記録する。

## エッジケース

- ゼロバイト: 空のデータに対する挙動は各ポリシーの閾値設定に依存する（通常はDB保存）。
- 閾値ちょうどのサイズ: `len(data) > threshold` のため、閾値と等しい場合はDB保存となる。

Parents: ARCH004
Children: IMPL011, TST011
2.12 MsgpackSerializer SPEC012
SERIAL

## インターフェース

    class MsgpackSerializer(SerializerProtocol, TypeRegistryProtocol):
        def dumps(self, obj: Any) -> bytes: ...
        def loads(self, data: bytes) -> Any: ...

## 振る舞い

### シリアライズ (`dumps`)

1. オブジェクトの型を `type(obj)` で取得
2. 登録済み型またはそのサブクラス（MRO順）を探索
3. ヒットした場合、カスタムエンコーダを実行し `ExtType` としてパック
4. ヒットしない場合、MessagePack のデフォルト処理（または `SerializationError`）

### デシリアライズ (`loads`)

1. MessagePack ストリームをパース
2. `ExtType` を発見した場合、コードに対応するカスタムデコーダを呼び出す

## パラメータ詳細

設定 | デフォルト | 説明
--- | --- | ---
`max_cache_size` | `1024` | スレッドごとの型解決LRUキャッシュの最大サイズ

## エラーハンドリング

- 未登録型: シリアライズ時に対応するエンコーダが見つからない場合、`SerializationError` を送出
- デコード失敗: データ破損やコード不一致時は `SerializationError` を送出

## エッジケース

- 動的型: 名前が同じでも ID が異なる動的生成型は、LRUキャッシュによりメモリリークを防ぎつつ効率的に処理される
- スレッド安全性: `registry_generation` カウンタを使用して、スレッドローカルキャッシュの整合性を維持

Parents: ARCH005
Children: IMPL012, TST012
2.13 カスタム型登録 SPEC013
SERIAL

## インターフェース

    def register(
        code: int,
        encoder: Callable[[Any], Any],
        decoder: Callable[[Any], Any],
    ) -> Callable: ... # デコレータ形式

    def register_type(
        type_class: Type,
        code: int,
        encoder: Callable[[Any], Any],
        decoder: Callable[[Any], Any],
    ) -> None: ...

## 振る舞い

1. 重複チェック: 同一コードまたは同一クラスが既に登録されていないか確認
2. CoW更新: 現在のレジストリ辞書をコピーし、新しいエントリを追加した後に参照を差し替える
3. 世代更新: `_registry_generation` をインクリメントし、全スレッドのLRUキャッシュを無効化対象とする

## パラメータ詳細

パラメータ | 制限 | 説明
--- | --- | ---
`code` | `0` 〜 `127` | MessagePack ExtType のユーザー定義領域コード
`encoder` | 引数1、戻り値シリアライズ可能 | オブジェクトをプリミティブ構造へ変換する関数
`decoder` | 引数1、戻り値オブジェクト | プリミティブ構造からオブジェクトを復元する関数

## エラーハンドリング

- コード重複: `ValueError` を送出し、レジストリの破損を防ぐ
- 範囲外コード: 128以上のコードを指定した場合は `ValueError`

## エッジケース

- 継承: 基底クラスを登録すると、サブクラスも自動的にそのエンコーダを使用する（オーバーライド可能）

Parents: ARCH005
Children: IMPL013, TST013
2.14 BackgroundLoop SPEC014
BGIO

## インターフェース

    class _BackgroundLoop:
        def submit(self, coro: Coroutine) -> None: ...
        def stop(self) -> None: ...
        def is_running(self) -> bool: ...

## 振る舞い

1. 起動: 初回の `submit` 時にデーモンスレッドを作成し、`asyncio.run()` を開始する
2. 投入: `run_coroutine_threadsafe` を使用して、メインスレッドからコルーチンをイベントループへ投入
3. 待機: 投入されたタスクの完了は待たず、制御を即座に呼び出し元へ返す

## パラメータ詳細

内部属性 | 型 | 説明
--- | --- | ---
`_loop` | `AbstractEventLoop` | スレッド内で稼働するループ本体
`_thread` | `Thread` | `daemon=True` 設定のスレッド

## エラーハンドリング

- 投入失敗: ループ停止中にタスクが投入された場合、エラーをログ出力し無視する
- タスク内エラー: コルーチン内で発生した例外は、`on_background_error` コールバックで処理される

## エッジケース

- 多重起動: すでにスレッドが実行中の場合は、既存のスレッドを再利用する

Parents: ARCH006
Children: IMPL014, TST014
2.15 save_sync / flush / drain SPEC015
BGIO

## インターフェース

    def flush(timeout: float | None = None) -> bool: ...

    @spot.mark(save_sync=False)
    def my_func(): ...

## 振る舞い

### 同期保存 (`save_sync=True`)

- 関数終了後、DBおよびBlobへの保存が完了するまで呼び出し元をブロックする。

### 非同期保存 (`save_sync=False`)

- 保存処理を `BackgroundLoop` に委譲し、即座に関数の戻り値を返す。

### フラッシュ (`flush`)

- `_inflight` タスク（保存中）のリストを確認し、それら全てが完了するまで、またはタイムアウトまで待機する。

## パラメータ詳細

パラメータ | デフォルト | 説明
--- | --- | ---
`timeout` | `None` | `flush` の最大待機時間（秒）。`None` は無限待機
`on_background_error` | `logger.error` | 非同期保存失敗時のエラーハンドラ

## エラーハンドリング

- タイムアウト: `flush` がタイムアウトした場合、`False` を返し、一部のデータが未保存である可能性を通知する。

## エッジケース

- コンテキスト終了: `with spot:` 終了時に自動的に `flush` が呼ばれ、プロセス終了前のデータ保存を確実にする。

Parents: ARCH006
Children: IMPL015, TST015
2.16 LifecyclePolicy SPEC016
LIFE

## インターフェース

    class LifecyclePolicy:
        def add_rule(self, pattern: str, retention: Any) -> None: ...
        def resolve_with_fallback(self, func_name: str) -> float | None: ...

## 振る舞い

1. 順次照合: 登録された順にルールを走査する

2. パターンマッチ: `fnmatch.fnmatch` を使用し、関数名がパターンに合致するか判定

3. 二段階解決:

   1. 最初に「完全修飾名 (module.qualname)」で照合

   2. マッチしなければ「短縮名 (qualname)」で照合

4. 結果返却: 最初にマッチしたルールの保持期間を秒数に変換して返す

## パラメータ詳細

パラメータ例説明`pattern"my_mod.*"`, `"*_test"`Glob形式のパターン`retention"30d"`, `3600`保持期間の定義

## デフォルト保持期間

- `default_retention` パラメータで、どのルールにもマッチしない場合のデフォルト保持期間を指定できる
- `LifecyclePolicy.default()` のデフォルト値は `"30d"`（30日）
- `None` を指定すると無期限保持（従来互換）

## エラーハンドリング

- マッチなし: 全てのルールにマッチしない場合は、`default_retention` で指定された保持期間を返す（デフォルト: 30日）

## エッジケース

- 重複パターン: 先に登録されたルールが優先される。

Parents: ARCH007
Children: IMPL016, TST016
2.17 Retention 仕様 SPEC017
LIFE

## インターフェース

    def parse_retention(val: Any) -> float | None: ...

## 振る舞い

### 文字列パース

- `"7d"` (7日), `"12h"` (12時間), `"30m"` (30分), `"10s"` (10秒) 形式を秒数に変換する。大文字小文字は区別しない。

### 数値・オブジェクト

- `int`, `float`: 秒数としてそのまま扱う。
- `timedelta`: `total_seconds()` を取得。
- `None`: ポリシーに従う（または無期限）を意味する。

## パラメータ詳細 (単位)

単位 | 倍率 (秒) | 備考
--- | --- | ---
`d` | 86400 | 日数
`h` | 3600 | 時間
`m` | 60 | 分
`s` | 1 | 秒

## エラーハンドリング

- 不正な形式: パーサが理解できない形式の場合は `ValidationError` を送出する。
- 負の値: 過去の時間は指定できず、`ValidationError` となる。

## エッジケース

- FOREVER: 特別な定数 `Retention.FOREVER` (無限大の秒数) を提供し、ポリシーに関わらず永続化を指示できる。

Parents: ARCH007
Children: IMPL017, TST017
2.18 TokenBucket (GCRA) SPEC018
LIMIT

## インターフェース

    class TokenBucket(LimiterProtocol):
        def consume(self, cost: float = 1.0) -> None: ...
        async def consume_async(self, cost: float = 1.0) -> None: ...

## 振る舞い

### GCRAアルゴリズム

- 各リクエストに対して「次回実行可能な理論時刻 (TAT: Theoretical Arrival Time)」を計算・更新する。
- 現在時刻が `TAT - 許容バースト` 以前の場合、リクエストを拒否または待機させる。

### 同期 vs 非同期

- `consume`: 待機が必要な場合は `time.sleep()` でブロック。
- `consume_async`: 待機が必要な場合は `asyncio.sleep()` で非占有待機。

## パラメータ詳細

設定 | デフォルト | 説明
--- | --- | ---
`tokens_per_minute` | 必須 | 1分間に許可する平均リクエスト数（コスト合計）
`max_burst` | `1.0` | 許容されるバースト数（トークン単位）

## エラーハンドリング

- 即時拒否モード: 待機を許容しない設定でレート超過した場合、`RateLimitError` (ValueErrorのサブクラス) を送出する。

## エッジケース

- 高コストリクエスト: `max_burst` を超えるコストを持つ単一リクエストは、常に拒否される。
- アイドル時間: 長時間リクエストがない場合も、`TAT` は現在時刻より過去に一定以上離れないよう補正される（過度なバーストの防止）。

Parents: ARCH008
Children: IMPL018, TST018
2.19 HookBase / ThreadSafeHookBase SPEC019
HOOK

## インターフェース

    class HookBase:
        def pre_execute(self, ctx: PreExecuteContext) -> None: ...
        def on_cache_hit(self, ctx: CacheHitContext) -> None: ...
        def on_cache_miss(self, ctx: CacheMissContext) -> None: ...

## 振る舞い

1. フック呼出: キャッシュエンジンの各ステート（実行前、ヒット、ミス）で登録されたフックを順番に実行する
2. スレッド安全化: `ThreadSafeHookBase` を継承した場合、メタクラスが自動的に各メソッドを `with self._lock:` で包む

## パラメータ詳細 (Context)

コンテキスト | 保持データ
--- | ---
`PreExecuteContext` | `func`, `args`, `kwargs`, `cache_key`
`CacheHitContext` | `cache_key`, `result`, `metadata`
`CacheMissContext` | `cache_key`, `result`, `execution_time`

## エラーハンドリング

- フック内例外: フックの実行中に発生した例外はキャッチされ、エラーログとして出力されるが、主処理（関数の実行結果）は中断されない。

## エッジケース

- フックの順序: `hooks=[h1, h2]` と指定した場合、`h1` -> `h2` の順で実行される。

Parents: ARCH009
Children: IMPL019, TST019
2.20 MaintenanceService SPEC020
MAINT

## インターフェース

    class MaintenanceService:
        def clean_garbage(self, grace_period: int = 3600) -> GarbageStats: ...
        def prune(self, days: int, func_name: str | None = None) -> int: ...
        def clear(self, func_name: str | None = None) -> int: ...

## 振る舞い

### ガベージコレクション (`clean_garbage`)

1. DB Flush: 保存中のタスクをDBへ反映させる
2. 期限切れ削除: `delete_expired()` でDBから古いタスクを除去
3. 孤立Blobスキャン: DB内の全 `blob_key` とストレージ内の全ファイルを比較
4. 物理削除: 参照のないBlobのうち、作成から `grace_period` 以上経過したものを削除
5. 構造清掃: ストレージ内の空ディレクトリを削除

## パラメータ詳細

パラメータ | デフォルト | 説明
--- | --- | ---
`grace_period` | `3600` | 孤立Blobを削除するまでの猶予期間（秒）。並行実行中の保存を保護する
`days` | なし | `prune` で削除対象とする経過日数

## エラーハンドリング

- 削除失敗: 個別のファイル削除失敗は警告ログに留め、全体の処理は継続する。
- DBロック: メンテナンス中にDBがロックされた場合は、タイムアウトまで待機し失敗させる。

## エッジケース

- 大規模ストレージ: Blobの列挙はジェネレータで行い、メモリ使用量を抑制する。
- 名前空間: `clear(func_name)` は前方一致ではなく、完全一致（または glob）で判定する。

Parents: ARCH010
Children: IMPL020, TST020
2.21 CLIコマンド SPEC021
CLI

## インターフェース

    beautyspot list [--db DB_PATH]
    beautyspot gc [--name PROJECT_NAME] [--force]
    beautyspot stats
    beautyspot ui

## 振る舞い

1. コンテキスト解決: `--db` 指定がない場合、デフォルトの `.beautyspot/` ディレクトリを探索する
2. コマンド実行: `MaintenanceService` または `Spot` インスタンスを生成し、対応するメソッドを呼び出す
3. フォーマット出力: `Rich` ライブラリを使用して、結果をテーブルやプログレスバーで表示する

## サブコマンド詳細

コマンド | 説明
--- | ---
`list` | 保存されているキャッシュキーと関数名の一覧を表示
`gc` | 期限切れタスクと孤立Blobのクリーンアップを実行
`stats` | キャッシュヒット率やストレージ使用量の統計を表示
`ui` | ターミナルベースのインタラクティブダッシュボードを起動

## エラーハンドリング

- DB不在: 指定されたパスにDBがない場合は、エラーメッセージを表示して終了する。
- 権限不足: ファイル削除権限がない場合は、スキップされた旨を表示する。

## エッジケース

- 非対話環境: CI等での実行を考慮し、色の無効化や確認プロンプトのスキップ（`--force`）に対応する。

Parents: ARCH011
Children: IMPL021, TST021
2.22 ファクトリ関数と Protocol SPEC022
DI

## インターフェース

    def Spot(
        name: str = "default",
        storage_path: str | Path | None = None,
        serializer: SerializerProtocol | None = None,
        limiter: LimiterProtocol | None = None,
        # ...その他の依存
    ) -> core.Spot: ...

## 振る舞い

1. パス解決: `storage_path` が未指定の場合、`.beautyspot/` 以下のプロジェクト名ディレクトリを使用する
2. DB初期化: `SQLiteTaskDB` を生成し、マイグレーションを実行
3. ストレージ構成: URI（`s3://`等）を判定し、適切な `BlobStorage` 実装を生成
4. インスタンス化: 全てのコンポーネントを `core.Spot` のコンストラクタに注入して返す

## パラメータ詳細

パラメータ | デフォルト値の導出
--- | ---
`db` | `.beautyspot/{name}.db`
`blobs` | `.beautyspot/blobs/{name}/`
`serializer` | `MsgpackSerializer()`
`policy` | `WarningOnlyPolicy` (閾値10MB)

## エラーハンドリング

- ディレクトリ作成失敗: 権限等の理由でストレージディレクトリが作成できない場合は、早期に `OSError` を送出する。

## エッジケース

- グローバルシングルトン: 同じ `name` で複数回呼び出した場合も、原則として新しいインスタンスを返すが、DBファイルへの接続は共有される（SQLiteのWALモードを活用）。

Parents: ARCH012
Children: IMPL022, TST022
2.23 Thundering Herd Protection SPEC023
HERD

## インターフェース

    class CacheManager:
        def get_or_create_inflight(
            self, key: str, is_async: bool
        ) -> ExecutionState: ...

## 振る舞い

### 同時実行制御

1. キャッシュキーをキーとした `_inflight` 辞書をチェックする
2. 存在しない場合: `ExecutionState` を新規作成し、自分が「実行者 (Executor)」となる
3. 存在する場合: 既存の `ExecutionState` を返し、自分は「待機者 (Waiter)」となる
4. 完了通知: 実行者が処理を終えたら、`ExecutionState` の Event (または Future) をセットし、結果を共有する

### 強参照によるインフライト状態の保持

#### 目的

実行中のタスク状態（イベント・結果・Future）が GC によって消失しないよう、 `_inflight` 辞書を通じて強参照で保持する。WeakRef は使用しない。

#### データ構造

`_inflight` 辞書はキャッシュキーをキーとし、以下の3要素タプルを値として保持する:

    _inflight: dict[str, tuple[threading.Event, list[asyncio.Future], list]]

要素 | 型 | 役割
--- | --- | ---
`event` | `threading.Event` | 同期待機者への完了通知シグナル
`futures` | `list[asyncio.Future]` | 非同期待機者への結果配信チャネル
`result_box` | `list` | 結果の共有ボックス。`[(success: bool, value: Any)]` 形式

#### ライフサイクル

1. 作成: `wait_herd_sync` / `wait_herd_async` で `_inflight` にキーが存在しない場合、 `_inflight_lock` を保持した状態で新規タプルを挿入する
2. 保持: 実行者が処理を完了するまで、`_inflight` 辞書がタプルへの唯一の管理参照を保持する。 待機者はロック取得時にタプル要素への参照を取得するが、`_inflight` エントリが正規の所有者である
3. 解放: `notify_and_cleanup_inflight` で以下の順序で解放する: a. `_inflight_lock` を取得し、エントリの同一性を `event` の identity (`is`) で確認する b. 辞書からエントリを削除する（`del _inflight[cache_key]`） c. ロックを解放した後、`event.set()` で同期待機者に通知する d. `futures` リスト内の各 `asyncio.Future` に結果またはエラーを伝播する

#### 不変条件

- `_inflight` 辞書への挿入・削除は常に `_inflight_lock` の保護下で行う
- 1つのキャッシュキーに対して `_inflight` 内には高々1つのエントリしか存在しない
- エントリの削除は、挿入時の `event` と同一オブジェクト（`is` 比較）を持つスレッドのみが行える
- `result_box` への書き込みは実行者スレッドのみが行い、待機者は読み取り専用とする
- `futures` リストへの追加は `_inflight_lock` の保護下でのみ行う

#### GC安全性

`_inflight` 辞書は `CacheManager` インスタンスの属性であり、`CacheManager` が 生存している限り、全てのインフライトエントリは GC の対象にならない。 `CacheManager` 自体は `Spot` インスタンスが所有するため、`with spot:` ブロック内での 安全性が保証される。

## パラメータ詳細

内部定数 | 値 | 説明
--- | --- | ---
`HERD_TIMEOUT` | `300.0` | 待機者が実行者の完了を待つ最大秒数
`HERD_MAX_RETRIES` | `3` | 実行者が失敗またはタイムアウトした際のリトライ回数

## エラーハンドリング

- 実行者失敗: 実行者が例外を投げた場合、待機者全員にも同じ例外が伝播される。
- タイムアウト: タイムアウト時は待機を解除し、自らが実行者となって再試行（またはエラー）を試みる。

## エッジケース

- 非同期混在: 同一キーに対して同期実行と非同期実行が混在する場合も、スレッドセーフなロックにより一つの実行に絞り込まれる。

Parents: ARCH013
Children: IMPL023, TST023
2.24 Blobストレージ抽象インターフェース SPEC024
STORE

## インターフェース

    class BlobStorageBase(ABC):
        @abstractmethod
        def save(self, key: str, data: ReadableBuffer) -> str: ...
        @abstractmethod
        def load(self, location: str) -> bytes: ...
        @abstractmethod
        def delete(self, location: str) -> None: ...
        @abstractmethod
        def list_keys(self) -> Iterator[str]: ...
        @abstractmethod
        def get_mtime(self, location: str) -> float: ...

## 振る舞い

- save: データを永続化し、一意な識別子（パスやURI）を返す。
- load: 識別子を基にバイトデータを復元する。
- delete: 識別子を基にデータを削除する。存在しない場合は無視する（冪等）。
- list_keys: メンテナンス用に、保存されている全オブジェクトの識別子を列挙する。
- get_mtime: 最終更新時刻を POSIX タイムスタンプで返す。

## パラメータ詳細

パラメータ | 型 | 説明
--- | --- | ---
`key` | `str` | キャッシュキー（通常はハッシュ値）
`data` | `bytes` / `memoryview` | シリアライズ済みバイナリ
`location` | `str` | 実体へのポインタ（ファイルパス、S3 URI等）

## エラーハンドリング

- 実装クラスは、データの欠損やアクセス不可時に `CacheCorruptedError` を送出すべきである。

## エッジケース

- バイナリ互換: `ReadableBuffer` を受け入れることで、`memoryview` 等を使用したゼロコピー書き込みを可能にする。

Parents: ARCH004
Children: IMPL024, TST024

# TST Specification

## 1.0 序文 TST025

本ドキュメントは、『beautyspot』のテスト計画および検証内容を定義する。

## 2.0 詳細仕様 TST026

各機能が仕様通りに動作することを確認するための検証観点およびテストシナリオを定義する。

2.1 mark デコレータテスト TST001
CACHE

## 目的

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

## 検証観点

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

Parents: SPEC001
Ref: tests/integration/core/test_basic.py
2.2 cached_run テスト TST002
CACHE

## 目的

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

## 検証観点

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

Parents: SPEC002
Ref: tests/integration/core/test_cached_run.py
2.3 同期・非同期サポートテスト TST003
CACHE

## 目的

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

## 検証観点

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

Parents: SPEC003
Ref: tests/integration/core/test_async_save.py
2.4 SHA-256 キャッシュキーテスト TST004
KEY

## 目的

キャッシュキーはキャッシュの同一性判定の基盤であり、ハッシュが不安定だとキャッシュミスの頻発（性能劣化）や誤ヒット（データ不整合）を引き起こす。Python バージョンやプラットフォームに依存しないキー生成の安定性を保証する。

## 検証観点

- 決定性: 同一の `func_identifier:input_id[:version]` 入力に対して、複数回の呼び出しで常に同じ SHA-256 ハッシュが生成されること
- 一意性: 異なる引数から異なるキーが生成されること（衝突しないこと）
- 構造: キーが `module.qualname` 形式の func_identifier を含む正しい構造であること
- version 反映: version 文字列がキーに含まれ、version 変更でキーが変わること

Parents: SPEC004
Ref: tests/unit/test_cachekey.py
2.5 再帰的正規化テスト TST005
KEY

## 目的

`canonicalize()` は引数の型ごとに正規化ルールを適用し、論理的に同値な入力が同じキャッシュキーを生成することを保証する。正規化の不備は「同じ入力なのにキャッシュミス」または「異なる入力なのに誤ヒット」という致命的なバグに直結する。

## 検証観点

- dict の順序非依存: `{"a": 1, "b": 2}` と `{"b": 2, "a": 1}` が同一キーを生成すること
- 型タグによる区別: `[1, 2]` (list) と `(1, 2)` (tuple) が異なるキーを生成すること（型タグ付き正規化）
- set/frozenset: 要素の挿入順序に依存しないこと
- deque/defaultdict/OrderedDict: コレクション型ごとに正しく正規化されること
- Enum: 値ではなく `型名.メンバー名` で正規化されること
- numpy 配列: `shape + dtype + tobytes` による正規化で、同値配列が同一キーを生成すること
- Pydantic モデル: モデルの dict 表現で正規化されること
- bool vs int の区別: `True` と `1` が異なるキーを生成すること（Python では `True == 1` だが区別が必要）
- 再帰的処理: ネストされたデータ構造（dict の中の list の中の set 等）が正しく正規化されること

Parents: SPEC005
Ref: tests/unit/test_cachekey.py
2.6 KeyGenPolicy テスト TST006
KEY

## 目的

`KeyGenPolicy` は引数ごとのキー生成戦略をカスタマイズする機構であり、ログレベルやデバッグフラグのような非決定的引数をキーから除外したり、ファイルパスの代わりにファイル内容でキーを生成するユースケースを支える。戦略の誤適用はキャッシュの正確性を根本から損なう。

## 検証観点

- KeyGen.ignore(): 指定された引数がキャッシュキーの計算から除外され、その引数の値を変えてもキャッシュヒットすること
- KeyGen.map() + FILE_CONTENT: ファイルパス引数に対し、パス文字列ではなくファイル内容のハッシュがキーに使われること。同一内容の別パスファイルが同一キーを生成すること
- KeyGen.path_stat(): ファイルの mtime/size に基づくキー生成が機能すること
- bind() 連携: `KeyGenPolicy.bind(func)` が関数シグネチャ（位置引数・キーワード引数・デフォルト値）を正しく解釈すること
- @mark 統合: `@spot.mark(keygen=KeyGen.ignore("verbose"))` のようにデコレータ経由で戦略が正しく適用されること

Parents: SPEC006
Ref: tests/unit/test_cachekey.py
2.7 SQLiteTaskDB テスト TST007
DB

## 目的

`SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす。WALモード・専用ライタースレッドという独自アーキテクチャの信頼性を保証する。

## 検証観点

- スキーマ初期化: 新規DBファイルに対してテーブルとインデックスが正しく作成されること。既存DBへの自動マイグレーション（content_type, version, result_data, func_identifier, expires_at カラム追加）が冪等に動作すること
- CRUD操作: タスクの作成・取得・更新・削除が正しく動作し、存在しないキーへのアクセスが適切にハンドルされること
- 読み取り接続リカバリ: スレッドローカルな読み取り接続がクローズされた場合に自動的に再作成され、接続リークが発生しないこと
- ライタースレッドのタイムアウト: 書き込みキューのタイムアウト時に適切なエラーが返り、ライタースレッドがデッドロックしないこと
- GC耐性: ガベージコレクション実行後もDBファイルが存続し、有効なタスクが失われないこと

Parents: SPEC007
Ref: tests/unit/test_db_robustness.py
2.8 DB プロトコル階層テスト TST008
DB

## 目的

`TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL 等）の差し替えを可能にする拡張ポイントである。インターフェース契約が不明確だと、カスタム実装が実行時に予期しないエラーを起こす。

## 検証観点

- インターフェース準拠: `TaskDBCore` の必須メソッド（save, load, delete 等）を実装したクラスが `isinstance` チェックをパスすること
- TaskDBBase 拡張: `TaskDBBase` がメンテナンス系メソッド（prune, stats 等）のデフォルト実装を提供し、サブクラスが必須メソッドのみの実装で動作すること
- デフォルト動作: メンテナンスメソッドのデフォルト実装が安全なno-op（空リスト返却等）であること
- カスタム実装の注入: `bs.Spot(db=CustomDB())` でプロトコル準拠のカスタムDBが正常に動作すること

Parents: SPEC008
Ref: tests/unit/test_db_writer_queue.py
2.9 LocalStorage テスト TST009
STORE

## 目的

`LocalStorage` はBlobデータのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性が発生しうる。アトミック書き込みとセキュリティバリデーションの正確性を保証する。

## 検証観点

- ワークスペース初期化: `base_dir` が存在しない場合に自動作成され、`.gitignore` が配置されること
- アトミック書き込み: `tempfile.mkstemp` → `fsync` → `os.replace` のフローで、書き込み中のクラッシュや並行アクセスでファイルが破損しないこと
- 並行書き込み一貫性: 複数スレッドから同一キーへの同時書き込みで、最終状態が正しい完全なデータであること
- パストラバーサル防止: キーに `..` や `/` を含む場合に `ValidationError` が送出され、`base_dir` 外へのアクセスが不可能であること。`load()` でも `is_relative_to` 検証が機能すること
- 一時ファイルクリーンアップ: `clean_temp_files()` が猶予期間超過の `.spot_tmp` ファイルを削除し、猶予期間内のファイルは保持すること
- 冪等削除: `delete()` が存在しないキーに対してもエラーを送出しないこと

Parents: SPEC009
Ref: tests/integration/storage/test_local.py
2.10 S3Storage テスト TST010
STORE

## 目的

`S3Storage` はクラウド環境での大規模Blob保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが存在する。boto3 オプショナル依存のガード処理も含め、クラウドストレージ統合の信頼性を検証する。

## 検証観点

- 基本CRUD: S3互換ストレージへの `save` / `load` / `delete` / `list_keys` が正しく動作すること
- オプショナル依存ガード: boto3 がインストールされていない環境で `import` 時にわかりやすいエラーメッセージが表示されること
- エラーハンドリング: バケット不在、権限不足、ネットワークエラー時に適切な例外が送出されること
- 注記: boto3 のモック戦略（moto 等）を使用したテストの実装を推奨

Parents: SPEC010
Ref: tests/integration/storage/test_s3.py
2.11 ストレージポリシーテスト TST011
STORE

## 目的

ストレージポリシーは「データをDB直接保存するか、Blobストレージに分離するか」を決定する戦略レイヤーである。閾値判定の誤りはDBの肥大化（性能劣化）や不要なBlob分離（オーバーヘッド増加）を招く。3種のポリシー実装がそれぞれの契約を正しく満たすことを検証する。

## 検証観点

- ThresholdStoragePolicy: 閾値未満のデータで `False`、閾値以上のデータで `True` を返すこと。境界値（ちょうど閾値サイズ）での挙動が明確であること
- WarningOnlyPolicy: 常に `False` を返しDB直接保存を選択すること。ただし閾値超過時に WARNING レベルのログが出力されること
- AlwaysBlobPolicy: データサイズに関係なく常に `True` を返すこと
- Spot 統合: `@spot.mark(save_blob=True/False)` の明示指定がポリシー判定より優先されること。`save_blob=None`（デフォルト）時にポリシーの `should_save_as_blob()` が呼ばれること

Parents: SPEC011
Ref: tests/unit/test_storage_policy.py
2.12 MsgpackSerializer テスト TST012
SERIAL

## 目的

`MsgpackSerializer` はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失（デシリアライズ不能）やサイレントなデータ劣化（精度低下等）に直結する。スレッドセーフ性の欠如はマルチスレッド環境での競合状態を引き起こす。

## 検証観点

- ラウンドトリップ: プリミティブ型（int, float, str, bytes, None, bool）と基本コレクション（list, dict）が `dumps` → `loads` で元の値と一致すること
- Pydantic モデル: Pydantic モデルが dict に変換されてシリアライズされ、デシリアライズ後に元のモデルが復元されること
- numpy 配列: ndarray の shape, dtype, 値が保持されること。float64 等の精度が劣化しないこと
- スレッドセーフ LRU キャッシュ: サブクラス解決のキャッシュがスレッドローカルであり、複数スレッドからの同時アクセスで競合しないこと
- ExtType 拡張: カスタム型が ExtType (code 0-127) で正しく登録・解決されること

Parents: SPEC012
Ref: tests/unit/test_serializer.py
2.13 カスタム型登録テスト TST013
SERIAL

## 目的

カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッシュ保存失敗を引き起こし、ユーザーの既存コードとの統合を阻害する。デコレータ方式と命令方式の両方の登録パスを検証する。

## 検証観点

- デコレータ登録: `@spot.register(code=N, encoder=..., decoder=...)` でクラスが登録され、そのインスタンスのシリアライズ/デシリアライズが正常に動作すること
- 命令的登録: `spot.register_type(MyClass, code=N, ...)` でも同等の登録が行われること
- Pydantic factory デコーダ: Pydantic モデルの `model_validate` をデコーダとして使用する場合に、バリデーション付きの復元が正しく動作すること
- ネスト構造: カスタム型を含む dict、カスタム型のリスト、カスタム型をフィールドに持つカスタム型など、複雑な入れ子構造が正しくエンコード/デコードされること
- code 衝突: 同一 code を重複登録した場合の挙動が明確であること

Parents: SPEC013
Ref: tests/unit/test_type_registry.py
2.14 BackgroundLoop テスト TST014
BGIO

## 目的

`_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコルーチン受け渡しは競合状態やデッドロックが発生しやすく、シャットダウン時のタスク消失はデータロスに直結する。

## 検証観点

- 保存の正確性: `submit(coro)` で投入したコルーチンが正しく実行され、結果がDBとストレージに反映されること
- 直列化保証: 同一キーへの保存が投入順に処理され、最終状態が最後に投入されたデータであること
- ゾンビスレッド防止: シャットダウン後にアクティブタスクが `drain_timeout` 秒以内に完了すること。タイムアウト後は強制終了されること
- atexit 安全網: プロセス終了時に atexit ハンドラが発火し、未完了タスクの drain が試行されること
- 新規受付停止: シャットダウン後の `submit()` 呼び出しが受け付けられないこと

Parents: SPEC014
Ref: tests/integration/core/test_background_loop.py
2.15 save_sync / flush / drain テスト TST015
BGIO

## 目的

`save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユーザーが制御する手段である。`save_sync=False` 使用時にデータが失われないことと、コンテキストマネージャによる確実なフラッシュを保証する。

## 検証観点

- save_sync=True（デフォルト）: キャッシュ保存が呼び出しスレッドで同期的に完了し、関数リターン時点でデータが永続化されていること
- save_sync=False: キャッシュ保存がバックグラウンドに委譲され、関数がレイテンシなしでリターンすること。保存が最終的に完了すること
- flush(): `spot.flush(timeout)` 呼び出しで全ペンディング保存が完了するまで待機すること。タイムアウト時の挙動が明確であること
- コンテキスト exit での drain: `with spot:` ブロック終了時に未完了の保存が自動的に drain されること
- コンテキストマネージャの再利用: 同一 spot インスタンスが複数の `with` ブロックで再利用でき、各ブロック終了時に適切に drain されること
- バックグラウンド保存失敗: 非同期保存が例外を送出した場合に `on_background_error` コールバックが呼ばれ、メインスレッドには影響しないこと

Parents: SPEC015
Ref: tests/integration/core/test_exit_drain.py
2.16 LifecyclePolicy テスト TST016
LIFE

## 目的

`LifecyclePolicy` はキャッシュデータの保持期間を関数名パターンで制御する。パターンマッチングの不備は、重要なキャッシュの意図しない削除（データロス）や不要なキャッシュの蓄積（ディスク圧迫）を引き起こす。

## 検証観点

- パターンマッチング: glob パターン（`"train_*"`, `"*.preprocess"` 等）がルールの `func_pattern` として正しく機能し、マッチした関数に対して指定の `Retention` が適用されること
- ルール優先順位: 複数ルールがマッチした場合に、リスト順（先勝ち or 後勝ち）でルールが決定されること
- resolve_with_fallback: 完全修飾名（`module.qualname`）でルールが見つからない場合に、短縮名（`qualname` のみ）でフォールバック検索が行われること
- デフォルト保持期間: どのルールにもマッチしない関数に対して `default_retention` で指定された保持期間（デフォルト: 30日）が適用されること
- 無期限保持の互換性: `default_retention=None` を指定した場合に無期限保持（`None`）が返されること

Parents: SPEC016
Ref: tests/unit/test_lifecycle.py
2.17 Retention テスト TST017
LIFE

## 目的

`Retention` はキャッシュの有効期間をユーザーフレンドリーな形式で指定する値オブジェクトである。パースの不備は意図しない保持期間（例: "7d" を 7秒と誤解釈）を招き、重要なキャッシュの早期削除やディスクの際限ない肥大化に繋がる。

## 検証観点

- 文字列パース: `"7d"` → 7日、`"12h"` → 12時間、`"30m"` → 30分、`"10s"` → 10秒として正しく解釈されること
- timedelta 入力: `timedelta(days=7)` がそのまま保持期間として受け入れられること
- 秒数入力: 整数・浮動小数点数が秒として解釈されること
- FOREVER シングルトン: `Retention.FOREVER` が無期限保持を表し、シングルトンであること（`is` 比較が成立）
- バリデーション: 負値（`"-1d"`）やゼロ（`"0s"`）が `ValidationError` を送出すること。不正な形式（`"abc"`, `""` 等）も拒否されること

Parents: SPEC017
Ref: tests/unit/test_lifecycle_extended.py
2.18 TokenBucket テスト TST018
LIMIT

## 目的

`TokenBucket` はAPI呼び出しやリソースアクセスのレート制限を実現する。レートリミッターの不具合はAPIの過負荷（制限が甘い場合）やスループットの不必要な低下（制限が厳しすぎる場合）を招く。GCRA アルゴリズムのスムーズなレート制御を検証する。

## 検証観点

- 初期化バリデーション: rate ≤ 0 や capacity ≤ 0 などの不正パラメータが拒否されること
- max_cost 超過拒否: 1回のリクエストコストがバケット容量を超える場合に即座に拒否されること（無限待機にならないこと）
- GCRA 待機動作: バケットが空の状態でリクエストした場合に、トークン補充まで適切な時間だけ待機すること。待機時間がレートに基づいて正確であること
- バースト許容: 容量分のリクエストがバーストで処理でき、超過分からスロットリングが開始されること

Parents: SPEC018
Ref: tests/unit/test_limiter.py
2.19 Hook テスト TST019
HOOK

## 目的

Hook システムはキャッシュライフサイクルへのユーザー拡張ポイント（ロギング、メトリクス収集、監査等）であり、フック内の不具合がメインの関数実行に影響を与えないことが最重要の契約である。また `ThreadSafeHookBase` の自動ロック機構の正確性を保証する。

## 検証観点

- ライフサイクル呼び出し順: キャッシュミス時に `pre_execute` → 関数実行 → `on_cache_miss` の順でフックが呼ばれること。キャッシュヒット時に `pre_execute` → `on_cache_hit` の順で呼ばれること
- コンテキスト情報: 各フックに渡されるコンテキスト（`PreExecuteContext`, `CacheHitContext`, `CacheMissContext`）が正しい情報（func_name, args, result 等）を含むこと
- 例外隔離: フック内で例外が発生しても、関数の実行結果には影響せず、ERROR ログにのみ記録されること
- 状態保持: フックインスタンスが呼び出し間で状態（カウンタ等）を保持できること
- ThreadSafeHookBase: `__init_subclass__` によりユーザー定義メソッドが自動的に RLock でラップされ、複数スレッドからの同時呼び出しで競合状態が発生しないこと

Parents: SPEC019
Ref: tests/integration/core/test_hooks.py
2.20 MaintenanceService テスト TST020
MAINT

## 目的

`MaintenanceService` は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用者の誤判断を招き、メンテナンス操作の不備はデータの意図しない削除に繋がる。

## 検証観点

- タスク詳細取得: 存在するタスクIDに対して正しいメタデータ（func_name, input_key, result, created_at 等）が返却されること
- 存在しないタスク: 不在のタスクIDに対して `None` が返却され、例外が送出されないこと
- 統計情報: キャッシュの件数、サイズ、関数ごとの分布など統計情報が正確に算出されること
- クリア操作: 指定条件に合致するタスクのみが削除され、他のタスクが影響を受けないこと
- GC 操作: 有効期限切れタスクの削除と孤立Blobファイルのクリーンアップが正しく連動すること

Parents: SPEC020
Ref: tests/unit/test_maintenance.py
2.21 CLI テスト TST021
CLI

## 目的

CLI はユーザーがキャッシュの状況確認・管理を行う主要インターフェースであり、コマンドの不具合はユーザー体験と運用効率に直接影響する。`CliRunner` によるコマンド実行と出力の正確性を E2E で検証する。

## 検証観点

- list コマンド: キャッシュ済みタスクの一覧が正しく表示されること。DBが空の場合に適切なメッセージが表示されること
- show コマンド: 指定タスクの詳細情報（func_name, input_key, created_at, result サイズ等）が正しく表示されること
- stats コマンド: キャッシュ統計（総件数、関数別件数、ストレージ使用量等）が正確に表示されること
- clear コマンド: 確認プロンプト後にキャッシュが削除されること。`--force` フラグでプロンプトがスキップされること
- gc / prune コマンド: 有効期限切れタスクの削除と孤立Blobの清掃が正しく実行されること
- E2E パイプライン: `@mark` でキャッシュ保存 → `beautyspot list` で確認 → `beautyspot clear` で削除、という一連のワークフローが正常に動作すること
- エラー時の終了コード: 不正な引数やDB不在時に非ゼロの終了コードが返ること

Parents: SPEC021
Ref: tests/integration/cli/test_cli.py
2.22 ファクトリ関数・DI テスト TST022
DI

## 目的

`bs.Spot()` ファクトリ関数は全コンポーネントの DI 配線を担う唯一のパブリックエントリポイントであり、デフォルト構成の正確性とカスタム実装の差し替え可能性がシステム全体の柔軟性と正確性を決定する。

## 検証観点

- デフォルト構成: 引数未指定時に `SQLiteTaskDB`, `LocalStorage`, `MsgpackSerializer`, `TokenBucket`, `WarningOnlyPolicy` が自動注入されること。ワークスペースディレクトリ（`.beautyspot/`）が自動作成されること
- カスタム DB 注入: `TaskDBBase` / `TaskDBCore` プロトコル準拠のカスタムDBを `db=` で注入し、正常にキャッシュ操作が行えること
- カスタム Storage 注入: `BlobStorageBase` 準拠のモック Storage を `storage_backend=` で注入し、Blob保存が委譲されること
- カスタム Serializer 注入: `SerializerProtocol` 準拠のカスタム Serializer を注入し、シリアライズ処理が差し替わること
- 型推論: `bs.Spot()` の戻り値型が pyright / mypy で正しく推論されること
- Protocol 不準拠の拒否: 必須メソッドを欠くオブジェクトを注入した場合に型チェックエラーまたは実行時エラーが発生すること

Parents: SPEC022
Ref: tests/integration/core/test_dependency_injection.py
2.23 Thundering Herd テスト TST023
HERD

## 目的

Thundering Herd Protection は、同一キーへの並行リクエストが一斉に関数を実行する「Thundering Herd」問題を防ぐ。この保護が不完全だと、重い計算やAPI呼び出しが不要に多重実行され、リソース浪費やレートリミット超過を招く。並行性バグはテスト困難であるため、複数の観点から網羅的に検証する。

## 検証観点

- 同期並行: 複数スレッドが同一キーで同時にキャッシュミスした場合、関数が1回のみ実行され、全スレッドが同じ結果を受け取ること
- 非同期並行: 複数の `asyncio.Task` が同一キーで同時にキャッシュミスした場合も、関数が1回のみ実行されること
- 例外伝播: 実行者（executor）の関数が例外を送出した場合、全待機スレッド/タスクに同じ例外が伝播すること。例外後に `_inflight` エントリがクリーンアップされること
- タイムアウト: 待機が `HERD_TIMEOUT`（300秒）を超過した場合にタイムアウト処理が発動し、無限待機にならないこと
- リトライ: `HERD_MAX_RETRIES`（3回）の範囲でリトライが行われ、上限超過で最終エラーが返ること
- CacheManager モック: `_inflight` 辞書の状態遷移（エントリ追加→結果セット→エントリ削除）が正しい順序で行われること

Parents: SPEC023
Ref: tests/integration/core/test_thundering_herd.py
2.24 BlobStorageBase インターフェース契約テスト TST024
STORE

## 目的

`BlobStorageBase` は全ストレージバックエンド（LocalStorage, S3Storage, サードパーティ）が 準拠すべき抽象契約を定義する。この契約が正しく機能しないと、キャッシュデータの 保存・復元・削除・メンテナンス（GC）の全てが破綻する。 `LocalStorage` を具象実装として使い、インターフェース契約を網羅的に検証する。

## 検証観点

- save 基本動作: データを永続化し、一意な識別子（文字列）を返すこと
- ReadableBuffer 互換: `bytes`, `bytearray`, `memoryview` の全てを受け入れること（ゼロコピー書き込み）
- save 上書き: 同一キーに再保存するとデータが更新されること
- 空データ: 空バイト (`b""`) を正常に保存・復元できること
- load ラウンドトリップ: save した内容を load で完全に復元できること
- load 欠損時エラー: 存在しないロケーションへの load は `CacheCorruptedError` を送出すること
- delete 基本動作: 保存済みデータを削除し、以後の load がエラーになること
- delete 冪等性: 存在しないロケーションや二重削除でエラーにならないこと（冪等）
- list_keys 空: データ未保存時は空イテレータを返すこと
- list_keys 列挙: save したアイテムが list_keys に含まれること
- list_keys 削除後: delete 後は list_keys に含まれないこと
- get_mtime 正確性: POSIX タイムスタンプ（float）を返し、保存直後は現在時刻に近いこと
- get_mtime 欠損時エラー: 存在しないロケーションでエラーを送出すること

Parents: SPEC024
Ref: tests/integration/storage/test_blob_storage_base.py

# IMPL Specification

## 1.0 序文 IMPL025

本ドキュメントは、『beautyspot』の実装記録（ジャーナル）を保持する。

## 2.0 詳細仕様 IMPL026

ソースコードの実装構造、設計判断の根拠、および技術的負債について記録する。

2.1 mark デコレータ実装 IMPL001
CACHE

## 実装概要

`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回付ける）時の検出・警告が未実装

Parents: SPEC001
Ref: src/beautyspot/core.py
2.2 cached_run 実装 IMPL002
CACHE

## 実装概要

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

## 設計判断

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

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

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

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

## 実装メモ

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

Parents: SPEC002
Ref: src/beautyspot/core.py
2.3 同期・非同期実行エンジン IMPL003
CACHE

## 実装概要

`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()` でコルーチンとして投入される

Parents: SPEC003
Ref: src/beautyspot/core.py
2.4 SHA-256 キャッシュキー実装 IMPL004
KEY

## 実装概要

`KeyGen` クラスの静的メソッド群がキャッシュキー生成を担う。 `_default(args, kwargs)` が主要なエントリポイントで、引数を `canonicalize()` で正規化 → msgpack でシリアライズ → SHA-256 でハッシュする。 最終キーは `func_identifier:input_id[:version]` 形式の文字列を SHA-256 した値。

## 設計判断

### SHA-256 の採用

MD5 より衝突耐性が高く、Python 標準ライブラリの `hashlib` で利用可能。 キャッシュキーとして64文字の hex 文字列は十分にコンパクトで、 DB のインデックスとしても効率的。

### msgpack 経由のシリアライズ

正規化結果を直接 `str()` でハッシュする方式と比較し、 msgpack はバイナリ表現が安定しており、Python バージョン間での `repr()` の差異に影響されない。

### ファイルベースの戦略

`from_file_content()` はファイルを 65KB チャンクで読み取り、 拡張子をハッシュに含める。大きなファイルでもメモリ効率が良い。 `from_path_stat()` は mtime + size のみでハッシュし、 ファイル内容の読み取りを避ける高速版。ファイル不在時は `"MISSING_{filepath}"` を返し、不在自体をキーに反映する。

## 実装メモ

- `hash_items()` は汎用リストハッシュ。内部でも `_default` でも使われる
- kwargs は dict をソートしてからハッシュするため、引数の順序に非依存

Parents: SPEC004
Ref: src/beautyspot/cachekey.py
2.5 再帰的正規化 (canonicalize) IMPL005
KEY

## 実装概要

`canonicalize()` は `functools.singledispatch` で実装された再帰的正規化関数。 型ごとにディスパッチハンドラを登録し、ネストされたデータ構造を再帰的に正規化する。 各ハンドラは型タグ付きのタプルを返し、異なるコレクション型が同じ内容でも 区別されるようにする。

## 設計判断

### singledispatch の採用

if/elif チェーンと比較して、新しい型のサポート追加が局所的で、 既存コードへの影響がない。numpy や Pydantic のようなオプショナル依存の 型ハンドラも、条件付きで登録できる。

### 型タグによるコレクション区別

`[1, 2]`（list）と `(1, 2)`（tuple）を区別するため、 正規化結果に `("__list__", ...)`, `("__tuple__", ...)` のように 型タグを含める。これにより、構造的に同一でも型が異なるデータが 異なるキャッシュキーを生成する。

### bool vs int の区別

Python では `True == 1` かつ `isinstance(True, int)` だが、 キャッシュキーとしては区別が必要。singledispatch は MRO 順でマッチするため、 `bool` を `int` より先にチェックするインライン処理を base handler に配置した。

## 実装メモ

- カスタムオブジェクトの正規化は `__dict__` と `__slots__` の両方を MRO 走査で収集する。 `__dict__` スロット自体はスキップする
- `OrderedDict` は順序が意味を持つため、キーのソートを行わない（dict とは異なる）
- `defaultdict` は `default_factory` を無視し、内容のみで正規化する
- `Enum` は `型名.メンバー名 + value` で正規化し、モジュール・qualname も含める
- 循環参照を検出した場合は `str()` にフォールバックし、警告を出力する（非決定的）
- 未知の型も `str()` にフォールバックするが、安定性は保証されない

Parents: SPEC005
Ref: src/beautyspot/cachekey.py
2.6 KeyGenPolicy 実装 IMPL006
KEY

## 実装概要

`Strategy` enum（DEFAULT, IGNORE, FILE_CONTENT, PATH_STAT の4種）と `KeyGenPolicy` クラスで構成。`KeyGenPolicy` は引数名→戦略のマッピングを保持し、 `bind(func)` で `inspect.signature()` を使って関数シグネチャにバインドされた キー生成関数を返す。ファクトリメソッド `KeyGen.ignore()`, `KeyGen.map()`, `KeyGen.file_content()`, `KeyGen.path_stat()` で宣言的にポリシーを構築できる。

## 設計判断

### bind() によるシグネチャバインディング

デコレーション時に `inspect.signature(func)` を呼び、 位置引数・キーワード引数・デフォルト値を解決する。 これにより、呼び出し時にはシグネチャ解析のオーバーヘッドがなくなる。

### Strategy enum による宣言的指定

引数ごとの戦略を enum で宣言する方式を採用。 関数を渡す方式（`keygen=lambda ...`）と比較して、 シリアライズ可能で、デバッグ時の可読性が高い。

## 実装メモ

- `bind()` 内で `RecursionError` を catch し、警告を出力してフォールバックする
- `FILE_CONTENT` 戦略は `KeyGen.from_file_content()` に委譲し、65KB チャンク読み取りを行う
- `PATH_STAT` 戦略は `KeyGen.from_path_stat()` に委譲し、mtime + size でハッシュする
- `IGNORE` 戦略の引数はキー計算から完全に除外される（値に関係なくキャッシュヒット）

Parents: SPEC006
Ref: src/beautyspot/cachekey.py
2.7 SQLiteTaskDB 実装 IMPL007
DB

## 実装概要

`SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_writer_thread`（デーモン）が `_write_queue` から書き込みタスクを取り出して 直列処理し、読み取りは `threading.local()` のスレッドローカル接続で並行実行する。 WAL モードにより読み取りと書き込みが並行可能。

## 設計判断

### 専用ライタースレッドの採用

SQLite は単一書き込み接続しかサポートしないため、全書き込みを1つのスレッドに 集約する Producer-Consumer パターンを採用。キュー経由でタスクを受け渡し、 ライタースレッドがコミット/ロールバックを管理する。 マルチスレッドからの並行書き込みによるロック競合を根本的に回避する。

### スレッドローカル読み取り接続

`threading.local()` に `_ReadConnWrapper` を格納し、スレッドごとに 読み取り専用接続（`PRAGMA query_only = ON`）を保持する。 接続エラー時は自動的に再作成し、リーク防止のため `WeakSet` で全接続を追跡する。

### _WriteTask 状態マシン

書き込みタスクを `PENDING → RUNNING → DONE`（または `CANCELLED`）の 状態マシンで管理。タイムアウト時の `try_cancel()` により、 キュー内で待機中のタスクを安全にキャンセルできる。 既に `RUNNING` 状態のタスクはキャンセルせず完了を待つ。

## 実装メモ

- スキーマの自動マイグレーションは `PRAGMA table_info` で既存カラムを確認し、 不足カラム（content_type, version, result_data, func_identifier, expires_at）を追加する
- Lazy Expiration: `get()` 時に `expires_at < NOW` を確認し、期限切れなら `None` を返す。 物理削除は `beautyspot gc` コマンドに委譲する
- `flush()` は空の no-op タスクをキューに投入し、その完了を待つことで 先行する全書き込みの完了を保証する
- `shutdown()` は全読み取り接続のクローズ → `_STOP` センチネルの投入 → ライタースレッドの join で安全に停止する
- `_read_connect()` のダブルチェックパターンで TOCTOU 競合を回避している

Parents: SPEC007
Ref: src/beautyspot/db.py
2.8 DB プロトコル階層実装 IMPL008
DB

## 実装概要

DB 層のプロトコル階層を定義。`TaskDBCore`（ランタイム必須の CRUD）、 `Flushable`（書き込み同期）、`Shutdownable`（安全な停止）、 `Maintenable`（GC・統計等の管理操作）の4つのプロトコルと、 それらを統合した `TaskDBMaintenable` を提供する。 `TaskDBBase` は ABC として `Maintenable` のデフォルト実装（安全な no-op）を提供する。

## 設計判断

### プロトコルの細分化

単一の巨大インターフェースではなく、責務ごとにプロトコルを分割した。 これにより、カスタム DB 実装は `TaskDBCore` のみ実装すればランタイムで動作し、 メンテナンス機能は段階的にオプトインできる。

### デフォルト実装の安全性

`TaskDBBase` のメンテナンスメソッド（`delete_expired()`, `prune()` 等）は デフォルトで空リスト返却や no-op を行い、サブクラスが未実装でも安全に動作する。 CLI の `gc` コマンドが未対応の DB バックエンドでもエラーにならない設計。

## 実装メモ

- `Flushable.flush(timeout)` のデフォルトは no-op。SQLiteTaskDB のみがキュー同期を実装
- `Shutdownable.shutdown(wait)` のデフォルトは no-op。リソースを持たない軽量実装に配慮
- カスタム DB 実装は `isinstance(db, Maintenable)` で機能の有無を判定できる

Parents: SPEC008
Ref: src/beautyspot/db.py
2.9 LocalStorage 実装 IMPL009
STORE

## 実装概要

- `LocalStorage` クラスが `BlobStorageBase` を実装。 ファイルは `{base_dir}/{key}.bin` 形式で保存される。

- `save()` は `tempfile.mkstemp()` → `fsync()` → `os.replace()` の アトミック書き込みパターンで実装。

## 設計判断

### アトミック書き込みパターン

一時ファイルに書き込み → fsync でディスクに確実に反映 → os.replace で アトミックにリネーム。この3段階により、書き込み中のクラッシュや 並行アクセスでファイルが半壊状態になることを防ぐ。 一時ファイルには `.spot_tmp` 接尾辞を使用し、残存時のクリーンアップを容易にした。

### パストラバーサル防止の二重ガード

- `save()` / `_validate_key()`: キーに `..`, `/`, `\` を含む場合に `ValidationError`

- `load()` / `delete()`: 解決済みパスが `base_dir.is_relative_to()` を満たすか検証 二重のチェックにより、キー経由とロケーション経由の両方のパストラバーサルを防ぐ。

## 実装メモ

- 初期化時に `base_dir` を絶対パスに正規化し、ディレクトリと `.gitignore` を自動作成

- `delete()` は冪等。ファイル不在時もエラーを送出しない。パーミッションエラーは警告のみ

- `list_keys()` は `rglob("*.bin")` でレガシーサブディレクトリ構造にも対応

- `clean_temp_files()` は猶予期間（デフォルト24時間）超過の `.spot_tmp` ファイルのみ削除。 ファイルロック（アンチウイルス等）のエラーは安全に無視する

- `prune_empty_dirs()` はボトムアップで走査し、`.DS_Store` 等のシステムファイルのみの ディレクトリも空とみなして削除する。`base_dir` 自体は保持する

Parents: SPEC009
Ref: src/beautyspot/storage.py
2.10 S3Storage 実装 IMPL010
STORE

## 実装概要

`S3Storage` クラスが `BlobStorageBase` を実装。 `s3://bucket/prefix/` 形式の URI を解析し、boto3 の S3 クライアントを使用。 ファイルは `{prefix}/{key}.bin` のキーで S3 に保存される。

## 設計判断

### オプショナル依存のガード

boto3 は `import` 時にガードし、未インストール時は クラス定義は成功するがインスタンス化で明確なエラーメッセージを表示する。 beautyspot 全体が boto3 に依存しないよう、遅延インポートパターンを採用。

### URI ベースのバックエンド選択

`s3://bucket/prefix` 形式の URI から bucket と prefix を自動解析する `_parse_s3_uri()` ヘルパーにより、ファクトリ関数がスキームに基づいて LocalStorage と S3Storage を透過的に切り替えられる。

## 実装メモ

- `save()` は `upload_fileobj()` を使用し、5GB 超のデータにも マルチパートアップロードで対応する
- ロケーションは完全な S3 URI（`s3://bucket/prefix/key.bin`）を返す
- `list_keys()` は paginator で全キーを列挙し、prefix を除いた相対キーを返す
- `get_mtime()` は `head_object` で `LastModified` を取得（GC の grace period 判定用）

Parents: SPEC010
Ref: src/beautyspot/storage.py
2.11 ストレージポリシー実装 IMPL011
STORE

## 実装概要

`StoragePolicyProtocol` が `should_save_as_blob(data: bytes) -> bool` を定義。 3つの実装を提供: - `ThresholdStoragePolicy`: `len(data) > threshold` で判定 - `WarningOnlyPolicy`: 常に `False` を返し、閾値超過時に WARNING ログを出力 - `AlwaysBlobPolicy`: 常に `True` を返す

## 設計判断

### Strategy パターンによるポリシー分離

保存先判定ロジックを `core.Spot` から分離し、差し替え可能なポリシーオブジェクトに 委譲する。`@mark(save_blob=True/False)` の明示指定はポリシーより優先されるが、 `save_blob=None`（デフォルト）時にポリシーが判定を行う。

### WarningOnlyPolicy をデフォルトにした理由

v2.0 との後方互換性を維持するため。v2.0 では全データが DB 直接保存だったため、 デフォルトを `ThresholdStoragePolicy` にすると既存ユーザーの挙動が変わる。 WARNING ログにより、ユーザーに Blob 分離の恩恵を段階的に周知する。

## 実装メモ

- 各ポリシーは dataclass で実装し、`threshold` パラメータを属性として保持
- `WarningOnlyPolicy` のログは `warnings.warn` ではなく `logging.warning` を使用。 ユーザーコードの警告フィルタに影響されないため

Parents: SPEC011
Ref: src/beautyspot/storage.py
2.12 MsgpackSerializer 実装 IMPL012
SERIAL

## 実装概要

`MsgpackSerializer` クラスが `SerializerProtocol` と `TypeRegistryProtocol` を実装。 `dumps(obj) -> bytes` / `loads(data) -> Any` が主要 API。 カスタム型は msgpack の `ExtType(code, payload)` で拡張する。 `_default_packer()` がエンコード時のカスタム型ディスパッチ、 `_ext_hook()` がデコード時の型復元を行う。

## 設計判断

### Copy-on-Write レジストリ

`_encoders` / `_decoders` 辞書は `register()` 時に新しい辞書を作成して アトミックに差し替える（CoW）。読み取り側はスナップショットを参照するため、 ロックなしで安全に読める。GIL に依存せず、PEP 703（free-threading）にも 対応できる設計。世代カウンタ `_registry_generation` をレジストリ差し替えの前に インクリメントすることで、読み取り側が古いキャッシュを使い続けることを防ぐ。

### スレッドローカル LRU キャッシュ

MRO スキャンの結果（サブクラス→登録済み親クラスの解決）を `threading.local()` の `OrderedDict` にキャッシュする。`_cache_generation` と `_registry_generation` を比較し、 世代が変わったらキャッシュを破棄する。LRU のサイズ上限は `max_cache_size` で制御。

### MRO スキャンによるサブクラス自動解決

Pydantic モデルのサブクラスのように、親クラスが登録済みなら サブクラスも自動的に同じエンコーダで処理できる。`type(obj).__mro__` を走査し、 最初にマッチした登録型のエンコーダを使用する。

## 実装メモ

- `register()` の code は 0-127（msgpack ExtType の制約）
- 同一型や同一 code の重複登録は `ConfigurationError` で拒否
- `_ext_hook` 内のデシリアライズは再帰的に `msgpack.unpackb(ext_hook=self._ext_hook)` を 呼ぶことで、ネストされたカスタム型も復元できる
- `SerializationError` のメッセージには未登録型のヒント（`spot.register()` の使い方）を含める

Parents: SPEC012
Ref: src/beautyspot/serializer.py
2.13 カスタム型登録実装 IMPL013
SERIAL

## 実装概要

2つのレイヤーで構成: - `MsgpackSerializer.register(type, code, encoder, decoder)`: 低レベルの型登録 API - `Spot.register()` / `Spot.register_type()`: ユーザー向けの高レベル API

`Spot.register()` はデコレータ形式、`Spot.register_type()` は命令形式で、 いずれも内部で `serializer.register()` に委譲する（`TypeRegistryProtocol` 準拠時のみ）。

## 設計判断

### 二層 API の提供

デコレータ形式 `@spot.register(code=1, ...)` はクラス定義と同時に登録でき、 宣言的で読みやすい。命令形式 `spot.register_type(MyClass, code=1, ...)` は 外部ライブラリのクラスなどデコレートできない場合に使用する。

### TypeRegistryProtocol による条件分岐

シリアライザが `TypeRegistryProtocol` を実装していない場合（カスタムシリアライザ）、 `register()` / `register_type()` は `ConfigurationError` を送出する。 これにより、型登録非対応のシリアライザを使用する場合のエラーメッセージが明確になる。

## 実装メモ

- `register()` の内部で `_write_lock` を取得してから CoW で辞書を差し替える
- Pydantic モデルの factory デコーダは `model_validate(data)` を使用し、 バリデーション付きの復元が可能
- code の重複登録はエラーとなるが、同一型の再登録（code 変更）もエラーとなる

Parents: SPEC013
Ref: src/beautyspot/serializer.py, src/beautyspot/core.py
2.14 BackgroundLoop 実装 IMPL014
BGIO

## 実装概要

`_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_loop()` で新しいイベントループを作成し、 `_thread`（デーモンスレッド）上で `loop.run_forever()` を実行する。 外部からは `submit()` を通じてコルーチンを投入でき、`run_coroutine_threadsafe` で スレッドセーフにループへ渡される。

## 設計判断

### デーモンスレッドと明示的なシャットダウン

スレッドは `daemon=True` とし、メインスレッド終了時にプロセスがブロックされないようにしている。 しかし、安全なリソース解放のために `drain()` メソッドを提供し、 投入された全タスクの完了を `drain_timeout` の範囲で待機する。

### 独立したイベントループ

メインスレッドのイベントループと競合しないよう、専用のイベントループを 作成してバックグラウンドスレッドで駆動する。これにより `save_sync=False` 時の 保存処理などが、呼び出し元の asyncio 環境から完全に隔離される。

## 実装メモ

- `submit()` は投入されたタスクを `_tasks` 集合に登録し、`add_done_callback` で 完了時に自身を削除することで、未完了タスクの追跡を可能にしている
- シャットダウン時の競合を防ぐため、`_lock` による排他制御を行っている
- `atexit` ハンドラとしてもシャットダウンが登録される

Parents: SPEC014
Ref: src/beautyspot/core.py
2.15 save_sync / flush / drain 実装 IMPL015
BGIO

## 実装概要

`Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの保存処理は `_bg_loop.submit()` で バックグラウンドに投入される。これら未完了のタスクやDBキューを同期するため、 `flush()` およびコンテキストマネージャによる `drain` が実装されている。

## 設計判断

### flush と drain の使い分け

- `flush()`: DBライタースレッドのキュー (`self.db.flush()`) を空になるまで待機する
- `__exit__`: コンテキストマネージャ終了時に `flush()` を呼び出した上で、 さらに `_bg_loop.drain()` を呼び出し、非同期タスクの完了も待つ これにより、スクリプト終了時やバッチ処理の区切りで、データの完全な永続化を保証する。

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

バックグラウンド保存時のエラーは呼び出し元のメインフローを妨げないよう、 `on_background_error` コールバックに通知され、ログ出力のみ行う。 例外の再送出は行わない。

## 実装メモ

- `flush()` にはタイムアウトを設定でき、ハングアップを防止している
- `on_background_error` は `SaveErrorContext` を引数に取り、失敗時の キーや関数の詳細情報を提供する

Parents: SPEC015
Ref: src/beautyspot/core.py
2.16 LifecyclePolicy 実装 IMPL016
LIFE

## 実装概要

`LifecyclePolicy` クラスは `Rule` オブジェクトのリストを保持し、 関数名に基づいてキャッシュの保持期間を決定する。 `resolve()` メソッドが `fnmatch.fnmatch()` を使って関数名をパターンと照合し、 最初にマッチしたルールの保持期間を返す。

## 設計判断

### First-Match セマンティクス

ルールのリストは順序が意味を持ち、最初にマッチしたものが採用される。 これにより、特定プレフィックスの関数には短い保持期間を設定し、 最後に `*`（ワイルドカード）でデフォルトポリシーを設定するような フォールバック構造が簡単に記述できる。

### resolve_with_fallback の導入

後方互換性と柔軟性のため、まず `func_identifier` (モジュール名付きの完全修飾名) で マッチングを試み、マッチしなかった場合は `func_name` (短い関数名) で再度マッチングを 試みる `resolve_with_fallback()` を提供している。

### default_retention の導入

`LifecyclePolicy` コンストラクタに `default_retention` パラメータを追加。 どのルールにもマッチしない場合に返す保持期間を指定できる。 `LifecyclePolicy.default()` は `default_retention="30d"` で生成し、 デフォルトで30日の保持期間を設定する。

## 実装メモ

- `LifecyclePolicy.default()` は空のルールリスト + `default_retention="30d"` を持つ
- `default_retention=None` を指定すれば従来通り無期限保持となる

Parents: SPEC016
Ref: src/beautyspot/lifecycle.py
2.17 Retention 実装 IMPL017
LIFE

## 実装概要

`Retention` クラスを名前空間として使用し、保持期間のパースや 特殊な定数（`INDEFINITE`, `FOREVER`）を管理する。 `parse_retention()` 関数は文字列（"7d", "12h"等）、`timedelta`、 または秒数（`int`/`float`）を受け取り、標準化された `timedelta` オブジェクトに変換する。

## 設計判断

### _ForeverSentinel シングルトン

`Retention.FOREVER` はポリシーを強制的にバイパスするための特殊値。 PEP 703 (free-threading) 環境での安全性を考慮し、 `_ForeverSentinel` は `threading.Lock` を用いたダブルチェックロッキングで 厳密なシングルトンとして実装されている。

### 直感的な文字列表現のサポート

`"7d"`, `"12h"`, `"30m"`, `"10s"` のような文字列フォーマットを 正規表現 `_TIME_PATTERN` でパースすることで、設定ファイルや ハードコード時の可読性を高めている。

## 実装メモ

- `parse_retention()` は無効なフォーマットや負の値に対して `ValidationError` を送出する
- `_ForeverSentinel` は `__bool__()` で `True` を返すようオーバーライドされている

Parents: SPEC017
Ref: src/beautyspot/lifecycle.py
2.18 TokenBucket 実装 IMPL018
LIMIT

## 実装概要

`LimiterProtocol` を実装する `TokenBucket` クラス。 GCRA (Generic Cell Rate Algorithm) に基づき、スレッドセーフおよび 非同期対応のスムーズなレートリミッタを提供する。 内部状態として Theoretical Arrival Time (TAT) を保持し、 `consume()` で同期的スリープ、`consume_async()` で非同期的スリープを行う。

## 設計判断

### GCRAアルゴリズムの採用

伝統的なトークンバケットとは異なり、長時間アイドル後に 一気にバーストを許容しない「Strict Pacing」を実現できる。 TATの更新と現在時刻の比較だけで待機時間を計算できるため、 メモリ効率と計算効率に優れる。

### モノトニッククロックの利用

時刻の取得に `time.monotonic()` を使用し、システム時刻の変更（NTP同期など）の 影響を受けない堅牢な設計としている。

## 実装メモ

- `_consume_reservation()` メソッドが待機時間の計算と TAT 更新を `threading.Lock` でアトミックに行う
- `cost > max_cost`（バケット容量超過）の場合は、どれだけ待っても 処理できないため即座に `ValueError` を送出する
- 同期パスは `time.sleep()`、非同期パスは `asyncio.sleep()` で待機する

Parents: SPEC018
Ref: src/beautyspot/limiter.py
2.19 HookBase 実装 IMPL019
HOOK

## 実装概要

タスク実行ライフサイクルに介入するインターフェース `HookBase` と、 そのスレッドセーフ版 `ThreadSafeHookBase` の実装。 `pre_execute`, `on_cache_hit`, `on_cache_miss` の各コールバックが提供される。 `ThreadSafeHookBase` は `__init_subclass__` を利用し、サブクラスで定義された フックメソッドを自動的にロックでラップする。

## 設計判断

### init_subclass による透過的ロッキング

ユーザーが手動でロックを書く手間を省くため、メタクラス的なアプローチを採用。 クラス定義時にフックメソッドを抽出し、`_wrap_with_lock` デコレータで包むことで、 ユーザーコードを汚さずに完全な排他制御を実現している。

### RLock（再入可能ロック）の使用

当初の `Lock` から `RLock` に変更された。 サブクラスが `super().pre_execute(...)` のように親のメソッドを呼び出した際、 同一スレッドが再度ロックを取得しようとしてデッドロックする問題を防ぐため。

## 実装メモ

- `_wrap_with_lock` は `functools.wraps` を使い、元のメタデータを保持する
- `ThreadSafeHookBase` は `__getattr__` をオーバーライドし、 `super().__init__()` の呼び出し忘れに対して親切なエラーメッセージを返す

Parents: SPEC019
Ref: src/beautyspot/hooks.py
2.20 MaintenanceService 実装 IMPL020
MAINT

## 実装概要

`MaintenanceService` クラスがDBとBlobストレージ間の整合性チェック、 期限切れキャッシュの削除、孤立ファイルの検出といったガベージコレクション（GC）を担当。 主にCLIの `beautyspot gc` コマンドから呼び出される。 対象となる DB (`TaskDBMaintenable`) とストレージ (`BlobStorageMaintenable`) を受け取り、 複数のフェーズに分けてクリーンアップを実行する。

## 設計判断

### 5フェーズのクリーンアッププロセス

`clean_garbage()` は以下の順序で安全にGCを実行する： 1. 期限切れDBレコードの削除 2. Blobストレージの一時ファイル（`.spot_tmp`）のクリーンアップ 3. 孤立したBlobファイル（DBに存在しないファイル）の特定 4. 孤立ファイルの削除 5. 空ディレクトリの剪定

### Grace Period (猶予期間) の導入

実行中のタスクがBlobを書き込んだ直後で、まだDBにメタデータが保存されていない タイミングでGCが走ると、必要なファイルが孤立と誤認されるリスクがある。 これを防ぐため、`orphan_grace_seconds`（デフォルト60秒）より新しいファイルは 孤立判定から除外する設計とした。

## 実装メモ

- 孤立ファイルの検出 `scan_garbage()` は、ストレージの全キーを列挙し、 DBの `get_blob_keys()` との差分を取ることで行う
- 複数プロジェクトを一括でGCするためのヘルパー `scan_orphan_projects()` も提供される

Parents: SPEC020
Ref: src/beautyspot/maintenance.py
2.21 CLI コマンド実装 IMPL021
CLI

## 実装概要

`Typer` を利用したコマンドラインインターフェースの実装。 `list`, `show`, `stats`, `clear`, `clean`, `gc`, `prune`, `version` 等の コマンドを提供し、`rich` ライブラリを用いてコンソール出力（テーブル、パネル、 プログレスバー等）をリッチにフォーマットしている。

## 設計判断

### Typer と Rich の組み合わせ

Typer は型ヒントベースでコマンドやオプションを定義でき、コードの記述量と メンテナンスコストを大幅に削減できる。Rich による出力は、単なるテキストではなく 構造化された情報（JSONやMarkdown）の視認性を劇的に向上させ、DXを高める。

### 安全性を考慮した対話的プロンプト

`clear` や `clean` など、データを大規模に削除するコマンドについては、 `--force` オプションが指定されない限り、`rich.prompt.Confirm` を用いて ユーザーに明示的な確認を求めることで、誤操作によるデータ喪失を防いでいる。

## 実装メモ

- コマンドは `MaintenanceService` などの内部 API を利用して処理を行う
- 各コマンドは特定のプロジェクト (`--project` / `-p`) に対して操作を行うが、 一部のコマンド（`list` 等）はワークスペース全体の走査もサポートする

Parents: SPEC021
Ref: src/beautyspot/cli.py
2.22 ファクトリ関数実装 IMPL022
DI

## 実装概要

`beautyspot` パッケージのメインエントリポイントとなる `Spot` ファクトリ関数の実装。 引数として渡された各コンポーネント（DB、Serializer、Storage等）を依存性注入（DI）で 解決し、デフォルトのコンポーネント（`SQLiteTaskDB`, `MsgpackSerializer`, `LocalStorage`等）を インスタンス化して `CacheManager` と `_Spot` コアエンジンを組み立てる。

## 設計判断

### Factory 関数による Composition

`_Spot` クラス自体のコンストラクタは複雑な依存関係を要求するが、 ユーザー向けに `Spot()` 関数を提供することで、通常は `name` を渡すだけで 「ゼロ設定」で動作するようにカプセル化している。 同時に、高度なユーザーは各コンポーネントを自由に差し替え可能な DI アーキテクチャを維持している。

### DB ライフサイクルの委譲

`Spot()` 関数内でデフォルトのDB（`SQLiteTaskDB`）を自動生成した場合、 `spot._owns_db = True` フラグを立て、Spotエンジンのシャットダウン時に DBも自動でクローズされるようにする。一方、ユーザーが明示的に `db=` を 渡した場合は、DBのライフサイクル管理は呼び出し元に委ねる（勝手に閉じない）。

## 実装メモ

- Storage Policy は `save_blob` フラグや引数によって `WarningOnlyPolicy`, `AlwaysBlobPolicy` 等に解決される
- `__all__` に各種プロトコルやデフォルト実装、例外クラスをエクスポートし、 ライブラリとしての公開API境界を明確に定義している

Parents: SPEC022
Ref: src/beautyspot/__init__.py
2.23 Thundering Herd 実装 IMPL023
HERD

## 実装概要

`CacheManager` クラス内で、Thundering Herd（キャッシュミス時に同一キーへの 大量アクセスが同時に発生する問題）を防止する直列化機構を実装。 `_inflight` 辞書で実行中のキーを管理し、最初の1スレッド/タスクだけが関数を実行し、 後続の呼び出しは `wait_herd_sync` / `wait_herd_async` でその完了を待機する。

## 設計判断

### 同期・非同期の統合的保護

`_inflight` 辞書の値として `(threading.Event, list[asyncio.Future], list[result])` の タプルを保持し、スレッドベースの待機 (`Event.wait`) と asyncio ベースの待機 (`Future`) の 両方を単一の管理下で混在できるようにしている。

### タイムアウトと再試行（自己回復）

関数の実行がハングアップした場合に待機側が永遠にブロックされるのを防ぐため、 `HERD_TIMEOUT`（300秒）と `HERD_MAX_RETRIES`（3回）を設定。 タイムアウト時には警告ログを出しつつ再試行し、上限を超えれば `TimeoutError` で フェイルファストする堅牢な設計とした。

## 実装メモ

- `notify_and_cleanup_inflight()` で待機タスクへの結果伝播と `_inflight` からの削除を `_inflight_lock` によりアトミックに行う
- `result_box`（要素数1のリスト）をリファレンス渡しで共有することで、 `Event` が set された際に安全に結果を取得できるようにしている
- `_notify_future()` は `call_soon_threadsafe` を使い、非同期Futureに対して 別スレッドから安全に結果（または例外）をセットする

Parents: SPEC023
Ref: src/beautyspot/cache.py
2.24 BlobStorageBase 抽象インターフェース実装 IMPL024
STORE

## 実装概要

`storage.py` 内の `BlobStorageBase` ABC が SPEC024 の中核実装。 5つの抽象メソッド（`save`, `load`, `delete`, `list_keys`, `get_mtime`）を定義し、 `LocalStorage` と `S3Storage` が具象実装として継承する。

加えて、ランタイム型チェックのために `BlobStorageCore`・`Maintenable`・ `BlobStorageMaintenable` の3つの Protocol クラスを定義し、 `isinstance()` での型判定を可能にしている（`@runtime_checkable`）。

## 設計判断

### ABC と Protocol の併用

`BlobStorageBase` は ABC として抽象メソッドを強制する一方、 `BlobStorageCore` / `Maintenable` は Protocol として構造的部分型を提供する。 これにより、`BlobStorageBase` を継承しないサードパーティ実装でも `isinstance(obj, BlobStorageCore)` で利用可能になる柔軟性を確保した。

### 3層の Protocol 分割

- `BlobStorageCore`: 実行時に必要な最小限（save/load/delete）
- `Maintenable`: メンテナンス（GC）に必要な拡張（list_keys/get_mtime）
- `BlobStorageMaintenable`: 両方を兼備する完全版

これにより、save/load/delete のみを実装した軽量バックエンドも受け入れ可能。

### ReadableBuffer 型エイリアス

`bytes | bytearray | memoryview` を `ReadableBuffer` として定義。 `memoryview` を受け入れることでゼロコピー書き込みが可能になり、 大きなデータを保存する際のメモリ効率を改善している。

## 実装メモ

- `BlobStorageBase` 自体はロジックを持たず、インターフェース定義のみ
- 各 `@abstractmethod` にはdocstringで契約を明記（冪等性、エラー時の挙動など）
- `delete` は冪等であるべきことを docstring で規定
- `list_keys` は `delete` と同じフォーマットの識別子を yield すべきことを規定

Parents: SPEC024
Ref: src/beautyspot/storage.py