IMPL - Document View

# IMPL Specification

Generated at 2026-03-09 22:49:07

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

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` が元関数に適用される

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

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 をソートしてからハッシュするため、引数の順序に非依存

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()` にフォールバックするが、安定性は保証されない

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` 戦略の引数はキー計算から完全に除外される（値に関係なくキャッシュヒット）

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 競合を回避している

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)` で機能の有無を判定できる

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` 自体は保持する

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 判定用）

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` を使用。 ユーザーコードの警告フィルタに影響されないため

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()` の使い方）を含める

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 変更）もエラーとなる

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` ハンドラとしてもシャットダウンが登録される

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` を引数に取り、失敗時の キーや関数の詳細情報を提供する

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` を指定すれば従来通り無期限保持となる

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` を返すようオーバーライドされている

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()` で待機する

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__()` の呼び出し忘れに対して親切なエラーメッセージを返す

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()` も提供される

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` 等）はワークスペース全体の走査もサポートする

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境界を明確に定義している

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に対して 別スレッドから安全に結果（または例外）をセットする

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 すべきことを規定

Ref: src/beautyspot/storage.py