# トレーサビリティレポート

生成日時: 2026-03-13 23:59:50

### ドキュメント

6

### 総アイテム

152

### グループ

14

### レビュー済

152/152

### Suspect

0

### エラー

0

### 警告

3

## フィルタ

グループ:
状態:
アイテムID:

## 検証結果

### 警告 (3件)

- REQ013 [['HERD']]: 子ドキュメント ADR からのリンクがありません
- TST009: references 'tests/integration/storage/test_local.py' のファイルが存在しません
- TST010: references 'tests/integration/storage/test_s3.py' のファイルが存在しません

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

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

グループ▲▼ | REQ▲▼ | ARCH▲▼ | SPEC▲▼ | TST▲▼ | IMPL▲▼ | ADR▲▼
--- | --- | --- | --- | --- | --- | ---
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC007 ✓ ## インターフェース ```python class SQLiteTaskDB(TaskDBBase): def __init__(self, db... | TST007 ✓ ## 目的 `SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす... tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py | IMPL007 ✓ ## 実装概要 `SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_wri... src/beautyspot/db.py | ADR007 ✓ # Semantic Content Type Support ## Context and Problem Statement / コンテキスト 生成AI...
DB | REQ019 ✓ データベース操作が指定されたタイムアウト時間内に完了しない場合、呼び出し元を無期限にブロックせずにエラーを返し、システムの可用性を維持すること。特にSQLite... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC007 ✓ ## インターフェース ```python class SQLiteTaskDB(TaskDBBase): def __init__(self, db... | TST007 ✓ ## 目的 `SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす... tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py | IMPL007 ✓ ## 実装概要 `SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_wri... src/beautyspot/db.py | ADR051 ✓ # Thread-Safe SQLite Connection Closing and Lock Management ## Context and Prob...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC001 ✓ ## インターフェース ```python @spot.mark( save_blob: Optional[bool] = None, key... | TST001 ✓ ## 目的 `@spot.mark()` はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元... tests/integration/core/test_basic.py | IMPL001 ✓ ## 実装概要 `Spot.mark()` メソッドが本仕様の中核。二段階デコレータファクトリとして、 オプションを受け取る外側の `mark()` と、関数... src/beautyspot/core.py | ADR012 ✓ # Imperative Execution, Smart Defaults, and Workspace Management ## Context and...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC004 ✓ ## インターフェース ```python class KeyGen: @staticmethod def _default(args: tu... | TST004 ✓ ## 目的 キャッシュキーはキャッシュの同一性判定の基盤であり、ハッシュが不安定だとキャッシュミスの頻発（性能劣化）や誤ヒット（データ不整合）を引き起こす。P... tests/unit/test_cachekey.py | IMPL004 ✓ ## 実装概要 `KeyGen` クラスの静的メソッド群がキャッシュキー生成を担う。 `_default(args, kwargs)` が主要なエントリポイン... src/beautyspot/cachekey.py | ADR002 ✓ # Stable Hashing for Function Arguments ## Context and Problem Statement / コンテキ...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC009 ✓ ## インターフェース ```python class LocalStorage(BlobStorageMaintenable): def __ini... | TST009 ✓ ## 目的 `LocalStorage` はBlobデータのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性... tests/integration/storage/test_local.py | IMPL009 ✓ ## 実装概要 - `LocalStorage` クラスが `BlobStorageBase` を実装。 ファイルは `{base_dir}/{key}.... src/beautyspot/storage.py | ADR030 ✓ # Declarative Storage Policy ## Context and Problem Statement / コンテキスト これまでの `...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC012 ✓ ## インターフェース ```python class MsgpackSerializer(SerializerProtocol, TypeRegistryP... | TST012 ✓ ## 目的 `MsgpackSerializer` はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失（デシリアライズ不能）やサイレン... tests/unit/test_serializer.py | IMPL012 ✓ ## 実装概要 `MsgpackSerializer` クラスが `SerializerProtocol` と `TypeRegistryProtocol` ... src/beautyspot/serializer.py | ADR004 ✓ # Resilient Deserialization and Explicit Versioning ## Context and Problem Stat...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR005 ✓ # Manage Executor Lifecycle via Instance Ownership and Weak References ## Conte...
LIFE | REQ007 ✓ キャッシュの有効期限を関数名パターンに基づくルールで設定できること。個別の関数に対して保持期間を指定でき、期限切れのキャッシュを自動的に無効化できること。 | ARCH007 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|マッチング要求| B[LifecyclePolicy]... | SPEC016 ✓ ## インターフェース ```python class LifecyclePolicy: def add_rule(self, pattern: st... | TST016 ✓ ## 目的 `LifecyclePolicy` はキャッシュデータの保持期間を関数名パターンで制御する。パターンマッチングの不備は、重要なキャッシュの意図しな... tests/unit/test_lifecycle.py | IMPL016 ✓ ## 実装概要 `LifecyclePolicy` クラスは `Rule` オブジェクトのリストを保持し、 関数名に基づいてキャッシュの保持期間を決定する。 ... src/beautyspot/lifecycle.py | ADR032 ✓ # Declarative Lifecycle Policy ## Context and Problem Statement / コンテキスト 機械学習や...
LIMIT | REQ008 ✓ 関数の実行頻度をトークンバケット方式で制限し、外部APIの呼び出しレートを制御できること。同期・非同期の両方に対応すること。 | ARCH008 ✓ ## コンポーネント構成 ```mermaid graph TD A["@spot.consume"] -->|宣言的適用| B[LimiterProto... | SPEC018 ✓ ## インターフェース ```python class TokenBucket(LimiterProtocol): def consume(self,... | TST018 ✓ ## 目的 `TokenBucket` はAPI呼び出しやリソースアクセスのレート制限を実現する。レートリミッターの不具合はAPIの過負荷（制限が甘い場合）や... tests/unit/test_limiter.py | IMPL018 ✓ ## 実装概要 `LimiterProtocol` を実装する `TokenBucket` クラス。 GCRA (Generic Cell Rate Algo... src/beautyspot/limiter.py | ADR003 ✓ # Smooth Rate Limiter (GCRA) ## Context and Problem Statement / コンテキスト `beauty...
HOOK | REQ009 ✓ 関数の実行前、キャッシュヒット時、キャッシュミス時にカスタムコールバック（フック）を実行できること。スレッドセーフなフック実装を提供すること。 | ARCH009 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|イベント通知| B[HookBase] C[Thr... | SPEC019 ✓ ## インターフェース ```python class HookBase: def pre_execute(self, ctx: PreExecute... | TST019 ✓ ## 目的 Hook システムはキャッシュライフサイクルへのユーザー拡張ポイント（ロギング、メトリクス収集、監査等）であり、フック内の不具合がメインの関数実行... tests/integration/core/test_hooks.py | IMPL019 ✓ ## 実装概要 タスク実行ライフサイクルに介入するインターフェース `HookBase` と、 そのスレッドセーフ版 `ThreadSafeHookBase`... src/beautyspot/hooks.py | ADR035 ✓ # クラスベース・フックシステムと並列実行サポート ## Context and Problem Statement / コンテキスト `beautyspo...
MAINT | REQ010 ✓ 期限切れキャッシュの削除、孤立Blobファイルのガベージコレクション、時間ベースの一括削除（prune）等のメンテナンス操作ができること。 | ARCH010 ✓ ## コンポーネント構成 ```mermaid graph TD A[MaintenanceService] -->|操作集約| B[TaskDBBase... | SPEC020 ✓ ## インターフェース ```python class MaintenanceService: def clean_garbage(self, gra... | TST020 ✓ ## 目的 `MaintenanceService` は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用... tests/unit/test_maintenance.py | IMPL020 ✓ ## 実装概要 `MaintenanceService` クラスがDBとBlobストレージ間の整合性チェック、 期限切れキャッシュの削除、孤立ファイルの検出と... src/beautyspot/maintenance.py | ADR016 ✓ # Interactive Deletion and Cleanup Policy ## Context and Problem Statement / コン...
CLI | REQ011 ✓ コマンドラインインターフェースからキャッシュの一覧表示、詳細確認、統計表示、削除、GC等の管理操作ができること。 | ARCH011 ✓ ## コンポーネント構成 ```mermaid graph TD A[beautyspot CLI] -->|Command| B[Typer App] ... | SPEC021 ✓ ## インターフェース ```bash beautyspot list [--db DB_PATH] beautyspot gc [--name PROJEC... | TST021 ✓ ## 目的 CLI はユーザーがキャッシュの状況確認・管理を行う主要インターフェースであり、コマンドの不具合はユーザー体験と運用効率に直接影響する。`CliR... tests/integration/cli/test_cli.py | IMPL021 ✓ ## 実装概要 `Typer` を利用したコマンドラインインターフェースの実装。 `list`, `show`, `stats`, `clear`, `cle... src/beautyspot/cli.py | ADR006 ✓ # Dashboard Interaction Model ## Context and Problem Statement / コンテキスト 現状のダッシ...
DI | REQ012 ✓ 全コンポーネント（DB、ストレージ、シリアライザ、リミッター等）が `Protocol/ABC` に基づく依存注入により差し替え可能であること。ファクトリ関数が... | ARCH012 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|構築| B[core.Spot] A ... | SPEC022 ✓ ## インターフェース ```python def Spot( name: str = "default", storage_path: st... | TST022 ✓ ## 目的 `bs.Spot()` ファクトリ関数は全コンポーネントの DI 配線を担う唯一のパブリックエントリポイントであり、デフォルト構成の正確性とカスタ... tests/integration/core/test_dependency_injection.py | IMPL022 ✓ ## 実装概要 `beautyspot` パッケージのメインエントリポイントとなる `Spot` ファクトリ関数の実装。 引数として渡された各コンポーネント（... src/beautyspot/__init__.py | ADR009 ✓ # Database Dependency Injection and Abstraction ## Context and Problem Statemen...
HERD | REQ013 ✓ 同一キャッシュキーに対する並行リクエストを直列化し、1つのリクエストのみが関数を実行し、他のリクエストはその結果を共有することで重複実行を防止できること。 | ARCH013 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|管理要求| B[CacheManager] B -... | SPEC023 ✓ ## インターフェース ```python class CacheManager: def herd_sync( self, key:... | TST023 ✓ ## 目的 Thundering Herd Protection は、同一キーへの並行リクエストが一斉に関数を実行する「Thundering Herd」問題を... tests/integration/core/test_thundering_herd.py | IMPL023 ✓ ## 実装概要 `CacheManager` クラス内で、Thundering Herd（キャッシュミス時に同一キーへの 大量アクセスが同時に発生する問題）を... src/beautyspot/cache.py | —
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC008 ✓ ## インターフェース ```python @runtime_checkable class TaskDBCore(Protocol): def ge... | TST008 ✓ ## 目的 `TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL... tests/unit/test_db_writer_queue.py | IMPL008 ✓ ## 実装概要 DB 層のプロトコル階層を定義。`TaskDBCore`（ランタイム必須の CRUD）、 `Flushable`（書き込み同期）、`Shutd... src/beautyspot/db.py | ADR007 ✓ # Semantic Content Type Support ## Context and Problem Statement / コンテキスト 生成AI...
DB | REQ019 ✓ データベース操作が指定されたタイムアウト時間内に完了しない場合、呼び出し元を無期限にブロックせずにエラーを返し、システムの可用性を維持すること。特にSQLite... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC008 ✓ ## インターフェース ```python @runtime_checkable class TaskDBCore(Protocol): def ge... | TST008 ✓ ## 目的 `TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL... tests/unit/test_db_writer_queue.py | IMPL008 ✓ ## 実装概要 DB 層のプロトコル階層を定義。`TaskDBCore`（ランタイム必須の CRUD）、 `Flushable`（書き込み同期）、`Shutd... src/beautyspot/db.py | ADR051 ✓ # Thread-Safe SQLite Connection Closing and Lock Management ## Context and Prob...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC002 ✓ ## インターフェース ```python @contextmanager def cached_run( self, *funcs: Any... | TST002 ✓ ## 目的 `cached_run` はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーテ... tests/integration/core/test_cached_run.py | IMPL002 ✓ ## 実装概要 `Spot.cached_run()` はコンテキストマネージャとして実装。 渡された関数群を内部で `mark()` と同等のラッピングを行... src/beautyspot/core.py | ADR012 ✓ # Imperative Execution, Smart Defaults, and Workspace Management ## Context and...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC003 ✓ ## 概要 `@spot.mark()` デコレータは同期関数と非同期（`async def`）関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャ... | TST003 ✓ ## 目的 beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約（スレッドをまたぐコルーチン実... tests/integration/core/test_async_save.py | IMPL003 ✓ ## 実装概要 `Spot._execute_sync()` と `Spot._execute_async()` が実行エンジンの中核。 両メソッドは同一のフ... src/beautyspot/core.py | ADR012 ✓ # Imperative Execution, Smart Defaults, and Workspace Management ## Context and...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC005 ✓ ## インターフェース ```python @singledispatch def canonicalize(obj: Any) -> Any: ... ``... | TST005 ✓ ## 目的 `canonicalize()` は引数の型ごとに正規化ルールを適用し、論理的に同値な入力が同じキャッシュキーを生成することを保証する。正規化の不... tests/unit/test_cachekey.py | IMPL005 ✓ ## 実装概要 `canonicalize()` は `functools.singledispatch` で実装された再帰的正規化関数。 型ごとにディスパッ... src/beautyspot/cachekey.py | ADR002 ✓ # Stable Hashing for Function Arguments ## Context and Problem Statement / コンテキ...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC006 ✓ ## インターフェース ```python class Strategy(Enum): DEFAULT = auto() # 再帰的正規化 ... | TST006 ✓ ## 目的 `KeyGenPolicy` は引数ごとのキー生成戦略をカスタマイズする機構であり、ログレベルやデバッグフラグのような非決定的引数をキーから除外し... tests/unit/test_cachekey.py | IMPL006 ✓ ## 実装概要 `Strategy` enum（DEFAULT, IGNORE, FILE_CONTENT, PATH_STAT の4種）と `KeyGenP... src/beautyspot/cachekey.py | ADR002 ✓ # Stable Hashing for Function Arguments ## Context and Problem Statement / コンテキ...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC010 ✓ ## インターフェース ```python class S3Storage(BlobStorageMaintenable): def __init__... | TST010 ✓ ## 目的 `S3Storage` はクラウド環境での大規模Blob保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが... tests/integration/storage/test_s3.py | IMPL010 ✓ ## 実装概要 `S3Storage` クラスが `BlobStorageBase` を実装。 `s3://bucket/prefix/` 形式の URI を... src/beautyspot/storage.py | ADR030 ✓ # Declarative Storage Policy ## Context and Problem Statement / コンテキスト これまでの `...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC011 ✓ ## インターフェース ```python class StoragePolicyProtocol(Protocol): def should_sav... | TST011 ✓ ## 目的 ストレージポリシーは「データをDB直接保存するか、Blobストレージに分離するか」を決定する戦略レイヤーである。閾値判定の誤りはDBの肥大化（性能... tests/unit/test_storage_policy.py | IMPL011 ✓ ## 実装概要 `StoragePolicyProtocol` が `should_save_as_blob(data: bytes) -> bool` を定... src/beautyspot/storage.py | ADR030 ✓ # Declarative Storage Policy ## Context and Problem Statement / コンテキスト これまでの `...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC024 ✓ ## インターフェース ```python class BlobStorageBase(ABC): @abstractmethod def s... | TST024 ✓ ## 目的 `BlobStorageBase` は全ストレージバックエンド（LocalStorage, S3Storage, サードパーティ）が 準拠すべき抽... tests/integration/storage/test_blob_storage_base.py | IMPL024 ✓ ## 実装概要 `storage.py` 内の `BlobStorageBase` ABC が SPEC024 の中核実装。 5つの抽象メソッド（`save`... src/beautyspot/storage.py | ADR030 ✓ # Declarative Storage Policy ## Context and Problem Statement / コンテキスト これまでの `...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC013 ✓ ## インターフェース ```python def register( code: int, encoder: Callable[[Any],... | TST013 ✓ ## 目的 カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッ... tests/unit/test_type_registry.py | IMPL013 ✓ ## 実装概要 2つのレイヤーで構成: - `MsgpackSerializer.register(type, code, encoder, decoder)... src/beautyspot/serializer.py, src/beautyspot/core.py | ADR004 ✓ # Resilient Deserialization and Explicit Versioning ## Context and Problem Stat...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR005 ✓ # Manage Executor Lifecycle via Instance Ownership and Weak References ## Conte...
LIFE | REQ007 ✓ キャッシュの有効期限を関数名パターンに基づくルールで設定できること。個別の関数に対して保持期間を指定でき、期限切れのキャッシュを自動的に無効化できること。 | ARCH007 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|マッチング要求| B[LifecyclePolicy]... | SPEC017 ✓ ## インターフェース ```python def parse_retention(val: Any) -> float | None: ... ``` #... | TST017 ✓ ## 目的 `Retention` はキャッシュの有効期間をユーザーフレンドリーな形式で指定する値オブジェクトである。パースの不備は意図しない保持期間（例: ... tests/unit/test_lifecycle_extended.py | IMPL017 ✓ ## 実装概要 `Retention` クラスを名前空間として使用し、保持期間のパースや 特殊な定数（`INDEFINITE`, `FOREVER`）を管理す... src/beautyspot/lifecycle.py | ADR032 ✓ # Declarative Lifecycle Policy ## Context and Problem Statement / コンテキスト 機械学習や...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC007 ✓ ## インターフェース ```python class SQLiteTaskDB(TaskDBBase): def __init__(self, db... | TST007 ✓ ## 目的 `SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす... tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py | IMPL007 ✓ ## 実装概要 `SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_wri... src/beautyspot/db.py | ADR010 ✓ # Msgpack Everywhere with Native BLOB Support ## Context and Problem Statement ...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC007 ✓ ## インターフェース ```python class SQLiteTaskDB(TaskDBBase): def __init__(self, db... | TST007 ✓ ## 目的 `SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす... tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py | IMPL007 ✓ ## 実装概要 `SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_wri... src/beautyspot/db.py | ADR043 ✓ # Serialize SQLite Writes with a Writer Queue ## Context and Problem Statement ...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC007 ✓ ## インターフェース ```python class SQLiteTaskDB(TaskDBBase): def __init__(self, db... | TST007 ✓ ## 目的 `SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす... tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py | IMPL007 ✓ ## 実装概要 `SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_wri... src/beautyspot/db.py | ADR045 ✓ # Introduce Explicit DB Queue Flushing ## Context and Problem Statement / コンテキス...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC007 ✓ ## インターフェース ```python class SQLiteTaskDB(TaskDBBase): def __init__(self, db... | TST007 ✓ ## 目的 `SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす... tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py | IMPL007 ✓ ## 実装概要 `SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_wri... src/beautyspot/db.py | ADR048 ✓ # SQLite ライタースレッドの死活監視におけるポーリング間隔の維持 ## Context and Problem Statement / コンテキスト ...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC007 ✓ ## インターフェース ```python class SQLiteTaskDB(TaskDBBase): def __init__(self, db... | TST007 ✓ ## 目的 `SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす... tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py | IMPL007 ✓ ## 実装概要 `SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_wri... src/beautyspot/db.py | ADR050 ✓ # Delegate Database Lifecycle Management to Caller ## Context and Problem State...
DB | REQ019 ✓ データベース操作が指定されたタイムアウト時間内に完了しない場合、呼び出し元を無期限にブロックせずにエラーを返し、システムの可用性を維持すること。特にSQLite... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC007 ✓ ## インターフェース ```python class SQLiteTaskDB(TaskDBBase): def __init__(self, db... | TST007 ✓ ## 目的 `SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす... tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py | IMPL007 ✓ ## 実装概要 `SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_wri... src/beautyspot/db.py | ADR052 ✓ # SQLite Write Task Cancellation Strategy ## Context and Problem Statement / コン...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC001 ✓ ## インターフェース ```python @spot.mark( save_blob: Optional[bool] = None, key... | TST001 ✓ ## 目的 `@spot.mark()` はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元... tests/integration/core/test_basic.py | IMPL001 ✓ ## 実装概要 `Spot.mark()` メソッドが本仕様の中核。二段階デコレータファクトリとして、 オプションを受け取る外側の `mark()` と、関数... src/beautyspot/core.py | ADR013 ✓ # Rename Project to Spot and Task to Mark ## Context and Problem Statement / コン...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC001 ✓ ## インターフェース ```python @spot.mark( save_blob: Optional[bool] = None, key... | TST001 ✓ ## 目的 `@spot.mark()` はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元... tests/integration/core/test_basic.py | IMPL001 ✓ ## 実装概要 `Spot.mark()` メソッドが本仕様の中核。二段階デコレータファクトリとして、 オプションを受け取る外側の `mark()` と、関数... src/beautyspot/core.py | ADR015 ✓ # Strict Scoping for Imperative Execution (Runtime Guard) ## Context and Proble...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC001 ✓ ## インターフェース ```python @spot.mark( save_blob: Optional[bool] = None, key... | TST001 ✓ ## 目的 `@spot.mark()` はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元... tests/integration/core/test_basic.py | IMPL001 ✓ ## 実装概要 `Spot.mark()` メソッドが本仕様の中核。二段階デコレータファクトリとして、 オプションを受け取る外側の `mark()` と、関数... src/beautyspot/core.py | ADR034 ✓ # cached_run スコープ制限の廃止 ## Context and Problem Statement / コンテキスト ADR-0014 では、`...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC004 ✓ ## インターフェース ```python class KeyGen: @staticmethod def _default(args: tu... | TST004 ✓ ## 目的 キャッシュキーはキャッシュの同一性判定の基盤であり、ハッシュが不安定だとキャッシュミスの頻発（性能劣化）や誤ヒット（データ不整合）を引き起こす。P... tests/unit/test_cachekey.py | IMPL004 ✓ ## 実装概要 `KeyGen` クラスの静的メソッド群がキャッシュキー生成を担う。 `_default(args, kwargs)` が主要なエントリポイン... src/beautyspot/cachekey.py | ADR011 ✓ # Canonical Input Serialization with Msgpack and SHA-256 ## Context and Problem...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC004 ✓ ## インターフェース ```python class KeyGen: @staticmethod def _default(args: tu... | TST004 ✓ ## 目的 キャッシュキーはキャッシュの同一性判定の基盤であり、ハッシュが不安定だとキャッシュミスの頻発（性能劣化）や誤ヒット（データ不整合）を引き起こす。P... tests/unit/test_cachekey.py | IMPL004 ✓ ## 実装概要 `KeyGen` クラスの静的メソッド群がキャッシュキー生成を担う。 `_default(args, kwargs)` が主要なエントリポイン... src/beautyspot/cachekey.py | ADR022 ✓ # Refactoring Complex Modules with Single Dispatch ## Context and Problem State...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC009 ✓ ## インターフェース ```python class LocalStorage(BlobStorageMaintenable): def __ini... | TST009 ✓ ## 目的 `LocalStorage` はBlobデータのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性... tests/integration/storage/test_local.py | IMPL009 ✓ ## 実装概要 - `LocalStorage` クラスが `BlobStorageBase` を実装。 ファイルは `{base_dir}/{key}.... src/beautyspot/storage.py | ADR044 ✓ # Drop Absolute Path Support in LocalStorage ## Context and Problem Statement /...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC009 ✓ ## インターフェース ```python class LocalStorage(BlobStorageMaintenable): def __ini... | TST009 ✓ ## 目的 `LocalStorage` はBlobデータのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性... tests/integration/storage/test_local.py | IMPL009 ✓ ## 実装概要 - `LocalStorage` クラスが `BlobStorageBase` を実装。 ファイルは `{base_dir}/{key}.... src/beautyspot/storage.py | ADR046 ✓ # Delegate Workspace Initialization to Storage Backends ## Context and Problem ...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC012 ✓ ## インターフェース ```python class MsgpackSerializer(SerializerProtocol, TypeRegistryP... | TST012 ✓ ## 目的 `MsgpackSerializer` はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失（デシリアライズ不能）やサイレン... tests/unit/test_serializer.py | IMPL012 ✓ ## 実装概要 `MsgpackSerializer` クラスが `SerializerProtocol` と `TypeRegistryProtocol` ... src/beautyspot/serializer.py | ADR008 ✓ # Customizable Serialization Strategy with Msgpack Default ## Context and Probl...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC012 ✓ ## インターフェース ```python class MsgpackSerializer(SerializerProtocol, TypeRegistryP... | TST012 ✓ ## 目的 `MsgpackSerializer` はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失（デシリアライズ不能）やサイレン... tests/unit/test_serializer.py | IMPL012 ✓ ## 実装概要 `MsgpackSerializer` クラスが `SerializerProtocol` と `TypeRegistryProtocol` ... src/beautyspot/serializer.py | ADR014 ✓ # Decorator-based Type Registration with Late Binding ## Context and Problem St...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC012 ✓ ## インターフェース ```python class MsgpackSerializer(SerializerProtocol, TypeRegistryP... | TST012 ✓ ## 目的 `MsgpackSerializer` はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失（デシリアライズ不能）やサイレン... tests/unit/test_serializer.py | IMPL012 ✓ ## 実装概要 `MsgpackSerializer` クラスが `SerializerProtocol` と `TypeRegistryProtocol` ... src/beautyspot/serializer.py | ADR017 ✓ title: Nested Msgpack Protocol via Serializer Wrapping status: Accepted date: 20...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC012 ✓ ## インターフェース ```python class MsgpackSerializer(SerializerProtocol, TypeRegistryP... | TST012 ✓ ## 目的 `MsgpackSerializer` はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失（デシリアライズ不能）やサイレン... tests/unit/test_serializer.py | IMPL012 ✓ ## 実装概要 `MsgpackSerializer` クラスが `SerializerProtocol` と `TypeRegistryProtocol` ... src/beautyspot/serializer.py | ADR019 ✓ # Per-Task Serializer Override ## Context and Problem Statement / コンテキスト プロジェク...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC012 ✓ ## インターフェース ```python class MsgpackSerializer(SerializerProtocol, TypeRegistryP... | TST012 ✓ ## 目的 `MsgpackSerializer` はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失（デシリアライズ不能）やサイレン... tests/unit/test_serializer.py | IMPL012 ✓ ## 実装概要 `MsgpackSerializer` クラスが `SerializerProtocol` と `TypeRegistryProtocol` ... src/beautyspot/serializer.py | ADR025 ✓ # Protocol-based Serializer Registration ## Context and Problem Statement / コンテ...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC012 ✓ ## インターフェース ```python class MsgpackSerializer(SerializerProtocol, TypeRegistryP... | TST012 ✓ ## 目的 `MsgpackSerializer` はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失（デシリアライズ不能）やサイレン... tests/unit/test_serializer.py | IMPL012 ✓ ## 実装概要 `MsgpackSerializer` クラスが `SerializerProtocol` と `TypeRegistryProtocol` ... src/beautyspot/serializer.py | ADR039 ✓ # Bounded LRU Cache for Subclass Resolution in Serializer ## Context and Proble...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC012 ✓ ## インターフェース ```python class MsgpackSerializer(SerializerProtocol, TypeRegistryP... | TST012 ✓ ## 目的 `MsgpackSerializer` はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失（デシリアライズ不能）やサイレン... tests/unit/test_serializer.py | IMPL012 ✓ ## 実装概要 `MsgpackSerializer` クラスが `SerializerProtocol` と `TypeRegistryProtocol` ... src/beautyspot/serializer.py | ADR042 ✓ # Thread-safe LRU Cache for Serializer ## Context and Problem Statement / コンテキス...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR024 ✓ # Non-blocking Cache Persistence and Task Tracking ## Context and Problem State...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR033 ✓ # 非同期保存処理におけるエラー可視化とコンテキストの導入 ## Context and Problem Statement / コンテキスト beauty...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR036 ✓ # バックグラウンドループのライフサイクル管理とGC時のデータロスト対策 ## Context and Problem Statement / コンテキスト ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR037 ✓ # GC時のデータロスト防止とバックグラウンドループのシャットダウン戦略 ## Context and Problem Statement / コンテキスト ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR040 ✓ # Background Loop Shutdown Strategy and Threading Model ## Context and Problem ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR047 ✓ # Background Loop Shutdown Strategy on Garbage Collection ## Context and Proble...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR049 ✓ # 実行中タスクの強参照セットによる追跡 ## Context and Problem Statement / コンテキスト `Spot` インスタンスは、...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR053 ✓ # Background Loop and Executor Shutdown Sequence ## Context and Problem Stateme...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... tests/integration/core/test_background_loop.py | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... src/beautyspot/core.py | ADR001 ✓ # Manage Executor Lifecycle via Instance Ownership and Weak References ## Conte...
LIFE | REQ007 ✓ キャッシュの有効期限を関数名パターンに基づくルールで設定できること。個別の関数に対して保持期間を指定でき、期限切れのキャッシュを自動的に無効化できること。 | ARCH007 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|マッチング要求| B[LifecyclePolicy]... | SPEC016 ✓ ## インターフェース ```python class LifecyclePolicy: def add_rule(self, pattern: st... | TST016 ✓ ## 目的 `LifecyclePolicy` はキャッシュデータの保持期間を関数名パターンで制御する。パターンマッチングの不備は、重要なキャッシュの意図しな... tests/unit/test_lifecycle.py | IMPL016 ✓ ## 実装概要 `LifecyclePolicy` クラスは `Rule` オブジェクトのリストを保持し、 関数名に基づいてキャッシュの保持期間を決定する。 ... src/beautyspot/lifecycle.py | ADR038 ✓ # Probabilistic Auto-Eviction for Storage Maintenance ## Context and Problem St...
LIMIT | REQ008 ✓ 関数の実行頻度をトークンバケット方式で制限し、外部APIの呼び出しレートを制御できること。同期・非同期の両方に対応すること。 | ARCH008 ✓ ## コンポーネント構成 ```mermaid graph TD A["@spot.consume"] -->|宣言的適用| B[LimiterProto... | SPEC018 ✓ ## インターフェース ```python class TokenBucket(LimiterProtocol): def consume(self,... | TST018 ✓ ## 目的 `TokenBucket` はAPI呼び出しやリソースアクセスのレート制限を実現する。レートリミッターの不具合はAPIの過負荷（制限が甘い場合）や... tests/unit/test_limiter.py | IMPL018 ✓ ## 実装概要 `LimiterProtocol` を実装する `TokenBucket` クラス。 GCRA (Generic Cell Rate Algo... src/beautyspot/limiter.py | ADR031 ✓ # Limiter Dependency Injection ## Context and Problem Statement / コンテキスト 以前、`c...
MAINT | REQ010 ✓ 期限切れキャッシュの削除、孤立Blobファイルのガベージコレクション、時間ベースの一括削除（prune）等のメンテナンス操作ができること。 | ARCH010 ✓ ## コンポーネント構成 ```mermaid graph TD A[MaintenanceService] -->|操作集約| B[TaskDBBase... | SPEC020 ✓ ## インターフェース ```python class MaintenanceService: def clean_garbage(self, gra... | TST020 ✓ ## 目的 `MaintenanceService` は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用... tests/unit/test_maintenance.py | IMPL020 ✓ ## 実装概要 `MaintenanceService` クラスがDBとBlobストレージ間の整合性チェック、 期限切れキャッシュの削除、孤立ファイルの検出と... src/beautyspot/maintenance.py | ADR018 ✓ # Expansion of Testing Strategy: Introduction of High-Level Integration Tests #...
MAINT | REQ010 ✓ 期限切れキャッシュの削除、孤立Blobファイルのガベージコレクション、時間ベースの一括削除（prune）等のメンテナンス操作ができること。 | ARCH010 ✓ ## コンポーネント構成 ```mermaid graph TD A[MaintenanceService] -->|操作集約| B[TaskDBBase... | SPEC020 ✓ ## インターフェース ```python class MaintenanceService: def clean_garbage(self, gra... | TST020 ✓ ## 目的 `MaintenanceService` は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用... tests/unit/test_maintenance.py | IMPL020 ✓ ## 実装概要 `MaintenanceService` クラスがDBとBlobストレージ間の整合性チェック、 期限切れキャッシュの削除、孤立ファイルの検出と... src/beautyspot/maintenance.py | ADR020 ✓ # Tolerant Deletion Policy for Task Cleanup ## Context and Problem Statement / ...
MAINT | REQ010 ✓ 期限切れキャッシュの削除、孤立Blobファイルのガベージコレクション、時間ベースの一括削除（prune）等のメンテナンス操作ができること。 | ARCH010 ✓ ## コンポーネント構成 ```mermaid graph TD A[MaintenanceService] -->|操作集約| B[TaskDBBase... | SPEC020 ✓ ## インターフェース ```python class MaintenanceService: def clean_garbage(self, gra... | TST020 ✓ ## 目的 `MaintenanceService` は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用... tests/unit/test_maintenance.py | IMPL020 ✓ ## 実装概要 `MaintenanceService` クラスがDBとBlobストレージ間の整合性チェック、 期限切れキャッシュの削除、孤立ファイルの検出と... src/beautyspot/maintenance.py | ADR021 ✓ # Code Visualization and Quality Metrics Strategy ## Context and Problem Statem...
MAINT | REQ010 ✓ 期限切れキャッシュの削除、孤立Blobファイルのガベージコレクション、時間ベースの一括削除（prune）等のメンテナンス操作ができること。 | ARCH010 ✓ ## コンポーネント構成 ```mermaid graph TD A[MaintenanceService] -->|操作集約| B[TaskDBBase... | SPEC020 ✓ ## インターフェース ```python class MaintenanceService: def clean_garbage(self, gra... | TST020 ✓ ## 目的 `MaintenanceService` は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用... tests/unit/test_maintenance.py | IMPL020 ✓ ## 実装概要 `MaintenanceService` クラスがDBとBlobストレージ間の整合性チェック、 期限切れキャッシュの削除、孤立ファイルの検出と... src/beautyspot/maintenance.py | ADR023 ✓ # Service Layer for Maintenance Operations ## Context and Problem Statement / コ...
MAINT | REQ010 ✓ 期限切れキャッシュの削除、孤立Blobファイルのガベージコレクション、時間ベースの一括削除（prune）等のメンテナンス操作ができること。 | ARCH010 ✓ ## コンポーネント構成 ```mermaid graph TD A[MaintenanceService] -->|操作集約| B[TaskDBBase... | SPEC020 ✓ ## インターフェース ```python class MaintenanceService: def clean_garbage(self, gra... | TST020 ✓ ## 目的 `MaintenanceService` は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用... tests/unit/test_maintenance.py | IMPL020 ✓ ## 実装概要 `MaintenanceService` クラスがDBとBlobストレージ間の整合性チェック、 期限切れキャッシュの削除、孤立ファイルの検出と... src/beautyspot/maintenance.py | ADR029 ✓ # Recursive Storage Cleanup and Zombie Project Collection ## Context and Proble...
MAINT | REQ010 ✓ 期限切れキャッシュの削除、孤立Blobファイルのガベージコレクション、時間ベースの一括削除（prune）等のメンテナンス操作ができること。 | ARCH010 ✓ ## コンポーネント構成 ```mermaid graph TD A[MaintenanceService] -->|操作集約| B[TaskDBBase... | SPEC020 ✓ ## インターフェース ```python class MaintenanceService: def clean_garbage(self, gra... | TST020 ✓ ## 目的 `MaintenanceService` は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用... tests/unit/test_maintenance.py | IMPL020 ✓ ## 実装概要 `MaintenanceService` クラスがDBとBlobストレージ間の整合性チェック、 期限切れキャッシュの削除、孤立ファイルの検出と... src/beautyspot/maintenance.py | ADR041 ✓ # Garbage Collection Strategy for Abandoned Temporary Files ## Context and Prob...
CLI | REQ011 ✓ コマンドラインインターフェースからキャッシュの一覧表示、詳細確認、統計表示、削除、GC等の管理操作ができること。 | ARCH011 ✓ ## コンポーネント構成 ```mermaid graph TD A[beautyspot CLI] -->|Command| B[Typer App] ... | SPEC021 ✓ ## インターフェース ```bash beautyspot list [--db DB_PATH] beautyspot gc [--name PROJEC... | TST021 ✓ ## 目的 CLI はユーザーがキャッシュの状況確認・管理を行う主要インターフェースであり、コマンドの不具合はユーザー体験と運用効率に直接影響する。`CliR... tests/integration/cli/test_cli.py | IMPL021 ✓ ## 実装概要 `Typer` を利用したコマンドラインインターフェースの実装。 `list`, `show`, `stats`, `clear`, `cle... src/beautyspot/cli.py | ADR027 ✓ # Pragmatic CLI Refactoring Policy ## Context and Problem Statement / コンテキスト `...
CLI | REQ011 ✓ コマンドラインインターフェースからキャッシュの一覧表示、詳細確認、統計表示、削除、GC等の管理操作ができること。 | ARCH011 ✓ ## コンポーネント構成 ```mermaid graph TD A[beautyspot CLI] -->|Command| B[Typer App] ... | SPEC021 ✓ ## インターフェース ```bash beautyspot list [--db DB_PATH] beautyspot gc [--name PROJEC... | TST021 ✓ ## 目的 CLI はユーザーがキャッシュの状況確認・管理を行う主要インターフェースであり、コマンドの不具合はユーザー体験と運用効率に直接影響する。`CliR... tests/integration/cli/test_cli.py | IMPL021 ✓ ## 実装概要 `Typer` を利用したコマンドラインインターフェースの実装。 `list`, `show`, `stats`, `clear`, `cle... src/beautyspot/cli.py | ADR028 ✓ # CLI Scope Definition and Explicit Storage Linkage ## Context and Problem Stat...
DI | REQ012 ✓ 全コンポーネント（DB、ストレージ、シリアライザ、リミッター等）が `Protocol/ABC` に基づく依存注入により差し替え可能であること。ファクトリ関数が... | ARCH012 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|構築| B[core.Spot] A ... | SPEC022 ✓ ## インターフェース ```python def Spot( name: str = "default", storage_path: st... | TST022 ✓ ## 目的 `bs.Spot()` ファクトリ関数は全コンポーネントの DI 配線を担う唯一のパブリックエントリポイントであり、デフォルト構成の正確性とカスタ... tests/integration/core/test_dependency_injection.py | IMPL022 ✓ ## 実装概要 `beautyspot` パッケージのメインエントリポイントとなる `Spot` ファクトリ関数の実装。 引数として渡された各コンポーネント（... src/beautyspot/__init__.py | ADR026 ✓ # Factory Function for Default Dependency Injection ## Context and Problem Stat...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC008 ✓ ## インターフェース ```python @runtime_checkable class TaskDBCore(Protocol): def ge... | TST008 ✓ ## 目的 `TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL... tests/unit/test_db_writer_queue.py | IMPL008 ✓ ## 実装概要 DB 層のプロトコル階層を定義。`TaskDBCore`（ランタイム必須の CRUD）、 `Flushable`（書き込み同期）、`Shutd... src/beautyspot/db.py | ADR010 ✓ # Msgpack Everywhere with Native BLOB Support ## Context and Problem Statement ...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC008 ✓ ## インターフェース ```python @runtime_checkable class TaskDBCore(Protocol): def ge... | TST008 ✓ ## 目的 `TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL... tests/unit/test_db_writer_queue.py | IMPL008 ✓ ## 実装概要 DB 層のプロトコル階層を定義。`TaskDBCore`（ランタイム必須の CRUD）、 `Flushable`（書き込み同期）、`Shutd... src/beautyspot/db.py | ADR043 ✓ # Serialize SQLite Writes with a Writer Queue ## Context and Problem Statement ...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC008 ✓ ## インターフェース ```python @runtime_checkable class TaskDBCore(Protocol): def ge... | TST008 ✓ ## 目的 `TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL... tests/unit/test_db_writer_queue.py | IMPL008 ✓ ## 実装概要 DB 層のプロトコル階層を定義。`TaskDBCore`（ランタイム必須の CRUD）、 `Flushable`（書き込み同期）、`Shutd... src/beautyspot/db.py | ADR045 ✓ # Introduce Explicit DB Queue Flushing ## Context and Problem Statement / コンテキス...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC008 ✓ ## インターフェース ```python @runtime_checkable class TaskDBCore(Protocol): def ge... | TST008 ✓ ## 目的 `TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL... tests/unit/test_db_writer_queue.py | IMPL008 ✓ ## 実装概要 DB 層のプロトコル階層を定義。`TaskDBCore`（ランタイム必須の CRUD）、 `Flushable`（書き込み同期）、`Shutd... src/beautyspot/db.py | ADR048 ✓ # SQLite ライタースレッドの死活監視におけるポーリング間隔の維持 ## Context and Problem Statement / コンテキスト ...
DB | REQ003 ✓ キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバック... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC008 ✓ ## インターフェース ```python @runtime_checkable class TaskDBCore(Protocol): def ge... | TST008 ✓ ## 目的 `TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL... tests/unit/test_db_writer_queue.py | IMPL008 ✓ ## 実装概要 DB 層のプロトコル階層を定義。`TaskDBCore`（ランタイム必須の CRUD）、 `Flushable`（書き込み同期）、`Shutd... src/beautyspot/db.py | ADR050 ✓ # Delegate Database Lifecycle Management to Caller ## Context and Problem State...
DB | REQ019 ✓ データベース操作が指定されたタイムアウト時間内に完了しない場合、呼び出し元を無期限にブロックせずにエラーを返し、システムの可用性を維持すること。特にSQLite... | ARCH003 ✓ ## コンポーネント構成 ```mermaid classDiagram class TaskDBCore { <<Protocol>> ... | SPEC008 ✓ ## インターフェース ```python @runtime_checkable class TaskDBCore(Protocol): def ge... | TST008 ✓ ## 目的 `TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL... tests/unit/test_db_writer_queue.py | IMPL008 ✓ ## 実装概要 DB 層のプロトコル階層を定義。`TaskDBCore`（ランタイム必須の CRUD）、 `Flushable`（書き込み同期）、`Shutd... src/beautyspot/db.py | ADR052 ✓ # SQLite Write Task Cancellation Strategy ## Context and Problem Statement / コン...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC002 ✓ ## インターフェース ```python @contextmanager def cached_run( self, *funcs: Any... | TST002 ✓ ## 目的 `cached_run` はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーテ... tests/integration/core/test_cached_run.py | IMPL002 ✓ ## 実装概要 `Spot.cached_run()` はコンテキストマネージャとして実装。 渡された関数群を内部で `mark()` と同等のラッピングを行... src/beautyspot/core.py | ADR013 ✓ # Rename Project to Spot and Task to Mark ## Context and Problem Statement / コン...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC002 ✓ ## インターフェース ```python @contextmanager def cached_run( self, *funcs: Any... | TST002 ✓ ## 目的 `cached_run` はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーテ... tests/integration/core/test_cached_run.py | IMPL002 ✓ ## 実装概要 `Spot.cached_run()` はコンテキストマネージャとして実装。 渡された関数群を内部で `mark()` と同等のラッピングを行... src/beautyspot/core.py | ADR015 ✓ # Strict Scoping for Imperative Execution (Runtime Guard) ## Context and Proble...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC002 ✓ ## インターフェース ```python @contextmanager def cached_run( self, *funcs: Any... | TST002 ✓ ## 目的 `cached_run` はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーテ... tests/integration/core/test_cached_run.py | IMPL002 ✓ ## 実装概要 `Spot.cached_run()` はコンテキストマネージャとして実装。 渡された関数群を内部で `mark()` と同等のラッピングを行... src/beautyspot/core.py | ADR034 ✓ # cached_run スコープ制限の廃止 ## Context and Problem Statement / コンテキスト ADR-0014 では、`...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC003 ✓ ## 概要 `@spot.mark()` デコレータは同期関数と非同期（`async def`）関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャ... | TST003 ✓ ## 目的 beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約（スレッドをまたぐコルーチン実... tests/integration/core/test_async_save.py | IMPL003 ✓ ## 実装概要 `Spot._execute_sync()` と `Spot._execute_async()` が実行エンジンの中核。 両メソッドは同一のフ... src/beautyspot/core.py | ADR013 ✓ # Rename Project to Spot and Task to Mark ## Context and Problem Statement / コン...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC003 ✓ ## 概要 `@spot.mark()` デコレータは同期関数と非同期（`async def`）関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャ... | TST003 ✓ ## 目的 beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約（スレッドをまたぐコルーチン実... tests/integration/core/test_async_save.py | IMPL003 ✓ ## 実装概要 `Spot._execute_sync()` と `Spot._execute_async()` が実行エンジンの中核。 両メソッドは同一のフ... src/beautyspot/core.py | ADR015 ✓ # Strict Scoping for Imperative Execution (Runtime Guard) ## Context and Proble...
CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC003 ✓ ## 概要 `@spot.mark()` デコレータは同期関数と非同期（`async def`）関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャ... | TST003 ✓ ## 目的 beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約（スレッドをまたぐコルーチン実... tests/integration/core/test_async_save.py | IMPL003 ✓ ## 実装概要 `Spot._execute_sync()` と `Spot._execute_async()` が実行エンジンの中核。 両メソッドは同一のフ... src/beautyspot/core.py | ADR034 ✓ # cached_run スコープ制限の廃止 ## Context and Problem Statement / コンテキスト ADR-0014 では、`...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC005 ✓ ## インターフェース ```python @singledispatch def canonicalize(obj: Any) -> Any: ... ``... | TST005 ✓ ## 目的 `canonicalize()` は引数の型ごとに正規化ルールを適用し、論理的に同値な入力が同じキャッシュキーを生成することを保証する。正規化の不... tests/unit/test_cachekey.py | IMPL005 ✓ ## 実装概要 `canonicalize()` は `functools.singledispatch` で実装された再帰的正規化関数。 型ごとにディスパッ... src/beautyspot/cachekey.py | ADR011 ✓ # Canonical Input Serialization with Msgpack and SHA-256 ## Context and Problem...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC005 ✓ ## インターフェース ```python @singledispatch def canonicalize(obj: Any) -> Any: ... ``... | TST005 ✓ ## 目的 `canonicalize()` は引数の型ごとに正規化ルールを適用し、論理的に同値な入力が同じキャッシュキーを生成することを保証する。正規化の不... tests/unit/test_cachekey.py | IMPL005 ✓ ## 実装概要 `canonicalize()` は `functools.singledispatch` で実装された再帰的正規化関数。 型ごとにディスパッ... src/beautyspot/cachekey.py | ADR022 ✓ # Refactoring Complex Modules with Single Dispatch ## Context and Problem State...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC006 ✓ ## インターフェース ```python class Strategy(Enum): DEFAULT = auto() # 再帰的正規化 ... | TST006 ✓ ## 目的 `KeyGenPolicy` は引数ごとのキー生成戦略をカスタマイズする機構であり、ログレベルやデバッグフラグのような非決定的引数をキーから除外し... tests/unit/test_cachekey.py | IMPL006 ✓ ## 実装概要 `Strategy` enum（DEFAULT, IGNORE, FILE_CONTENT, PATH_STAT の4種）と `KeyGenP... src/beautyspot/cachekey.py | ADR011 ✓ # Canonical Input Serialization with Msgpack and SHA-256 ## Context and Problem...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC006 ✓ ## インターフェース ```python class Strategy(Enum): DEFAULT = auto() # 再帰的正規化 ... | TST006 ✓ ## 目的 `KeyGenPolicy` は引数ごとのキー生成戦略をカスタマイズする機構であり、ログレベルやデバッグフラグのような非決定的引数をキーから除外し... tests/unit/test_cachekey.py | IMPL006 ✓ ## 実装概要 `Strategy` enum（DEFAULT, IGNORE, FILE_CONTENT, PATH_STAT の4種）と `KeyGenP... src/beautyspot/cachekey.py | ADR022 ✓ # Refactoring Complex Modules with Single Dispatch ## Context and Problem State...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC010 ✓ ## インターフェース ```python class S3Storage(BlobStorageMaintenable): def __init__... | TST010 ✓ ## 目的 `S3Storage` はクラウド環境での大規模Blob保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが... tests/integration/storage/test_s3.py | IMPL010 ✓ ## 実装概要 `S3Storage` クラスが `BlobStorageBase` を実装。 `s3://bucket/prefix/` 形式の URI を... src/beautyspot/storage.py | ADR044 ✓ # Drop Absolute Path Support in LocalStorage ## Context and Problem Statement /...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC010 ✓ ## インターフェース ```python class S3Storage(BlobStorageMaintenable): def __init__... | TST010 ✓ ## 目的 `S3Storage` はクラウド環境での大規模Blob保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが... tests/integration/storage/test_s3.py | IMPL010 ✓ ## 実装概要 `S3Storage` クラスが `BlobStorageBase` を実装。 `s3://bucket/prefix/` 形式の URI を... src/beautyspot/storage.py | ADR046 ✓ # Delegate Workspace Initialization to Storage Backends ## Context and Problem ...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC011 ✓ ## インターフェース ```python class StoragePolicyProtocol(Protocol): def should_sav... | TST011 ✓ ## 目的 ストレージポリシーは「データをDB直接保存するか、Blobストレージに分離するか」を決定する戦略レイヤーである。閾値判定の誤りはDBの肥大化（性能... tests/unit/test_storage_policy.py | IMPL011 ✓ ## 実装概要 `StoragePolicyProtocol` が `should_save_as_blob(data: bytes) -> bool` を定... src/beautyspot/storage.py | ADR044 ✓ # Drop Absolute Path Support in LocalStorage ## Context and Problem Statement /...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC011 ✓ ## インターフェース ```python class StoragePolicyProtocol(Protocol): def should_sav... | TST011 ✓ ## 目的 ストレージポリシーは「データをDB直接保存するか、Blobストレージに分離するか」を決定する戦略レイヤーである。閾値判定の誤りはDBの肥大化（性能... tests/unit/test_storage_policy.py | IMPL011 ✓ ## 実装概要 `StoragePolicyProtocol` が `should_save_as_blob(data: bytes) -> bool` を定... src/beautyspot/storage.py | ADR046 ✓ # Delegate Workspace Initialization to Storage Backends ## Context and Problem ...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC024 ✓ ## インターフェース ```python class BlobStorageBase(ABC): @abstractmethod def s... | TST024 ✓ ## 目的 `BlobStorageBase` は全ストレージバックエンド（LocalStorage, S3Storage, サードパーティ）が 準拠すべき抽... tests/integration/storage/test_blob_storage_base.py | IMPL024 ✓ ## 実装概要 `storage.py` 内の `BlobStorageBase` ABC が SPEC024 の中核実装。 5つの抽象メソッド（`save`... src/beautyspot/storage.py | ADR044 ✓ # Drop Absolute Path Support in LocalStorage ## Context and Problem Statement /...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC024 ✓ ## インターフェース ```python class BlobStorageBase(ABC): @abstractmethod def s... | TST024 ✓ ## 目的 `BlobStorageBase` は全ストレージバックエンド（LocalStorage, S3Storage, サードパーティ）が 準拠すべき抽... tests/integration/storage/test_blob_storage_base.py | IMPL024 ✓ ## 実装概要 `storage.py` 内の `BlobStorageBase` ABC が SPEC024 の中核実装。 5つの抽象メソッド（`save`... src/beautyspot/storage.py | ADR046 ✓ # Delegate Workspace Initialization to Storage Backends ## Context and Problem ...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC013 ✓ ## インターフェース ```python def register( code: int, encoder: Callable[[Any],... | TST013 ✓ ## 目的 カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッ... tests/unit/test_type_registry.py | IMPL013 ✓ ## 実装概要 2つのレイヤーで構成: - `MsgpackSerializer.register(type, code, encoder, decoder)... src/beautyspot/serializer.py, src/beautyspot/core.py | ADR008 ✓ # Customizable Serialization Strategy with Msgpack Default ## Context and Probl...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC013 ✓ ## インターフェース ```python def register( code: int, encoder: Callable[[Any],... | TST013 ✓ ## 目的 カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッ... tests/unit/test_type_registry.py | IMPL013 ✓ ## 実装概要 2つのレイヤーで構成: - `MsgpackSerializer.register(type, code, encoder, decoder)... src/beautyspot/serializer.py, src/beautyspot/core.py | ADR014 ✓ # Decorator-based Type Registration with Late Binding ## Context and Problem St...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC013 ✓ ## インターフェース ```python def register( code: int, encoder: Callable[[Any],... | TST013 ✓ ## 目的 カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッ... tests/unit/test_type_registry.py | IMPL013 ✓ ## 実装概要 2つのレイヤーで構成: - `MsgpackSerializer.register(type, code, encoder, decoder)... src/beautyspot/serializer.py, src/beautyspot/core.py | ADR017 ✓ title: Nested Msgpack Protocol via Serializer Wrapping status: Accepted date: 20...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC013 ✓ ## インターフェース ```python def register( code: int, encoder: Callable[[Any],... | TST013 ✓ ## 目的 カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッ... tests/unit/test_type_registry.py | IMPL013 ✓ ## 実装概要 2つのレイヤーで構成: - `MsgpackSerializer.register(type, code, encoder, decoder)... src/beautyspot/serializer.py, src/beautyspot/core.py | ADR019 ✓ # Per-Task Serializer Override ## Context and Problem Statement / コンテキスト プロジェク...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC013 ✓ ## インターフェース ```python def register( code: int, encoder: Callable[[Any],... | TST013 ✓ ## 目的 カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッ... tests/unit/test_type_registry.py | IMPL013 ✓ ## 実装概要 2つのレイヤーで構成: - `MsgpackSerializer.register(type, code, encoder, decoder)... src/beautyspot/serializer.py, src/beautyspot/core.py | ADR025 ✓ # Protocol-based Serializer Registration ## Context and Problem Statement / コンテ...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC013 ✓ ## インターフェース ```python def register( code: int, encoder: Callable[[Any],... | TST013 ✓ ## 目的 カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッ... tests/unit/test_type_registry.py | IMPL013 ✓ ## 実装概要 2つのレイヤーで構成: - `MsgpackSerializer.register(type, code, encoder, decoder)... src/beautyspot/serializer.py, src/beautyspot/core.py | ADR039 ✓ # Bounded LRU Cache for Subclass Resolution in Serializer ## Context and Proble...
SERIAL | REQ005 ✓ 関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。 | ARCH005 ✓ ## コンポーネント構成 ```mermaid graph TD A[MsgpackSerializer] -->|Registry| B[TypeReg... | SPEC013 ✓ ## インターフェース ```python def register( code: int, encoder: Callable[[Any],... | TST013 ✓ ## 目的 カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッ... tests/unit/test_type_registry.py | IMPL013 ✓ ## 実装概要 2つのレイヤーで構成: - `MsgpackSerializer.register(type, code, encoder, decoder)... src/beautyspot/serializer.py, src/beautyspot/core.py | ADR042 ✓ # Thread-safe LRU Cache for Serializer ## Context and Problem Statement / コンテキス...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR024 ✓ # Non-blocking Cache Persistence and Task Tracking ## Context and Problem State...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR033 ✓ # 非同期保存処理におけるエラー可視化とコンテキストの導入 ## Context and Problem Statement / コンテキスト beauty...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR036 ✓ # バックグラウンドループのライフサイクル管理とGC時のデータロスト対策 ## Context and Problem Statement / コンテキスト ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR037 ✓ # GC時のデータロスト防止とバックグラウンドループのシャットダウン戦略 ## Context and Problem Statement / コンテキスト ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR040 ✓ # Background Loop Shutdown Strategy and Threading Model ## Context and Problem ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR047 ✓ # Background Loop Shutdown Strategy on Garbage Collection ## Context and Proble...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR049 ✓ # 実行中タスクの強参照セットによる追跡 ## Context and Problem Statement / コンテキスト `Spot` インスタンスは、...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR053 ✓ # Background Loop and Executor Shutdown Sequence ## Context and Problem Stateme...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... tests/integration/core/test_exit_drain.py | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... src/beautyspot/core.py | ADR001 ✓ # Manage Executor Lifecycle via Instance Ownership and Weak References ## Conte...
LIFE | REQ007 ✓ キャッシュの有効期限を関数名パターンに基づくルールで設定できること。個別の関数に対して保持期間を指定でき、期限切れのキャッシュを自動的に無効化できること。 | ARCH007 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|マッチング要求| B[LifecyclePolicy]... | SPEC017 ✓ ## インターフェース ```python def parse_retention(val: Any) -> float | None: ... ``` #... | TST017 ✓ ## 目的 `Retention` はキャッシュの有効期間をユーザーフレンドリーな形式で指定する値オブジェクトである。パースの不備は意図しない保持期間（例: ... tests/unit/test_lifecycle_extended.py | IMPL017 ✓ ## 実装概要 `Retention` クラスを名前空間として使用し、保持期間のパースや 特殊な定数（`INDEFINITE`, `FOREVER`）を管理す... src/beautyspot/lifecycle.py | ADR038 ✓ # Probabilistic Auto-Eviction for Storage Maintenance ## Context and Problem St...

## カバレッジ

リンク方向 | グループ | カバー数 | カバー率 | 未カバーアイテム
--- | --- | --- | --- | ---
ARCH → REQ | — | 14 / 14 | 100.0% | —
ARCH → REQ | BGIO | 1 / 1 | 100.0% | —
ARCH → REQ | CACHE | 1 / 1 | 100.0% | —
ARCH → REQ | CLI | 1 / 1 | 100.0% | —
ARCH → REQ | DB | 2 / 2 | 100.0% | —
ARCH → REQ | DI | 1 / 1 | 100.0% | —
ARCH → REQ | HERD | 1 / 1 | 100.0% | —
ARCH → REQ | HOOK | 1 / 1 | 100.0% | —
ARCH → REQ | KEY | 1 / 1 | 100.0% | —
ARCH → REQ | LIFE | 1 / 1 | 100.0% | —
ARCH → REQ | LIMIT | 1 / 1 | 100.0% | —
ARCH → REQ | MAINT | 1 / 1 | 100.0% | —
ARCH → REQ | SERIAL | 1 / 1 | 100.0% | —
ARCH → REQ | STORE | 1 / 1 | 100.0% | —
SPEC → ARCH | — | 13 / 13 | 100.0% | —
SPEC → ARCH | BGIO | 1 / 1 | 100.0% | —
SPEC → ARCH | CACHE | 1 / 1 | 100.0% | —
SPEC → ARCH | CLI | 1 / 1 | 100.0% | —
SPEC → ARCH | DB | 1 / 1 | 100.0% | —
SPEC → ARCH | DI | 1 / 1 | 100.0% | —
SPEC → ARCH | HERD | 1 / 1 | 100.0% | —
SPEC → ARCH | HOOK | 1 / 1 | 100.0% | —
SPEC → ARCH | KEY | 1 / 1 | 100.0% | —
SPEC → ARCH | LIFE | 1 / 1 | 100.0% | —
SPEC → ARCH | LIMIT | 1 / 1 | 100.0% | —
SPEC → ARCH | MAINT | 1 / 1 | 100.0% | —
SPEC → ARCH | SERIAL | 1 / 1 | 100.0% | —
SPEC → ARCH | STORE | 1 / 1 | 100.0% | —
TST → SPEC | — | 24 / 24 | 100.0% | —
TST → SPEC | BGIO | 2 / 2 | 100.0% | —
TST → SPEC | CACHE | 3 / 3 | 100.0% | —
TST → SPEC | CLI | 1 / 1 | 100.0% | —
TST → SPEC | DB | 2 / 2 | 100.0% | —
TST → SPEC | DI | 1 / 1 | 100.0% | —
TST → SPEC | HERD | 1 / 1 | 100.0% | —
TST → SPEC | HOOK | 1 / 1 | 100.0% | —
TST → SPEC | KEY | 3 / 3 | 100.0% | —
TST → SPEC | LIFE | 2 / 2 | 100.0% | —
TST → SPEC | LIMIT | 1 / 1 | 100.0% | —
TST → SPEC | MAINT | 1 / 1 | 100.0% | —
TST → SPEC | SERIAL | 2 / 2 | 100.0% | —
TST → SPEC | STORE | 4 / 4 | 100.0% | —
IMPL → SPEC | — | 24 / 24 | 100.0% | —
IMPL → SPEC | BGIO | 2 / 2 | 100.0% | —
IMPL → SPEC | CACHE | 3 / 3 | 100.0% | —
IMPL → SPEC | CLI | 1 / 1 | 100.0% | —
IMPL → SPEC | DB | 2 / 2 | 100.0% | —
IMPL → SPEC | DI | 1 / 1 | 100.0% | —
IMPL → SPEC | HERD | 1 / 1 | 100.0% | —
IMPL → SPEC | HOOK | 1 / 1 | 100.0% | —
IMPL → SPEC | KEY | 3 / 3 | 100.0% | —
IMPL → SPEC | LIFE | 2 / 2 | 100.0% | —
IMPL → SPEC | LIMIT | 1 / 1 | 100.0% | —
IMPL → SPEC | MAINT | 1 / 1 | 100.0% | —
IMPL → SPEC | SERIAL | 2 / 2 | 100.0% | —
IMPL → SPEC | STORE | 4 / 4 | 100.0% | —
ADR → REQ | — | 13 / 14 | 92.9% | REQ013
ADR → REQ | BGIO | 1 / 1 | 100.0% | —
ADR → REQ | CACHE | 1 / 1 | 100.0% | —
ADR → REQ | CLI | 1 / 1 | 100.0% | —
ADR → REQ | DB | 2 / 2 | 100.0% | —
ADR → REQ | DI | 1 / 1 | 100.0% | —
ADR → REQ | HERD | 0 / 1 | 0.0% | REQ013
ADR → REQ | HOOK | 1 / 1 | 100.0% | —
ADR → REQ | KEY | 1 / 1 | 100.0% | —
ADR → REQ | LIFE | 1 / 1 | 100.0% | —
ADR → REQ | LIMIT | 1 / 1 | 100.0% | —
ADR → REQ | MAINT | 1 / 1 | 100.0% | —
ADR → REQ | SERIAL | 1 / 1 | 100.0% | —
ADR → REQ | STORE | 1 / 1 | 100.0% | —

## アイテム詳細

ソート:

### REQ003 {h(g)} ✓ レビュー済 局所ビュー →

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

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

キャッシュのメタデータ（関数名、入力ID、バージョン、有効期限等）をデータベースに永続化できること。 抽象インターフェースにより、DBやストレージなどのバックエンド実装を差し替え可能であること。

親: —

子: ADR007, ADR010, ADR043, ADR045, ADR048, ADR050, ARCH003

### REQ019 {h(g)} ✓ レビュー済 局所ビュー →

データベース操作が指定されたタイムアウト時間内に完了しない場合、呼び出し元を無期限にブロックせずにエラーを返し、システムの可用性を維持すること。特にSQLiteの書き込みスレッドがスタックした場合でも、フェイルファストによって制御を戻せること。

データベース操作が指定されたタイムアウト時間内に完了しない場合、呼び出し元を無期限にブロックせずにエラーを返し、システムの可用性を維持すること。特にSQLiteの書き込みスレッドがスタックした場合でも、フェイルファストによって制御を戻せること。

親: —

子: ADR051, ADR052, ARCH003, TST007

### REQ001 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR012, ADR013, ADR015, ADR034, ARCH001

### REQ002 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR002, ADR011, ADR022, ARCH002

### REQ004 {h(g)} ✓ レビュー済 局所ビュー →

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

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

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

親: —

子: ADR030, ADR044, ADR046, ARCH004

### REQ005 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR004, ADR008, ADR014, ADR017, ADR019, ADR025, ADR039, ADR042, ARCH005

### REQ006 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR001, ADR005, ADR024, ADR033, ADR036, ADR037, ADR040, ADR047, ADR049, ADR053, ARCH006

### REQ007 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR032, ADR038, ARCH007

### REQ008 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR003, ADR031, ARCH008

### REQ009 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR035, ARCH009

### REQ010 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR016, ADR018, ADR020, ADR021, ADR023, ADR029, ADR041, ARCH010

### REQ011 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR006, ADR027, ADR028, ARCH011

### REQ012 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ADR009, ADR026, ARCH012

### REQ013 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: ARCH013

### REQ014 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### REQ015 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### REQ016 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### REQ017 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### REQ018 {h(g)} ✓ レビュー済 局所ビュー →

- Beautyspot: 本キャッシュライブラリの名称。

- Spot: 本ライブラリにおけるキャッシュ・レート制限のコアエンジンの名称。

- キャッシュキー (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` | 共通定義（用語集など、特定の機能に縛られないもの）

- **Beautyspot**: 本キャッシュライブラリの名称。 - **Spot**: 本ライブラリにおけるキャッシュ・レート制限のコアエンジンの名称。 - **キャッシュキー (Cache Key)**: 関数の入力引数等から一意に生成される識別子。 - **キャッシュヒット**: 保存された結果を再利用し、元の関数を実行しないこと。 - **キャッシュミス**: 結果が未保存または無効のため、元の関数を実行すること。 - **メタデータDB (Metadata DB)**: キャッシュの有効期限、サイズ、関数名などの付帯情報を保存するデータストア。 - **Blobストレージ (Blob Storage)**: キャッシュの実際のペイロード（戻り値のシリアライズ結果）を保存するストレージ。 - **シリアライザ (Serializer)**: 関数の戻り値をストレージに保存可能なバイト列等に変換、および復元するコンポーネント。 - **バックグラウンドIO (Background IO)**: メインスレッド（またはタスク）の実行をブロックせず、非同期にキャッシュ保存などを行う仕組み。 - **レート制限 (Rate Limiting)**: 外部API呼び出し等の頻度を制御する仕組み。トークンバケット方式を採用。 - **Thundering Herd**: キャッシュミス時に、多数の並行リクエストが同時に同一関数を実行しようとして過負荷になる現象。 - **フック (Hook)**: キャッシュのライフサイクル（ヒット、ミス、保存前後等）に介入できるコールバック機構。 - **依存注入 (Dependency Injection / DI)**: 各コンポーネント（DB、ストレージ等）を外部から差し替え可能にする設計パターン。 ### 仕様書の機能グループ (Groups) 仕様書アイテムに付与される `groups` 属性の意味は以下の通りです。 <table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>グループ名</p></th><th colspan="1" rowspan="1"><p>意味 / 対象領域</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>CACHE</code></p></td><td colspan="1" rowspan="1"><p>関数キャッシュの基本機能（同期・非同期のデコレータなど）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>KEY</code></p></td><td colspan="1" rowspan="1"><p>キャッシュキーの生成ロジック（引数のハッシュ化など）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>DB</code></p></td><td colspan="1" rowspan="1"><p>メタデータDB（有効期限、メタ情報の管理）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>STORE</code></p></td><td colspan="1" rowspan="1"><p>Blobストレージ（ペイロードの保存）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>SERIAL</code></p></td><td colspan="1" rowspan="1"><p>シリアライゼーション（データの変換処理）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>BGIO</code></p></td><td colspan="1" rowspan="1"><p>バックグラウンドIO（非同期書き込み処理）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>LIFE</code></p></td><td colspan="1" rowspan="1"><p>ライフサイクル管理（有効期限の判定、無効化ルールなど）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>LIMIT</code></p></td><td colspan="1" rowspan="1"><p>レート制限（トークンバケット方式等）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>HOOK</code></p></td><td colspan="1" rowspan="1"><p>コールバックフックの仕組み</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>MAINT</code></p></td><td colspan="1" rowspan="1"><p>メンテナンス（キャッシュの削除、ガベージコレクション）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>CLI</code></p></td><td colspan="1" rowspan="1"><p>コマンドラインインターフェース</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>DI</code></p></td><td colspan="1" rowspan="1"><p>依存注入の仕組みとデフォルトファクトリ</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>HERD</code></p></td><td colspan="1" rowspan="1"><p>Thundering Herd対策（直列化、重複実行防止）</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>COMMON</code></p></td><td colspan="1" rowspan="1"><p>共通定義（用語集など、特定の機能に縛られないもの）</p></td></tr></tbody></table>

親: —

子: —

### ARCH003 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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`）は実装に依存しない。

## コンポーネント構成 ```mermaid 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` 実装 | ## データフロー ```mermaid 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`）は実装に依存しない。

親: REQ003, REQ019

子: SPEC007, SPEC008

### ARCH001 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

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

## コンポーネント責務

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

## データフロー

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

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

## 技術選定

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

## 非機能要件方針

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

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

親: REQ001

子: SPEC001, SPEC002, SPEC003

### ARCH002 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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` を定義可能にし、柔軟なキャッシュ戦略をサポートする。

## コンポーネント構成 ```mermaid 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)` | ## データフロー ```mermaid 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` を定義可能にし、柔軟なキャッシュ戦略をサポートする。

親: REQ002

子: SPEC004, SPEC005, SPEC006

### ARCH004 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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超のマルチパートアップロードに自動対応

## コンポーネント構成 ```mermaid 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` | ## データフロー ```mermaid 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超のマルチパートアップロードに自動対応

親: REQ004

子: SPEC009, SPEC010, SPEC011, SPEC024

### ARCH005 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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` 未登録エラーを検知し、互換性問題を明示的に報告

## コンポーネント構成 ```mermaid 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) | ## データフロー ```mermaid 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` 未登録エラーを検知し、互換性問題を明示的に報告

親: REQ005

子: SPEC012, SPEC013

### ARCH006 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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()` により、クリティカルな処理の直後での保存完了を保証

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

## コンポーネント構成 ```mermaid 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()` | ## データフロー ```mermaid 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()` により、クリティカルな処理の直後での保存完了を保証 - **安全性**: シャットダウンシーケンス中に新規タスクを受け付けない排他制御を導入

親: REQ006

子: SPEC014, SPEC015

### ARCH007 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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` 等の定数により、グローバルポリシーの個別上書きに対応

## コンポーネント構成 ```mermaid 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()` | ## データフロー ```mermaid 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` 等の定数により、グローバルポリシーの個別上書きに対応

親: REQ007

子: SPEC016, SPEC017

### ARCH008 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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` により、マルチスレッド環境での二重消費を防止

## コンポーネント構成 ```mermaid 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` | ## データフロー ```mermaid 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` により、マルチスレッド環境での二重消費を防止

親: REQ008

子: SPEC018

### ARCH009 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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=[...]` で指定可能

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

## コンポーネント構成 ```mermaid 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` 等 | ## データフロー ```mermaid 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=[...]` で指定可能 - **パフォーマンス**: フック未登録時のオーバーヘッドを最小化するガード条件を実装

親: REQ009

子: SPEC019

### ARCH010 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

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

## コンポーネント構成 ```mermaid 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プロセス) ```mermaid 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の削除の順序を徹底し、参照エラーを防止 - **可用性**: メンテナンス中も通常のキャッシュ読み取りを妨げないロック粒度 - **観測性**: 削除されたタスク数や解放された容量を統計として返却

親: REQ010

子: SPEC020

### ARCH011 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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を横断的にスキャンする機能を提供

## コンポーネント構成 ```mermaid 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` | ## データフロー ```mermaid 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を横断的にスキャンする機能を提供

親: REQ011

子: SPEC021

### ARCH012 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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や仮装ストレージを注入することで、高速な単体テストが可能
- 拡張性: ユーザーが独自のシリアライザやストレージを自作して注入可能

## コンポーネント構成 ```mermaid 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や仮装ストレージを注入することで、高速な単体テストが可能 - **拡張性**: ユーザーが独自のシリアライザやストレージを自作して注入可能

親: REQ012

子: SPEC022

### ARCH013 {h(g)} ✓ レビュー済 局所ビュー →

## コンポーネント構成

    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による消失を防止

## コンポーネント構成 ```mermaid 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()` ## データフロー ```mermaid 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による消失を防止

親: REQ013

子: SPEC023

### ARCH014 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### ARCH015 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### SPEC007 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

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

## 振る舞い

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

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

### 読み取り並行性

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

### スキーマ管理

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

### タイムアウトとフェイルファスト

- 全ての書き込み操作は `timeout` 秒以内に完了する必要がある。
- PENDING（未着手）状態: `timeout` を超えた場合、タスクはキューから破棄（キャンセル）され、呼び出し元に `TimeoutError` を返す。
- RUNNING（実行中）状態: `timeout` を超えた場合、`sqlite3.Connection.interrupt()` を用いて実行中のクエリを中断し、呼び出し元に `TimeoutError` を返す。これにより、ライタースレッドの恒久的なハングを防ぎ、システムの可用性を維持する。

## パラメータ詳細

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

## エラーハンドリング

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

## エッジケース

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

## インターフェース ```python class SQLiteTaskDB(TaskDBBase): def __init__(self, db_path: str | Path, timeout: float = 30.0): ... def save(self, task: TaskRecord) -> None: ... def get(self, cache_key: str, *, include_expired: bool = False) -> TaskRecord | None: ... ``` ## 振る舞い ### ライタースレッド方式 - 書き込み操作は全て `_WriteTask` キューに投入され、専用のバックグラウンドスレッドで直列に実行される。これにより SQLite の `database is locked` エラーを回避する。 ### 読み取り並行性 - 読み取りはスレッドローカルな接続を使用し、`PRAGMA query_only = ON` 状態で並行して実行される。 ### スキーマ管理 - 起動時に `tasks` テーブルの存在を確認し、必要に応じて自動的に `ALTER TABLE` によるカラム追加（マイグレーション）を行う。 ### タイムアウトとフェイルファスト - 全ての書き込み操作は `timeout` 秒以内に完了する必要がある。 - **PENDING（未着手）状態**: `timeout` を超えた場合、タスクはキューから破棄（キャンセル）され、呼び出し元に `TimeoutError` を返す。 - **RUNNING（実行中）状態**: `timeout` を超えた場合、`sqlite3.Connection.interrupt()` を用いて実行中のクエリを中断し、呼び出し元に `TimeoutError` を返す。これにより、ライタースレッドの恒久的なハングを防ぎ、システムの可用性を維持する。 ## パラメータ詳細 | 設定 | デフォルト | 説明 | |---|---|---| | `journal_mode` | `WAL` | 書き込みと読み取りの並行性を高める設定 | | `synchronous` | `NORMAL` | パフォーマンスと耐久性のバランス設定 | | `timeout` | `30.0` | データベースロック待機およびクエリ実行のタイムアウト閾値 | ## エラーハンドリング - **タイムアウト**: `TimeoutError` を送出する。 - **接続失敗**: `sqlite3.Error` をキャッチし、適切にラップして再送出する。 - **シャットダウン時**: ライタースレッドへの新規タスク投入を停止し、保留中のタスクを完了させてから接続を閉じる。 ## エッジケース - **破損DB**: DBファイルが壊れている場合は、初期化時に検知しエラーとする。 - **インメモリDB**: `:memory:` 指定時は、プロセスの生存期間中のみキャッシュが保持される。

親: ARCH003

子: IMPL007, TST007

### SPEC023 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    class CacheManager:
        def herd_sync(
            self, key: str, serializer: Optional[SerializerProtocol] = None
        ) -> Generator[HerdWaitResult, None, None]: ...

        async def herd_async(
            self,
            key: str,
            serializer: Optional[SerializerProtocol],
            loop: asyncio.AbstractEventLoop,
            executor: Any,
        ) -> AsyncGenerator[HerdWaitResult, None]: ...

## 振る舞い

### 同時実行制御

1. キャッシュキーをキーとした `_inflight` 辞書をチェックする
2. 存在しない場合: `HerdWaitResult` を新規作成し、自分が「実行者 (Executor)」となる
3. 存在する場合: 既存の `HerdWaitResult` を返し、自分は「待機者 (Waiter)」となる
4. 完了通知: 実行者が処理を終えたら、`HerdWaitResult` の Event (または Future) をセットし、結果を共有する
5. 自動クリーンアップ: `with` または `async with` ブロックを抜ける際、実行者は自動的に `notify_and_cleanup_inflight` を呼び出し、インフライト状態を解除する。これにより例外発生時も状態が残留しないことが保証される。

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

#### 目的

実行中のタスク状態（イベント・結果・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. 作成: `herd_sync` / `herd_async` コンテキスト開始時に `_inflight` にキーが存在しない場合、 `_inflight_lock` を保持した状態で新規タプルを挿入する
2. 保持: 実行者が処理を完了するまで、`_inflight` 辞書がタプルへの唯一の管理参照を保持する。 待機者はロック取得時にタプル要素への参照を取得するが、`_inflight` エントリが正規の所有者である
3. 解放: コンテキスト終了（`__exit__` / `__aexit__`）時に `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` | 実行者が失敗またはタイムアウトした際のリトライ回数

## エラーハンドリング

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

## エッジケース

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

## インターフェース ```python class CacheManager: def herd_sync( self, key: str, serializer: Optional[SerializerProtocol] = None ) -> Generator[HerdWaitResult, None, None]: ... async def herd_async( self, key: str, serializer: Optional[SerializerProtocol], loop: asyncio.AbstractEventLoop, executor: Any, ) -> AsyncGenerator[HerdWaitResult, None]: ... ``` ## 振る舞い ### 同時実行制御 1. キャッシュキーをキーとした `_inflight` 辞書をチェックする 2. **存在しない場合**: `HerdWaitResult` を新規作成し、自分が「実行者 (Executor)」となる 3. **存在する場合**: 既存の `HerdWaitResult` を返し、自分は「待機者 (Waiter)」となる 4. **完了通知**: 実行者が処理を終えたら、`HerdWaitResult` の Event (または Future) をセットし、結果を共有する 5. **自動クリーンアップ**: `with` または `async with` ブロックを抜ける際、実行者は自動的に `notify_and_cleanup_inflight` を呼び出し、インフライト状態を解除する。これにより例外発生時も状態が残留しないことが保証される。 ### 強参照によるインフライト状態の保持 #### 目的 実行中のタスク状態（イベント・結果・Future）が GC によって消失しないよう、 `_inflight` 辞書を通じて強参照で保持する。WeakRef は使用しない。 #### データ構造 `_inflight` 辞書はキャッシュキーをキーとし、以下の3要素タプルを値として保持する: ```python _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. **作成**: `herd_sync` / `herd_async` コンテキスト開始時に `_inflight` にキーが存在しない場合、 `_inflight_lock` を保持した状態で新規タプルを挿入する 2. **保持**: 実行者が処理を完了するまで、`_inflight` 辞書がタプルへの唯一の管理参照を保持する。 待機者はロック取得時にタプル要素への参照を取得するが、`_inflight` エントリが正規の所有者である 3. **解放**: コンテキスト終了（`__exit__` / `__aexit__`）時に `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` | 実行者が失敗またはタイムアウトした際のリトライ回数 | ## エラーハンドリング - **実行者失敗**: 実行者が例外を投げた場合、待機者全員にも同じ例外が伝播される。 - **タイムアウト**: タイムアウト時は待機を解除し、自らが実行者となって再試行（またはエラー）を試みる。 ## エッジケース - **非同期混在**: 同一キーに対して同期実行と非同期実行が混在する場合も、スレッドセーフなロックにより一つの実行に絞り込まれる。

親: ARCH013

子: IMPL023, TST023

### SPEC001 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

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

## 振る舞い

### 基本フロー

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

### sync/async 自動判定

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

## パラメータ詳細

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

## エラーハンドリング

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

## エッジケース

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

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

親: ARCH001

子: IMPL001, TST001

### SPEC002 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

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

## 振る舞い

### 基本フロー

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

## パラメータ詳細

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

## 戻り値（Yields）の仕様

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

## エラーハンドリング

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

## エッジケース

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

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

親: ARCH001

子: IMPL002, TST002

### SPEC003 {h(g)} ✓ レビュー済 局所ビュー →

## 概要

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

## 振る舞い

### sync/async の自動判定

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

### 実行フロー

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

## エラーハンドリング

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

## エッジケース

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

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

親: ARCH001

子: IMPL003, TST003

### SPEC004 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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()` はオブジェクトによっては実行ごとに変わる可能性があるため、明示的な正規化が推奨される

## インターフェース ```python 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()` はオブジェクトによっては実行ごとに変わる可能性があるため、明示的な正規化が推奨される

親: ARCH002

子: IMPL004, TST004

### SPEC005 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    @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`）が辞書のキーに含まれていても安定したソート順を保証する

## インターフェース ```python @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`）が辞書のキーに含まれていても安定したソート順を保証する

親: ARCH002

子: IMPL005, TST005

### SPEC006 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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` を送出する（実行前バリデーション）
- デフォルト値の変更: 関数のデフォルト値が変わると、同じ引数でもハッシュ値が変わる（意図的な挙動）

## インターフェース ```python 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` を送出する（実行前バリデーション） - **デフォルト値の変更**: 関数のデフォルト値が変わると、同じ引数でもハッシュ値が変わる（意図的な挙動）

親: ARCH002

子: IMPL006, TST006

### SPEC008 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    @runtime_checkable
    class TaskDBCore(Protocol):
        def get(self, cache_key: str, *, include_expired: bool = False) -> 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` 構成プロトコルとして扱う。

## インターフェース ```python @runtime_checkable class TaskDBCore(Protocol): def get(self, cache_key: str, *, include_expired: bool = False) -> 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` 構成プロトコルとして扱う。

親: ARCH003

子: IMPL008, TST008

### SPEC009 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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パス: パス区切り文字として `/` と `\` の両方を検証対象とする

## インターフェース ```python 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パス**: パス区切り文字として `/` と `\` の両方を検証対象とする

親: ARCH004

子: IMPL009, TST009

### SPEC010 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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 は存在しないオブジェクトの削除要求に対してエラーを返さないため、常に成功として扱う

## インターフェース ```python 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 は存在しないオブジェクトの削除要求に対してエラーを返さないため、常に成功として扱う

親: ARCH004

子: IMPL010, TST010

### SPEC011 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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保存となる。

## インターフェース ```python 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保存となる。

親: ARCH004

子: IMPL011, TST011

### SPEC012 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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` カウンタを使用して、スレッドローカルキャッシュの整合性を維持

## インターフェース ```python 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` カウンタを使用して、スレッドローカルキャッシュの整合性を維持

親: ARCH005

子: IMPL012, TST012

### SPEC013 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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`

## エッジケース

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

## インターフェース ```python 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` ## エッジケース - **継承**: 基底クラスを登録すると、サブクラスも自動的にそのエンコーダを使用する（オーバーライド可能）

親: ARCH005

子: IMPL013, TST013

### SPEC014 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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` コールバックで処理される

## エッジケース

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

## インターフェース ```python 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` コールバックで処理される ## エッジケース - **多重起動**: すでにスレッドが実行中の場合は、既存のスレッドを再利用する

親: ARCH006

子: IMPL014, TST014

### SPEC015 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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` が呼ばれ、プロセス終了前のデータ保存を確実にする。

## インターフェース ```python 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` が呼ばれ、プロセス終了前のデータ保存を確実にする。

親: ARCH006

子: IMPL015, TST015

### SPEC016 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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日）

## エッジケース

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

## インターフェース ```python 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日） ## エッジケース - **重複パターン**: 先に登録されたルールが優先される。

親: ARCH007

子: IMPL016, TST016

### SPEC017 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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` (無限大の秒数) を提供し、ポリシーに関わらず永続化を指示できる。

## インターフェース ```python 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` (無限大の秒数) を提供し、ポリシーに関わらず永続化を指示できる。

親: ARCH007

子: IMPL017, TST017

### SPEC018 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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` は現在時刻より過去に一定以上離れないよう補正される（過度なバーストの防止）。

## インターフェース ```python 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` は現在時刻より過去に一定以上離れないよう補正される（過度なバーストの防止）。

親: ARCH008

子: IMPL018, TST018

### SPEC019 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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` の順で実行される。

## インターフェース ```python 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` の順で実行される。

親: ARCH009

子: IMPL019, TST019

### SPEC020 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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）で判定する。

## インターフェース ```python 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）で判定する。

親: ARCH010

子: IMPL020, TST020

### SPEC021 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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`）に対応する。

## インターフェース ```bash 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`）に対応する。

親: ARCH011

子: IMPL021, TST021

### SPEC022 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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モードを活用）。

## インターフェース ```python 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モードを活用）。

親: ARCH012

子: IMPL022, TST022

### SPEC024 {h(g)} ✓ レビュー済 局所ビュー →

## インターフェース

    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` 等を使用したゼロコピー書き込みを可能にする。

## インターフェース ```python 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` 等を使用したゼロコピー書き込みを可能にする。

親: ARCH004

子: IMPL024, TST024

### SPEC025 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### SPEC026 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### TST007 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 `SQLiteTaskDB` はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす。WALモード・専用ライタースレッドという独自アーキテクチャの信頼性を保証する。 ## 検証観点 - **スキーマ初期化**: 新規DBファイルに対してテーブルとインデックスが正しく作成されること。既存DBへの自動マイグレーション（content_type, version, result_data, func_identifier, expires_at カラム追加）が冪等に動作すること - **CRUD操作**: タスクの作成・取得・更新・削除が正しく動作し、存在しないキーへのアクセスが適切にハンドルされること - **読み取り接続リカバリ**: スレッドローカルな読み取り接続がクローズされた場合に自動的に再作成され、接続リークが発生しないこと - **ライタースレッドのタイムアウト**: 書き込みキューのタイムアウト時に適切なエラーが返り、ライタースレッドがデッドロックしないこと - **GC耐性**: ガベージコレクション実行後もDBファイルが存続し、有効なタスクが失われないこと

references: tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py

親: REQ019, SPEC007

子: —

### TST023 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 Thundering Herd Protection は、同一キーへの並行リクエストが一斉に関数を実行する「Thundering Herd」問題を防ぐ。この保護が不完全だと、重い計算やAPI呼び出しが不要に多重実行され、リソース浪費やレートリミット超過を招く。並行性バグはテスト困難であるため、複数の観点から網羅的に検証する。 ## 検証観点 - **同期並行**: 複数スレッドが同一キーで同時にキャッシュミスした場合、関数が1回のみ実行され、全スレッドが同じ結果を受け取ること - **非同期並行**: 複数の `asyncio.Task` が同一キーで同時にキャッシュミスした場合も、関数が1回のみ実行されること - **例外伝播**: 実行者（executor）の関数が例外を送出した場合、全待機スレッド/タスクに同じ例外が伝播すること。例外後に `_inflight` エントリがクリーンアップされること - **タイムアウト**: 待機が `HERD_TIMEOUT`（300秒）を超過した場合にタイムアウト処理が発動し、無限待機にならないこと - **リトライ**: `HERD_MAX_RETRIES`（3回）の範囲でリトライが行われ、上限超過で最終エラーが返ること - **CacheManager モック**: `_inflight` 辞書の状態遷移（エントリ追加→結果セット→エントリ削除）が正しい順序で行われること

references: tests/integration/core/test_thundering_herd.py

親: SPEC023

子: —

### TST001 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

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

references: tests/integration/core/test_basic.py

親: SPEC001

子: —

### TST002 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

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

references: tests/integration/core/test_cached_run.py

親: SPEC002

子: —

### TST003 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

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

references: tests/integration/core/test_async_save.py

親: SPEC003

子: —

### TST004 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 キャッシュキーはキャッシュの同一性判定の基盤であり、ハッシュが不安定だとキャッシュミスの頻発（性能劣化）や誤ヒット（データ不整合）を引き起こす。Python バージョンやプラットフォームに依存しないキー生成の安定性を保証する。 ## 検証観点 - **決定性**: 同一の `func_identifier:input_id[:version]` 入力に対して、複数回の呼び出しで常に同じ SHA-256 ハッシュが生成されること - **一意性**: 異なる引数から異なるキーが生成されること（衝突しないこと） - **構造**: キーが `module.qualname` 形式の func_identifier を含む正しい構造であること - **version 反映**: version 文字列がキーに含まれ、version 変更でキーが変わること

references: tests/unit/test_cachekey.py

親: SPEC004

子: —

### TST005 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

`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 等）が正しく正規化されること

## 目的 `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 等）が正しく正規化されること

references: tests/unit/test_cachekey.py

親: SPEC005

子: —

### TST006 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 `KeyGenPolicy` は引数ごとのキー生成戦略をカスタマイズする機構であり、ログレベルやデバッグフラグのような非決定的引数をキーから除外したり、ファイルパスの代わりにファイル内容でキーを生成するユースケースを支える。戦略の誤適用はキャッシュの正確性を根本から損なう。 ## 検証観点 - **KeyGen.ignore()**: 指定された引数がキャッシュキーの計算から除外され、その引数の値を変えてもキャッシュヒットすること - **KeyGen.map() + FILE_CONTENT**: ファイルパス引数に対し、パス文字列ではなくファイル内容のハッシュがキーに使われること。同一内容の別パスファイルが同一キーを生成すること - **KeyGen.path_stat()**: ファイルの mtime/size に基づくキー生成が機能すること - **bind() 連携**: `KeyGenPolicy.bind(func)` が関数シグネチャ（位置引数・キーワード引数・デフォルト値）を正しく解釈すること - **@mark 統合**: `@spot.mark(keygen=KeyGen.ignore("verbose"))` のようにデコレータ経由で戦略が正しく適用されること

references: tests/unit/test_cachekey.py

親: SPEC006

子: —

### TST008 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 `TaskDBCore` / `TaskDBBase` のプロトコル階層は、サードパーティによるカスタムDB実装（Redis、PostgreSQL 等）の差し替えを可能にする拡張ポイントである。インターフェース契約が不明確だと、カスタム実装が実行時に予期しないエラーを起こす。 ## 検証観点 - **インターフェース準拠**: `TaskDBCore` の必須メソッド（save, load, delete 等）を実装したクラスが `isinstance` チェックをパスすること - **TaskDBBase 拡張**: `TaskDBBase` がメンテナンス系メソッド（prune, stats 等）のデフォルト実装を提供し、サブクラスが必須メソッドのみの実装で動作すること - **デフォルト動作**: メンテナンスメソッドのデフォルト実装が安全なno-op（空リスト返却等）であること - **カスタム実装の注入**: `bs.Spot(db=CustomDB())` でプロトコル準拠のカスタムDBが正常に動作すること

references: tests/unit/test_db_writer_queue.py

親: SPEC008

子: —

### TST009 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

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

references: tests/integration/storage/test_local.py

親: SPEC009

子: —

### TST010 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 `S3Storage` はクラウド環境での大規模Blob保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが存在する。boto3 オプショナル依存のガード処理も含め、クラウドストレージ統合の信頼性を検証する。 ## 検証観点 - **基本CRUD**: S3互換ストレージへの `save` / `load` / `delete` / `list_keys` が正しく動作すること - **オプショナル依存ガード**: boto3 がインストールされていない環境で `import` 時にわかりやすいエラーメッセージが表示されること - **エラーハンドリング**: バケット不在、権限不足、ネットワークエラー時に適切な例外が送出されること - **注記**: boto3 のモック戦略（moto 等）を使用したテストの実装を推奨

references: tests/integration/storage/test_s3.py

親: SPEC010

子: —

### TST011 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

ストレージポリシーは「データを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()` が呼ばれること

## 目的 ストレージポリシーは「データを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()` が呼ばれること

references: tests/unit/test_storage_policy.py

親: SPEC011

子: —

### TST012 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

`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) で正しく登録・解決されること

## 目的 `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) で正しく登録・解決されること

references: tests/unit/test_serializer.py

親: SPEC012

子: —

### TST013 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は `SerializationError` によるキャッシュ保存失敗を引き起こし、ユーザーの既存コードとの統合を阻害する。デコレータ方式と命令方式の両方の登録パスを検証する。 ## 検証観点 - **デコレータ登録**: `@spot.register(code=N, encoder=..., decoder=...)` でクラスが登録され、そのインスタンスのシリアライズ/デシリアライズが正常に動作すること - **命令的登録**: `spot.register_type(MyClass, code=N, ...)` でも同等の登録が行われること - **Pydantic factory デコーダ**: Pydantic モデルの `model_validate` をデコーダとして使用する場合に、バリデーション付きの復元が正しく動作すること - **ネスト構造**: カスタム型を含む dict、カスタム型のリスト、カスタム型をフィールドに持つカスタム型など、複雑な入れ子構造が正しくエンコード/デコードされること - **code 衝突**: 同一 code を重複登録した場合の挙動が明確であること

references: tests/unit/test_type_registry.py

親: SPEC013

子: —

### TST014 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

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

references: tests/integration/core/test_background_loop.py

親: SPEC014

子: —

### TST015 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

`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` コールバックが呼ばれ、メインスレッドには影響しないこと

## 目的 `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` コールバックが呼ばれ、メインスレッドには影響しないこと

references: tests/integration/core/test_exit_drain.py

親: SPEC015

子: —

### TST016 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 `LifecyclePolicy` はキャッシュデータの保持期間を関数名パターンで制御する。パターンマッチングの不備は、重要なキャッシュの意図しない削除（データロス）や不要なキャッシュの蓄積（ディスク圧迫）を引き起こす。 ## 検証観点 - **パターンマッチング**: glob パターン（`"train_*"`, `"*.preprocess"` 等）がルールの `func_pattern` として正しく機能し、マッチした関数に対して指定の `Retention` が適用されること - **ルール優先順位**: 複数ルールがマッチした場合に、リスト順（先勝ち or 後勝ち）でルールが決定されること - **resolve_with_fallback**: 完全修飾名（`module.qualname`）でルールが見つからない場合に、短縮名（`qualname` のみ）でフォールバック検索が行われること - **デフォルト保持期間**: どのルールにもマッチしない関数に対して `default_retention` で指定された保持期間（デフォルト: 30日）が適用されること - **無期限保持の互換性**: `default_retention=None` を指定した場合に無期限保持（`None`）が返されること

references: tests/unit/test_lifecycle.py

親: SPEC016

子: —

### TST017 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 `Retention` はキャッシュの有効期間をユーザーフレンドリーな形式で指定する値オブジェクトである。パースの不備は意図しない保持期間（例: "7d" を 7秒と誤解釈）を招き、重要なキャッシュの早期削除やディスクの際限ない肥大化に繋がる。 ## 検証観点 - **文字列パース**: `"7d"` → 7日、`"12h"` → 12時間、`"30m"` → 30分、`"10s"` → 10秒として正しく解釈されること - **timedelta 入力**: `timedelta(days=7)` がそのまま保持期間として受け入れられること - **秒数入力**: 整数・浮動小数点数が秒として解釈されること - **FOREVER シングルトン**: `Retention.FOREVER` が無期限保持を表し、シングルトンであること（`is` 比較が成立） - **バリデーション**: 負値（`"-1d"`）やゼロ（`"0s"`）が `ValidationError` を送出すること。不正な形式（`"abc"`, `""` 等）も拒否されること

references: tests/unit/test_lifecycle_extended.py

親: SPEC017

子: —

### TST018 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 `TokenBucket` はAPI呼び出しやリソースアクセスのレート制限を実現する。レートリミッターの不具合はAPIの過負荷（制限が甘い場合）やスループットの不必要な低下（制限が厳しすぎる場合）を招く。GCRA アルゴリズムのスムーズなレート制御を検証する。 ## 検証観点 - **初期化バリデーション**: rate ≤ 0 や capacity ≤ 0 などの不正パラメータが拒否されること - **max_cost 超過拒否**: 1回のリクエストコストがバケット容量を超える場合に即座に拒否されること（無限待機にならないこと） - **GCRA 待機動作**: バケットが空の状態でリクエストした場合に、トークン補充まで適切な時間だけ待機すること。待機時間がレートに基づいて正確であること - **バースト許容**: 容量分のリクエストがバーストで処理でき、超過分からスロットリングが開始されること

references: tests/unit/test_limiter.py

親: SPEC018

子: —

### TST019 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 Hook システムはキャッシュライフサイクルへのユーザー拡張ポイント（ロギング、メトリクス収集、監査等）であり、フック内の不具合がメインの関数実行に影響を与えないことが最重要の契約である。また `ThreadSafeHookBase` の自動ロック機構の正確性を保証する。 ## 検証観点 - **ライフサイクル呼び出し順**: キャッシュミス時に `pre_execute` → 関数実行 → `on_cache_miss` の順でフックが呼ばれること。キャッシュヒット時に `pre_execute` → `on_cache_hit` の順で呼ばれること - **コンテキスト情報**: 各フックに渡されるコンテキスト（`PreExecuteContext`, `CacheHitContext`, `CacheMissContext`）が正しい情報（func_name, args, result 等）を含むこと - **例外隔離**: フック内で例外が発生しても、関数の実行結果には影響せず、ERROR ログにのみ記録されること - **状態保持**: フックインスタンスが呼び出し間で状態（カウンタ等）を保持できること - **ThreadSafeHookBase**: `__init_subclass__` によりユーザー定義メソッドが自動的に RLock でラップされ、複数スレッドからの同時呼び出しで競合状態が発生しないこと

references: tests/integration/core/test_hooks.py

親: SPEC019

子: —

### TST020 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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

## 検証観点

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

## 目的 `MaintenanceService` は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用者の誤判断を招き、メンテナンス操作の不備はデータの意図しない削除に繋がる。 ## 検証観点 - **タスク詳細取得**: 存在するタスクIDに対して正しいメタデータ（func_name, input_key, result, created_at 等）が返却されること - **存在しないタスク**: 不在のタスクIDに対して `None` が返却され、例外が送出されないこと - **統計情報**: キャッシュの件数、サイズ、関数ごとの分布など統計情報が正確に算出されること - **クリア操作**: 指定条件に合致するタスクのみが削除され、他のタスクが影響を受けないこと - **GC 操作**: 有効期限切れタスクの削除と孤立Blobファイルのクリーンアップが正しく連動すること

references: tests/unit/test_maintenance.py

親: SPEC020

子: —

### TST021 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

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不在時に非ゼロの終了コードが返ること

## 目的 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不在時に非ゼロの終了コードが返ること

references: tests/integration/cli/test_cli.py

親: SPEC021

子: —

### TST022 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

`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 不準拠の拒否: 必須メソッドを欠くオブジェクトを注入した場合に型チェックエラーまたは実行時エラーが発生すること

## 目的 `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 不準拠の拒否**: 必須メソッドを欠くオブジェクトを注入した場合に型チェックエラーまたは実行時エラーが発生すること

references: tests/integration/core/test_dependency_injection.py

親: SPEC022

子: —

### TST024 {h(g)} ✓ レビュー済 局所ビュー →

## 目的

`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 欠損時エラー: 存在しないロケーションでエラーを送出すること

## 目的 `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 欠損時エラー**: 存在しないロケーションでエラーを送出すること

references: tests/integration/storage/test_blob_storage_base.py

親: SPEC024

子: —

### TST025 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### TST026 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### IMPL007 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## タイムアウトの実装

- `_enqueue_write` 内で `task.event.wait(timeout=0.5)` をループさせ、`self.timeout` を超えた場合にフェイルファスト処理を行う。
- PENDING: `task.try_cancel()` でキュー内キャンセル。
- RUNNING: `self._writer_conn.interrupt()` を呼び出して実行中の SQLite クエリを強制終了させ、呼び出し元に `TimeoutError` を返す。
- interrupt 後、ライタースレッド側の例外処理を待つための猶予時間を設け、可用性と整合性を両立させている。

## 実装概要 `SQLiteTaskDB` クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の `_writer_thread`（デーモン）が `_write_queue` から書き込みタスクを取り出して直列処理し、読み取りは `threading.local()` のスレッドローカル接続で並行実行する。 WAL モードにより読み取りと書き込みの並行性を確保している。 ## タイムアウトの実装 - `_enqueue_write` 内で `task.event.wait(timeout=0.5)` をループさせ、`self.timeout` を超えた場合にフェイルファスト処理を行う。 - **PENDING**: `task.try_cancel()` でキュー内キャンセル。 - **RUNNING**: `self._writer_conn.interrupt()` を呼び出して実行中の SQLite クエリを強制終了させ、呼び出し元に `TimeoutError` を返す。 - interrupt 後、ライタースレッド側の例外処理を待つための猶予時間を設け、可用性と整合性を両立させている。

references: src/beautyspot/db.py

親: SPEC007

子: —

### IMPL023 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 設計判断

### コンテキストマネージャによる例外安全性

実行者 (Executor) が処理中に例外（KeyboardInterrupt や asyncio.CancelledError を含む）で 中断された場合でも、`_inflight` 状態が残留して後続タスクを永続的にブロックすることを防ぐため、 `herd_sync` / `herd_async` をコンテキストマネージャとして提供する。 クリーンアップ処理（`notify_and_cleanup_inflight`）は `finally` ブロック内で 自動的に実行される。

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

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

## 実装概要 `CacheManager` クラス内で、Thundering Herd（キャッシュミス時に同一キーへの 大量アクセスが同時に発生する問題）を防止する直列化機構を実装。 `_inflight` 辞書で実行中のキーを管理し、最初の1スレッド/タスクだけが関数を実行し、 後続の呼び出しは `herd_sync` / `herd_async` コンテキスト内でその完了を待機する。 ## 設計判断 ### コンテキストマネージャによる例外安全性 実行者 (Executor) が処理中に例外（KeyboardInterrupt や asyncio.CancelledError を含む）で 中断された場合でも、`_inflight` 状態が残留して後続タスクを永続的にブロックすることを防ぐため、 `herd_sync` / `herd_async` をコンテキストマネージャとして提供する。 クリーンアップ処理（`notify_and_cleanup_inflight`）は `finally` ブロック内で 自動的に実行される。 ### 同期・非同期の統合的保護 `_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に対して 別スレッドから安全に結果（または例外）をセットする

references: src/beautyspot/cache.py

親: SPEC023

子: —

### IMPL001 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 設計判断

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

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

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

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

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

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

## 実装メモ

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

## TODO / 技術的負債

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

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

references: src/beautyspot/core.py

親: SPEC001

子: —

### IMPL002 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 設計判断

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

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

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

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

## 実装メモ

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

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

references: src/beautyspot/core.py

親: SPEC002

子: —

### IMPL003 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 設計判断

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

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

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

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

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

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

## 実装メモ

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

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

references: src/beautyspot/core.py

親: SPEC003

子: —

### IMPL004 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/cachekey.py

親: SPEC004

子: —

### IMPL005 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/cachekey.py

親: SPEC005

子: —

### IMPL006 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/cachekey.py

親: SPEC006

子: —

### IMPL008 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/db.py

親: SPEC008

子: —

### IMPL009 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 実装概要 - `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` 自体は保持する

references: src/beautyspot/storage.py

親: SPEC009

子: —

### IMPL010 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 実装概要 `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 判定用）

references: src/beautyspot/storage.py

親: SPEC010

子: —

### IMPL011 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/storage.py

親: SPEC011

子: —

### IMPL012 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/serializer.py

親: SPEC012

子: —

### IMPL013 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 実装概要 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 変更）もエラーとなる

references: src/beautyspot/serializer.py, src/beautyspot/core.py

親: SPEC013

子: —

### IMPL014 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/core.py

親: SPEC014

子: —

### IMPL015 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/core.py

親: SPEC015

子: —

### IMPL016 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/lifecycle.py

親: SPEC016

子: —

### IMPL017 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/lifecycle.py

親: SPEC017

子: —

### IMPL018 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 実装概要 `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()` で待機する

references: src/beautyspot/limiter.py

親: SPEC018

子: —

### IMPL019 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

タスク実行ライフサイクルに介入するインターフェース `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__()` の呼び出し忘れに対して親切なエラーメッセージを返す

## 実装概要 タスク実行ライフサイクルに介入するインターフェース `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__()` の呼び出し忘れに対して親切なエラーメッセージを返す

references: src/beautyspot/hooks.py

親: SPEC019

子: —

### IMPL020 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 実装概要 `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()` も提供される

references: src/beautyspot/maintenance.py

親: SPEC020

子: —

### IMPL021 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

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

references: src/beautyspot/cli.py

親: SPEC021

子: —

### IMPL022 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 実装概要 `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境界を明確に定義している

references: src/beautyspot/__init__.py

親: SPEC022

子: —

### IMPL024 {h(g)} ✓ レビュー済 局所ビュー →

## 実装概要

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

## 実装概要 `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 すべきことを規定

references: src/beautyspot/storage.py

親: SPEC024

子: —

### IMPL025 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### IMPL026 {h(g)} ✓ レビュー済 局所ビュー →

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

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

親: —

子: —

### ADR002 {h(g)} ✓ レビュー済 局所ビュー →

# Stable Hashing for Function Arguments

## Context and Problem Statement / コンテキスト

`beautyspot` のコア機能である「関数のメモ化（キャッシュ）」において、関数の引数 (`args`, `kwargs`) から一意なキャッシュキーを生成する必要があります。

初期実装 (`v0.1.0`) では、`json.dumps` が失敗した場合のフォールバックとして `str((args, kwargs))` のハッシュ値を使用していました。

しかし、Pythonのデフォルトの `__str__` / `__repr__` 実装は、オブジェクトのメモリアドレス（例: `<MyObject at 0x10a...>`）を含むことが多くあります。

これにより以下の問題が発生していました：

1. 再起動ごとのキャッシュ無効化: プロセスを再起動するとメモリアドレスが変わり、同じ入力値でもハッシュが変わってしまう。
2. 分散環境での不整合: 異なるマシン（あるいは異なるプロセス）で実行した場合、キャッシュキーが一致しない。

外部ライブラリ（`joblib` 等）を使えば解決しますが、`beautyspot` は軽量な「黒子」ライブラリを目指しており、依存関係を増やしたくありません。

## Decision Drivers / 要求

- Stability across restarts: プロセス再起動後もキャッシュが有効に機能すること。
- Environment Independence: 異なるマシンや環境間でも一貫したハッシュ値が得られること。
- Zero External Dependencies: 軽量さを維持するため、外部ライブラリ（joblib, numpy等）に依存しないこと。
- Broad Type Support: `pydantic` モデルや `dataclass` 等、一般的なPythonオブジェクトを透過的に扱えること。

## Considered Options / 検討

- Option 1: 現状維持（`str()` によるフォールバック）。
- Option 2: 外部ライブラリ（`joblib` 等）の導入。
- Option 3: 標準ライブラリの `json` を拡張した、独自の安定化シリアライザの実装。

## Decision Outcome / 決定

Chosen option: Option 3.

標準ライブラリの `json` モジュールを使用し、独自の `default` シリアライザ (`_stable_serialize_default`) を実装することで、依存関係なしで 堅牢なハッシュ生成を実現します。

具体的には以下の戦略を採用します：

1. Set/Frozensetのソート: JSONは順序を持たない集合を扱えないため、`sorted(list(obj))` でリスト化し、順序を保証します。
2. カスタムオブジェクトの辞書化: `__dict__` または `__slots__` を参照し、オブジェクトの「中身の値」をシリアライズ対象とします。これにより、メモリアドレスへの依存を排除します。
3. Bytes型: 16進数文字列 (`hex`) に変換します。
4. 最終手段: それでもシリアライズできない型（循環参照など）については、例外的に `str()` を使用します（この場合のみ不安定になるリスクを許容します）。

## Consequences / 決定

- Positive:
  - アプリ再起動後もキャッシュが有効に機能する（永続化の信頼性向上）。
  - `pydantic` モデルや `dataclass` も設定なしでキャッシュ可能になる。
  - 外部依存（`numpy`, `joblib` 等）を増やさずに済む。
- Negative:
  - 単なる `str()` 変換よりも、シリアライズ処理のオーバーヘッド（CPUコスト）がわずかに増加する。
  - 循環参照を持つオブジェクトを渡された場合の完全な解決策ではない（ただし、タスクの入力引数としては稀であると判断）。

# Stable Hashing for Function Arguments ## Context and Problem Statement / コンテキスト `beautyspot` のコア機能である「関数のメモ化（キャッシュ）」において、関数の引数 (`args`, `kwargs`) から一意なキャッシュキーを生成する必要があります。 初期実装 (`v0.1.0`) では、`json.dumps` が失敗した場合のフォールバックとして `str((args, kwargs))` のハッシュ値を使用していました。 しかし、Pythonのデフォルトの `__str__` / `__repr__` 実装は、オブジェクトのメモリアドレス（例: `<MyObject at 0x10a...>`）を含むことが多くあります。 これにより以下の問題が発生していました： 1. **再起動ごとのキャッシュ無効化:** プロセスを再起動するとメモリアドレスが変わり、同じ入力値でもハッシュが変わってしまう。 2. **分散環境での不整合:** 異なるマシン（あるいは異なるプロセス）で実行した場合、キャッシュキーが一致しない。 外部ライブラリ（`joblib` 等）を使えば解決しますが、`beautyspot` は軽量な「黒子」ライブラリを目指しており、依存関係を増やしたくありません。 ## Decision Drivers / 要求 * **Stability across restarts**: プロセス再起動後もキャッシュが有効に機能すること。 * **Environment Independence**: 異なるマシンや環境間でも一貫したハッシュ値が得られること。 * **Zero External Dependencies**: 軽量さを維持するため、外部ライブラリ（joblib, numpy等）に依存しないこと。 * **Broad Type Support**: `pydantic` モデルや `dataclass` 等、一般的なPythonオブジェクトを透過的に扱えること。 ## Considered Options / 検討 * **Option 1**: 現状維持（`str()` によるフォールバック）。 * **Option 2**: 外部ライブラリ（`joblib` 等）の導入。 * **Option 3**: 標準ライブラリの `json` を拡張した、独自の安定化シリアライザの実装。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 標準ライブラリの `json` モジュールを使用し、独自の `default` シリアライザ (`_stable_serialize_default`) を実装することで、**依存関係なしで** 堅牢なハッシュ生成を実現します。 具体的には以下の戦略を採用します： 1. **Set/Frozensetのソート:** JSONは順序を持たない集合を扱えないため、`sorted(list(obj))` でリスト化し、順序を保証します。 2. **カスタムオブジェクトの辞書化:** `__dict__` または `__slots__` を参照し、オブジェクトの「中身の値」をシリアライズ対象とします。これにより、メモリアドレスへの依存を排除します。 3. **Bytes型:** 16進数文字列 (`hex`) に変換します。 4. **最終手段:** それでもシリアライズできない型（循環参照など）については、例外的に `str()` を使用します（この場合のみ不安定になるリスクを許容します）。 ## Consequences / 決定 * **Positive**: * アプリ再起動後もキャッシュが有効に機能する（永続化の信頼性向上）。 * `pydantic` モデルや `dataclass` も設定なしでキャッシュ可能になる。 * 外部依存（`numpy`, `joblib` 等）を増やさずに済む。 * **Negative**: * 単なる `str()` 変換よりも、シリアライズ処理のオーバーヘッド（CPUコスト）がわずかに増加する。 * 循環参照を持つオブジェクトを渡された場合の完全な解決策ではない（ただし、タスクの入力引数としては稀であると判断）。

親: REQ002

子: —

### ADR003 {h(g)} ✓ レビュー済 局所ビュー →

# Smooth Rate Limiter (GCRA)

## Context and Problem Statement / コンテキスト

`beautyspot` のレート制限機能 (`limiter`) において、以下の課題があった。

1. Idle Burst: トークンバケット方式では、アイドル中にトークンが溜まり、再開直後にバースト（集中アクセス）が発生してしまう。これを防ぐために容量(`capacity`)を小さくすると、今度は巨大なコストを持つタスクが実行できなくなる（デッドロック）問題が発生する。
2. Start Dash Prevention: プロセス起動直後に複数のタスクが同時に走るのを防ぎたいが、最初の1回目まで待たされるのは避けたい。
3. Clock Dependency: システム時刻の変更に堅牢である必要がある。
4. Max Cost Guard: `tokens_per_minute` を単発タスクのコスト上限とする。
   - これを超えるコストが `consume()` に渡された場合、即座に `ValueError` を送出する。
   - 理由: APIの物理的なレート制限を超えるリクエストは、待機したところで成功する見込みが薄く、早期に設定ミスや入力ミスをユーザーに通知すべきであるため。

## Decision Drivers / 要求

- Burst Prevention: 長時間のアイドル後に大量のタスクが集中するのを防ぎ、一定のレートを維持すること。
- Deadlock Freedom: レート制限設定値（TPM）以下のコストであれば、どんなに大きなコストのタスクでも（待機時間を経て）実行可能であること。
- Zero Initial Latency: 最初のタスクは待機時間なしで即座に開始できること。
- Fail Fast: システムの設定（TPM）で物理的に不可能なリクエストは、実行前に即座にエラーにすること。

## Considered Options / 検討

- Option 1: トークンバケット（既存実装）。バーストしやすく、容量設定によってはデッドロックのリスクがある。
- Option 2: GCRA (Generic Cell Rate Algorithm)。別名 "Leaky Bucket as a Meter"。定速性が高く、貯金を許さないためバーストに強い。

## Decision Outcome / 決定

Chosen option: Option 2.

アルゴリズムをトークンバケットから GCRA (Generic Cell Rate Algorithm) に変更する。

- 理論的到達時刻 (TAT: Theoretical Arrival Time) を管理する。
- タスクが来ると、そのコスト分だけ TAT を未来へ進める。
- `現在時刻 < TAT` ならば、その差分だけ待機（sleep）してから実行する。
- `現在時刻 >= TAT` （アイドル状態）ならば、TAT を現在時刻にリセットしてから計算する（過去の貯金は捨てる）。

## Consequences / 決定

- Positive:
  - 完全な定速運転: アイドル後でもバーストせず、設定されたレート（TPM）を厳守する。
  - デッドロックフリー: どんなに大きなコストのタスクでも、必要な待機時間を計算して処理できる。
  - 即時起動: 最初の1回のリクエストは待ち時間なしで実行され、2回目以降からペーシングが効く。
  - Fail Fast: 設定値を超える無茶なリクエストを早期発見できる。
- Negative:
  - 実装が「トークンを消費する」という直感的なメタファーから少し離れる（コード内の説明で補足する）。

# Smooth Rate Limiter (GCRA) ## Context and Problem Statement / コンテキスト `beautyspot` のレート制限機能 (`limiter`) において、以下の課題があった。 1. **Idle Burst:** トークンバケット方式では、アイドル中にトークンが溜まり、再開直後にバースト（集中アクセス）が発生してしまう。これを防ぐために容量(`capacity`)を小さくすると、今度は巨大なコストを持つタスクが実行できなくなる（デッドロック）問題が発生する。 2. **Start Dash Prevention:** プロセス起動直後に複数のタスクが同時に走るのを防ぎたいが、最初の1回目まで待たされるのは避けたい。 3. **Clock Dependency:** システム時刻の変更に堅牢である必要がある。 4. **Max Cost Guard:** `tokens_per_minute` を単発タスクのコスト上限とする。 * これを超えるコストが `consume()` に渡された場合、即座に `ValueError` を送出する。 * 理由: APIの物理的なレート制限を超えるリクエストは、待機したところで成功する見込みが薄く、早期に設定ミスや入力ミスをユーザーに通知すべきであるため。 ## Decision Drivers / 要求 * **Burst Prevention**: 長時間のアイドル後に大量のタスクが集中するのを防ぎ、一定のレートを維持すること。 * **Deadlock Freedom**: レート制限設定値（TPM）以下のコストであれば、どんなに大きなコストのタスクでも（待機時間を経て）実行可能であること。 * **Zero Initial Latency**: 最初のタスクは待機時間なしで即座に開始できること。 * **Fail Fast**: システムの設定（TPM）で物理的に不可能なリクエストは、実行前に即座にエラーにすること。 ## Considered Options / 検討 * **Option 1**: トークンバケット（既存実装）。バーストしやすく、容量設定によってはデッドロックのリスクがある。 * **Option 2**: GCRA (Generic Cell Rate Algorithm)。別名 "Leaky Bucket as a Meter"。定速性が高く、貯金を許さないためバーストに強い。 ## Decision Outcome / 決定 Chosen option: **Option 2**. アルゴリズムをトークンバケットから **GCRA (Generic Cell Rate Algorithm)** に変更する。 * **理論的到達時刻 (TAT: Theoretical Arrival Time)** を管理する。 * タスクが来ると、そのコスト分だけ TAT を未来へ進める。 * `現在時刻 < TAT` ならば、その差分だけ待機（sleep）してから実行する。 * `現在時刻 >= TAT` （アイドル状態）ならば、TAT を現在時刻にリセットしてから計算する（過去の貯金は捨てる）。 ## Consequences / 決定 * **Positive**: * **完全な定速運転:** アイドル後でもバーストせず、設定されたレート（TPM）を厳守する。 * **デッドロックフリー:** どんなに大きなコストのタスクでも、必要な待機時間を計算して処理できる。 * **即時起動:** 最初の1回のリクエストは待ち時間なしで実行され、2回目以降からペーシングが効く。 * **Fail Fast:** 設定値を超える無茶なリクエストを早期発見できる。 * **Negative**: * 実装が「トークンを消費する」という直感的なメタファーから少し離れる（コード内の説明で補足する）。

親: REQ008

子: —

### ADR004 {h(g)} ✓ レビュー済 局所ビュー →

# Resilient Deserialization and Explicit Versioning

## Context and Problem Statement / コンテキスト

Pythonの `pickle` は、保存されたオブジェクトのクラス定義と現在のコードが一致していることを前提とします。 開発中はクラス定義が頻繁に変更されるため、過去のキャッシュを読み込む際に `AttributeError` 等が発生し、アプリケーションがクラッシュする問題がありました。

## Decision Drivers / 要求

- Fault Tolerance: キャッシュデータがコードの変更と不整合を起こしても、アプリケーションをクラッシュさせないこと。
- Developer Experience (DX): 開発中の頻繁なリファクタリングを妨げないこと。
- Explicit Control: ユーザーが意図したタイミングでキャッシュを無効化できる手段を提供すること。
- Safe Deployment: 本番環境において、安全にキャッシュを刷新できること。

## Considered Options / 検討

- Option 1: デシリアライズエラー時に例外を送出し、実行を停止する（既存挙動）。
- Option 2: エラーを捕捉して「キャッシュミス」として扱い、再計算を行う。あわせて明示的な `version` 指定による無効化を可能にする。

## Decision Outcome / 決定

Chosen option: Option 2.

1. Fail Safe (Auto Recalc): `Storage.load()` でデシリアライズエラー（クラス不整合や破損）が発生した場合、これを `CacheCorruptedError` として捕捉し、`core.py` 側で 「キャッシュミス」 として扱う。これによりアプリをクラッシュさせず、自動的に再計算を行う。
2. Explicit Versioning: ユーザーが意図的にキャッシュを無効化できるよう、`@task` デコレータに `version` 引数を追加する。これを変更するとキャッシュキーが変わり、古い（互換性のない）キャッシュを参照しなくなる。
3. User Guidance: 自動再計算が発生した際、ログに「コード変更時は `version` の更新を検討してね」というヒントを出力し、ベストプラクティスへ誘導する。

## Consequences / 決定

- Positive:
  - 開発中のクラッシュを防ぎ、スムーズなDXを提供する。
  - 本番運用時でも、`version` を使うことで安全なデプロイ（キャッシュ切り替え）が可能になる。
- Negative:
  - 再計算コストが発生する（が、クラッシュよりはマシである）。

# Resilient Deserialization and Explicit Versioning ## Context and Problem Statement / コンテキスト Pythonの `pickle` は、保存されたオブジェクトのクラス定義と現在のコードが一致していることを前提とします。 開発中はクラス定義が頻繁に変更されるため、過去のキャッシュを読み込む際に `AttributeError` 等が発生し、アプリケーションがクラッシュする問題がありました。 ## Decision Drivers / 要求 * **Fault Tolerance**: キャッシュデータがコードの変更と不整合を起こしても、アプリケーションをクラッシュさせないこと。 * **Developer Experience (DX)**: 開発中の頻繁なリファクタリングを妨げないこと。 * **Explicit Control**: ユーザーが意図したタイミングでキャッシュを無効化できる手段を提供すること。 * **Safe Deployment**: 本番環境において、安全にキャッシュを刷新できること。 ## Considered Options / 検討 * **Option 1**: デシリアライズエラー時に例外を送出し、実行を停止する（既存挙動）。 * **Option 2**: エラーを捕捉して「キャッシュミス」として扱い、再計算を行う。あわせて明示的な `version` 指定による無効化を可能にする。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 1. **Fail Safe (Auto Recalc):** `Storage.load()` でデシリアライズエラー（クラス不整合や破損）が発生した場合、これを `CacheCorruptedError` として捕捉し、`core.py` 側で **「キャッシュミス」** として扱う。これによりアプリをクラッシュさせず、自動的に再計算を行う。 2. **Explicit Versioning:** ユーザーが意図的にキャッシュを無効化できるよう、`@task` デコレータに `version` 引数を追加する。これを変更するとキャッシュキーが変わり、古い（互換性のない）キャッシュを参照しなくなる。 3. **User Guidance:** 自動再計算が発生した際、ログに「コード変更時は `version` の更新を検討してね」というヒントを出力し、ベストプラクティスへ誘導する。 ## Consequences / 決定 * **Positive**: * 開発中のクラッシュを防ぎ、スムーズなDXを提供する。 * 本番運用時でも、`version` を使うことで安全なデプロイ（キャッシュ切り替え）が可能になる。 * **Negative**: * 再計算コストが発生する（が、クラッシュよりはマシである）。

親: REQ005

子: —

### ADR005 {h(g)} ✓ レビュー済 局所ビュー →

# Manage Executor Lifecycle via Instance Ownership and Weak References

## Context and Problem Statement / コンテキスト

`src/beautyspot/core.py` において、非同期タスクのIOオフロード用に `ThreadPoolExecutor` がグローバル変数 `_io_executor` として定義されている。

この実装には以下の課題がある：

1. リソース制御の欠如: スレッド数（`max_workers=4`）がハードコードされており、ユーザーが実行環境（AWS Lambda、強力なサーバー等）に合わせて調整できない。
2. ゾンビプロセス: プロセス終了時に明示的なシャットダウンが行われないため、環境によってはゾンビプロセス化するリスクがある。
3. テスト困難性: グローバル変数はモック化が難しく、ユニットテストの分離を妨げる。

代替案として `with` 文（Context Manager）による管理も検討されたが、`beautyspot` の「デコレータを付与するだけで動作する」という簡易な利用体験（DX）を損なうため、採用には至らなかった。ユーザーコードを変更させずに、安全にリソースをクリーンアップする仕組みが必要である。

## Decision Drivers / 要求

- Developer Experience (DX): ユーザーに `shutdown()` の呼び出しや `with` 文を強制したくない。
- Configurability: スレッド数や Executor の実装をユーザーが制御できるようにしたい。
- Safety: プロセス終了時やインスタンス破棄時に、確実にスレッドプールを閉じたい。
- Memory Safety: クリーンアップ処理の登録によって、メモリリーク（循環参照）を引き起こしてはならない。

## Considered Options / 検討

- Option 1: 現状維持（グローバル変数のまま）。
- Option 2: `Project` を Context Manager 化し、`__exit__` で `shutdown()` を呼ぶ。
- Option 3: `atexit` モジュールを使い、終了時に `shutdown()` を呼ぶ。
- Option 4: インスタンス管理に変更し、`weakref.finalize` で自動クリーンアップを行う。

## Decision Outcome / 決定

Chosen option: Option 4.

`Project` クラスの設計を以下のように変更する：

1. Executorのインスタンス化: グローバル変数を廃止し、`Project` インスタンスごとに `ThreadPoolExecutor` を保持する。
2. Dependency Injection (DI): コンストラクタで外部 `Executor` の注入を許可する。
3. 所有権と責任の分離:
   - 外部注入 (`executor` 引数あり): `Project` はそれを借用するのみ。シャットダウンの責任はユーザー（呼び出し元）にあるため、自動クリーンアップは行わない。
   - 内部生成 (`executor` 引数なし): `Project` がライフサイクルを管理する責任を持つ。
4. 自動クリーンアップ: 内部生成した場合に限り、`weakref.finalize` を使用してシャットダウンを自動化する。

### Technical Details / 技術詳細: Why `weakref.finalize` instead of `atexit`?

単純な `atexit.register(self.shutdown)` を採用しなかった理由は、メモリリーク（循環参照）のリスク である。

- 問題点: `atexit` レジストリにインスタンスメソッド（`self.shutdown`）を登録すると、`atexit` が `self` への強参照（Strong Reference）を保持し続ける。これにより、アプリケーション実行中に `Project` インスタンスが不要になってもガベージコレクション（GC）されず、メモリリークを引き起こす。これを防ぐには `atexit.unregister` が必要になるが、管理が複雑になる。
- 解決策: `weakref.finalize(self, func, *args)` を採用する。
  - `self` がGCされた時点で、即座に `func`（クリーンアップ処理）が実行される。
  - プログラム終了時まで `self` が生存していた場合も、`atexit` 相当のタイミングで `func` が実行される。
  - 重要: クリーンアップ関数 `_shutdown_executor` は `staticmethod` とし、引数には `self` ではなく `self.executor` のみを渡す。これにより、ファイナライザが `self` を参照し続けて寿命を延ばしてしまう事故を防ぐ。

    # Implementation Sketch
    class Project:
        def __init__(self, ...):
            # ...
            self.executor = ThreadPoolExecutor(...)
            # self を参照しないよう、executor オブジェクトだけを渡す
            self._finalizer = weakref.finalize(self, self._shutdown_executor, self.executor)

        @staticmethod
        def _shutdown_executor(executor):
            executor.shutdown(wait=True)

## Consequences / 決定

- Positive:
  - ユーザーはリソース管理を意識する必要がなくなり、APIもシンプルに保たれる。
  - `Project` インスタンスがGCされたタイミングでスレッドプールも回収されるため、リソース効率が良い。
  - DIにより、テスト時にモックExecutorを差し込むことが容易になる。
- Negative:
  - `core.py` の実装において、GCの挙動と `weakref` の仕様を理解したコーディングが必要になり、内部的な複雑性が若干増す。
  - `Project` インスタンスを大量に生成・破棄するような特殊な使い方をした場合、スレッドプールの生成コストがオーバーヘッドになる可能性がある（ドキュメントでシングルトン的な利用を推奨することで緩和する）。

# Manage Executor Lifecycle via Instance Ownership and Weak References ## Context and Problem Statement / コンテキスト `src/beautyspot/core.py` において、非同期タスクのIOオフロード用に `ThreadPoolExecutor` がグローバル変数 `_io_executor` として定義されている。 この実装には以下の課題がある： 1. **リソース制御の欠如**: スレッド数（`max_workers=4`）がハードコードされており、ユーザーが実行環境（AWS Lambda、強力なサーバー等）に合わせて調整できない。 2. **ゾンビプロセス**: プロセス終了時に明示的なシャットダウンが行われないため、環境によってはゾンビプロセス化するリスクがある。 3. **テスト困難性**: グローバル変数はモック化が難しく、ユニットテストの分離を妨げる。 代替案として `with` 文（Context Manager）による管理も検討されたが、`beautyspot` の「デコレータを付与するだけで動作する」という簡易な利用体験（DX）を損なうため、採用には至らなかった。ユーザーコードを変更させずに、安全にリソースをクリーンアップする仕組みが必要である。 ## Decision Drivers / 要求 * **Developer Experience (DX)**: ユーザーに `shutdown()` の呼び出しや `with` 文を強制したくない。 * **Configurability**: スレッド数や Executor の実装をユーザーが制御できるようにしたい。 * **Safety**: プロセス終了時やインスタンス破棄時に、確実にスレッドプールを閉じたい。 * **Memory Safety**: クリーンアップ処理の登録によって、メモリリーク（循環参照）を引き起こしてはならない。 ## Considered Options / 検討 * **Option 1**: 現状維持（グローバル変数のまま）。 * **Option 2**: `Project` を Context Manager 化し、`__exit__` で `shutdown()` を呼ぶ。 * **Option 3**: `atexit` モジュールを使い、終了時に `shutdown()` を呼ぶ。 * **Option 4**: インスタンス管理に変更し、`weakref.finalize` で自動クリーンアップを行う。 ## Decision Outcome / 決定 Chosen option: **Option 4**. `Project` クラスの設計を以下のように変更する： 1. **Executorのインスタンス化**: グローバル変数を廃止し、`Project` インスタンスごとに `ThreadPoolExecutor` を保持する。 2. **Dependency Injection (DI)**: コンストラクタで外部 `Executor` の注入を許可する。 3. **所有権と責任の分離**: * **外部注入 (`executor` 引数あり)**: `Project` はそれを借用するのみ。シャットダウンの責任はユーザー（呼び出し元）にあるため、自動クリーンアップは行わない。 * **内部生成 (`executor` 引数なし)**: `Project` がライフサイクルを管理する責任を持つ。 4. **自動クリーンアップ**: 内部生成した場合に限り、`weakref.finalize` を使用してシャットダウンを自動化する。 ### Technical Details / 技術詳細: Why `weakref.finalize` instead of `atexit`? 単純な `atexit.register(self.shutdown)` を採用しなかった理由は、**メモリリーク（循環参照）のリスク** である。 * **問題点**: `atexit` レジストリにインスタンスメソッド（`self.shutdown`）を登録すると、`atexit` が `self` への強参照（Strong Reference）を保持し続ける。これにより、アプリケーション実行中に `Project` インスタンスが不要になってもガベージコレクション（GC）されず、メモリリークを引き起こす。これを防ぐには `atexit.unregister` が必要になるが、管理が複雑になる。 * **解決策**: `weakref.finalize(self, func, *args)` を採用する。 * `self` がGCされた時点で、即座に `func`（クリーンアップ処理）が実行される。 * プログラム終了時まで `self` が生存していた場合も、`atexit` 相当のタイミングで `func` が実行される。 * **重要**: クリーンアップ関数 `_shutdown_executor` は `staticmethod` とし、引数には `self` ではなく `self.executor` のみを渡す。これにより、ファイナライザが `self` を参照し続けて寿命を延ばしてしまう事故を防ぐ。 ```python # Implementation Sketch class Project: def __init__(self, ...): # ... self.executor = ThreadPoolExecutor(...) # self を参照しないよう、executor オブジェクトだけを渡す self._finalizer = weakref.finalize(self, self._shutdown_executor, self.executor) @staticmethod def _shutdown_executor(executor): executor.shutdown(wait=True) ``` ## Consequences / 決定 * **Positive**: * ユーザーはリソース管理を意識する必要がなくなり、APIもシンプルに保たれる。 * `Project` インスタンスがGCされたタイミングでスレッドプールも回収されるため、リソース効率が良い。 * DIにより、テスト時にモックExecutorを差し込むことが容易になる。 * **Negative**: * `core.py` の実装において、GCの挙動と `weakref` の仕様を理解したコーディングが必要になり、内部的な複雑性が若干増す。 * `Project` インスタンスを大量に生成・破棄するような特殊な使い方をした場合、スレッドプールの生成コストがオーバーヘッドになる可能性がある（ドキュメントでシングルトン的な利用を推奨することで緩和する）。

親: REQ006

子: —

### ADR006 {h(g)} ✓ レビュー済 局所ビュー →

# Dashboard Interaction Model

## Context and Problem Statement / コンテキスト

現状のダッシュボード (`dashboard.py`) では、タスク一覧の表示と詳細データの復元操作が分離しています。 ユーザーは一覧テーブルで対象を確認した後、別途ドロップダウンメニューから対応する `cache_key` (ハッシュ値) を手動で探し出す必要があり、認知負荷が高い状態です。 プロジェクトの依存関係として `streamlit>=1.51.0` が確保されており、インタラクティブなデータフレーム機能が利用可能となっています。

## Decision Drivers / 要求

- Lower Cognitive Load: ユーザーがハッシュ値を意識することなく、直感的にデータを操作できること。
- Seamless Navigation: 一覧での選択が即座に詳細ビューに反映されること。
- Modern UI Experience: Streamlit の最新機能を活用し、洗練された操作感を提供すること。

## Considered Options / 検討

- Option 1: 現状維持（テーブルと選択用ドロップダウンの分離）。
- Option 2: `st.dataframe` の `on_select` 機能を使用し、テーブル行のクリックによって詳細ビュー（Restore Data）のコンテキストを切り替える方式。

## Decision Outcome / 決定

Chosen option: Option 2.

`st.dataframe` の `on_select` 機能を使用し、テーブル行のクリックによって詳細ビューのコンテキストを切り替える方式を採用します。これにより、ユーザーはテーブル上のレコードを直接クリックするだけで、そのタスクの詳細や保存されたデータを復元できるようになります。

## Consequences / 決定

- Positive:
  - ユーザーは直感的にレコードを選択・復元できる。
  - フィルタリングされた結果に対しても、正しい行インデックスで詳細へアクセスできる。
- Negative:
  - Streamlit の古いバージョン（<1.35）との互換性が失われる（ただし `pyproject.toml` でバージョン指定済みのため許容）。

# Dashboard Interaction Model ## Context and Problem Statement / コンテキスト 現状のダッシュボード (`dashboard.py`) では、タスク一覧の表示と詳細データの復元操作が分離しています。 ユーザーは一覧テーブルで対象を確認した後、別途ドロップダウンメニューから対応する `cache_key` (ハッシュ値) を手動で探し出す必要があり、認知負荷が高い状態です。 プロジェクトの依存関係として `streamlit>=1.51.0` が確保されており、インタラクティブなデータフレーム機能が利用可能となっています。 ## Decision Drivers / 要求 * **Lower Cognitive Load**: ユーザーがハッシュ値を意識することなく、直感的にデータを操作できること。 * **Seamless Navigation**: 一覧での選択が即座に詳細ビューに反映されること。 * **Modern UI Experience**: Streamlit の最新機能を活用し、洗練された操作感を提供すること。 ## Considered Options / 検討 * **Option 1**: 現状維持（テーブルと選択用ドロップダウンの分離）。 * **Option 2**: `st.dataframe` の `on_select` 機能を使用し、テーブル行のクリックによって詳細ビュー（Restore Data）のコンテキストを切り替える方式。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `st.dataframe` の `on_select` 機能を使用し、テーブル行のクリックによって詳細ビューのコンテキストを切り替える方式を採用します。これにより、ユーザーはテーブル上のレコードを直接クリックするだけで、そのタスクの詳細や保存されたデータを復元できるようになります。 ## Consequences / 決定 * **Positive**: * ユーザーは直感的にレコードを選択・復元できる。 * フィルタリングされた結果に対しても、正しい行インデックスで詳細へアクセスできる。 * **Negative**: * Streamlit の古いバージョン（<1.35）との互換性が失われる（ただし `pyproject.toml` でバージョン指定済みのため許容）。

親: REQ011

子: —

### ADR007 {h(g)} ✓ レビュー済 局所ビュー →

# Semantic Content Type Support

## Context and Problem Statement / コンテキスト

生成AIタスクの出力は、テキストだけでなく、画像、構造化データ、ダイアグラム（Mermaid, Graphviz/DOT, HTML）など多岐にわたります。 現状のデータベーススキーマ（`result_type` = `FILE` | `DIRECT`）は「データの保存形式」しか保持しておらず、「データの意味的種類（Semantic Type）」が不明であるため、ダッシュボードでの復元時に適切な可視化（レンダリング）ができません。

また、`beautyspot` はライブラリとしてユーザーの手元で動作するため、複雑なマイグレーション手順や重量級の依存関係（Alembicなど）を強制することはUXを損なうという課題があります。

## Decision Drivers / 要求

- Rich Visualization: 生成された図やデータを、ダッシュボード上で適切な形式で閲覧できること。
- Zero-Touch Upgrade: スキーマ変更時にユーザーが手動でマイグレーションコマンドを打つ必要がないこと。
- Lightweight: ライブラリの依存関係を最小限に保つこと。
- Type Safety: データの保存形式（Blob/Text）と意味（Mermaid/Image等）を明確に分離すること。

## Considered Options / 検討

- Option 1: 保存されたデータの拡張子や中身から動的に型を推論する。
- Option 2: スキーマに `content_type` カラムを追加し、開発者が `@task` で明示的に指定できるようにする。あわせて自動マイグレーション機能を実装する。

## Decision Outcome / 決定

Chosen option: Option 2.

### 1. Database Schema & Migration

- Schema Change: `tasks` テーブルに `content_type` (TEXT) カラムを追加する。
- Migration Strategy: "Auto-Migration on Startup" を採用する。
  - Alembic等は導入しない（依存関係の軽量化とUX維持のため）。
  - `Project` 初期化時（`_init_db`）に `PRAGMA table_info` でカラムの存在を確認し、不足していれば標準ライブラリのみで `ALTER TABLE` を自動実行する。

### 2. Type Definition

- `src/beautyspot/types.py` を新設し、`ContentType` クラスで定数を管理する。
- Graphviz (DOT) と Mermaid を明確に区別する。
  - `ContentType.GRAPHVIZ` (`text/vnd.graphviz`)
  - `ContentType.MERMAID` (`text/vnd.mermaid`)

### 3. Interface

- `@project.task` デコレータに `content_type` 引数を追加し、開発者が戻り値の型を宣言できるようにする。

### 4. Rendering Strategy (Dashboard)

- Graphviz: Pythonの `graphviz` ライブラリと `st.graphviz_chart` を使用する。
- Mermaid: Streamlit標準コンポーネントがないため、`st.components.v1.html` を使用して Mermaid.js (CDN) を注入し、ブラウザ側でレンダリングする。

### 5. Dependencies

- `pyproject.toml` に `graphviz>=0.20.1` を追加する。

## Consequences / 決定

- Positive:
  - ダッシュボードで、生成AIが出力したアーキテクチャ図やフローチャートを直感的に閲覧できるようになる。
  - ユーザーはデータベースのアップグレード作業を意識する必要がない（自動化）。
  - データの「中身」を解析して表示形式を推測する不安定なロジックを排除できる。
- Negative:
  - OS依存: Graphvizのレンダリングには、ユーザー環境に `dot` コマンド（OSレベルのパッケージ）がインストールされている必要がある。
  - Client-Side Rendering: MermaidはCDN経由のJS実行となるため、オフライン環境では表示されない場合がある。

# Semantic Content Type Support ## Context and Problem Statement / コンテキスト 生成AIタスクの出力は、テキストだけでなく、画像、構造化データ、ダイアグラム（Mermaid, Graphviz/DOT, HTML）など多岐にわたります。 現状のデータベーススキーマ（`result_type` = `FILE` | `DIRECT`）は「データの保存形式」しか保持しておらず、「データの意味的種類（Semantic Type）」が不明であるため、ダッシュボードでの復元時に適切な可視化（レンダリング）ができません。 また、`beautyspot` はライブラリとしてユーザーの手元で動作するため、複雑なマイグレーション手順や重量級の依存関係（Alembicなど）を強制することはUXを損なうという課題があります。 ## Decision Drivers / 要求 * **Rich Visualization**: 生成された図やデータを、ダッシュボード上で適切な形式で閲覧できること。 * **Zero-Touch Upgrade**: スキーマ変更時にユーザーが手動でマイグレーションコマンドを打つ必要がないこと。 * **Lightweight**: ライブラリの依存関係を最小限に保つこと。 * **Type Safety**: データの保存形式（Blob/Text）と意味（Mermaid/Image等）を明確に分離すること。 ## Considered Options / 検討 * **Option 1**: 保存されたデータの拡張子や中身から動的に型を推論する。 * **Option 2**: スキーマに `content_type` カラムを追加し、開発者が `@task` で明示的に指定できるようにする。あわせて自動マイグレーション機能を実装する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ### 1. Database Schema & Migration * **Schema Change:** `tasks` テーブルに `content_type` (TEXT) カラムを追加する。 * **Migration Strategy:** **"Auto-Migration on Startup"** を採用する。 * Alembic等は導入しない（依存関係の軽量化とUX維持のため）。 * `Project` 初期化時（`_init_db`）に `PRAGMA table_info` でカラムの存在を確認し、不足していれば標準ライブラリのみで `ALTER TABLE` を自動実行する。 ### 2. Type Definition * `src/beautyspot/types.py` を新設し、`ContentType` クラスで定数を管理する。 * **Graphviz (DOT)** と **Mermaid** を明確に区別する。 * `ContentType.GRAPHVIZ` (`text/vnd.graphviz`) * `ContentType.MERMAID` (`text/vnd.mermaid`) ### 3. Interface * `@project.task` デコレータに `content_type` 引数を追加し、開発者が戻り値の型を宣言できるようにする。 ### 4. Rendering Strategy (Dashboard) * **Graphviz:** Pythonの `graphviz` ライブラリと `st.graphviz_chart` を使用する。 * **Mermaid:** Streamlit標準コンポーネントがないため、`st.components.v1.html` を使用して Mermaid.js (CDN) を注入し、ブラウザ側でレンダリングする。 ### 5. Dependencies * `pyproject.toml` に `graphviz>=0.20.1` を追加する。 ## Consequences / 決定 * **Positive**: * ダッシュボードで、生成AIが出力したアーキテクチャ図やフローチャートを直感的に閲覧できるようになる。 * ユーザーはデータベースのアップグレード作業を意識する必要がない（自動化）。 * データの「中身」を解析して表示形式を推測する不安定なロジックを排除できる。 * **Negative**: * **OS依存:** Graphvizのレンダリングには、ユーザー環境に `dot` コマンド（OSレベルのパッケージ）がインストールされている必要がある。 * **Client-Side Rendering:** MermaidはCDN経由のJS実行となるため、オフライン環境では表示されない場合がある。

親: REQ003

子: —

### ADR008 {h(g)} ✓ レビュー済 局所ビュー →

# Customizable Serialization Strategy with Msgpack Default

## Context and Problem Statement / コンテキスト

現在、`beautyspot` はキャッシュデータのシリアライズ（保存）に Python 標準の `pickle` を使用している。 これには以下の重大な課題がある：

1. セキュリティリスク (RCE): `pickle` は信頼できないデータを読み込む際に任意のコード実行（RCE）を引き起こす可能性があり、「共有キャッシュ」としての利用にリスクがある。
2. 互換性: Python のバージョンやライブラリのバージョンが変わると、クラス定義の不整合によりデシリアライズに失敗しやすい。
3. 代替案の欠点: 標準ライブラリの `json` は安全だが、画像などのバイナリデータを効率的に扱えず（Base64化でサイズ増）、タプルなどのPython固有型が失われる。

v1.0.0 リリースにあたり、「デフォルトで安全（Secure by Default）」 かつ 「拡張性（Extensibility）」 のあるシリアライズ戦略が必要である。

## Decision Drivers / 要求

- Security: デフォルト状態で RCE のリスクを排除したい。
- Performance: 画像やNumpy配列などのバイナリデータを、オーバーヘッドなく高速に扱いたい。
- Extensibility: ユーザーが独自のクラスや特殊な型（例: `pandas.DataFrame`）を、ライブラリ側の変更なしに保存・復元できるようにしたい。
- Developer Experience (DX): シリアライズ失敗時に、「どのオブジェクトが原因か」と「どうすれば解決できるか」を明確に示したい。

## Considered Options / 検討

- Option 1: 現状維持（`pickle` のまま）。警告文で責任をユーザーに委ねる。
- Option 2: 標準ライブラリ `json` に一本化する。バイナリは Base64 エンコードを強制する。
- Option 3: `msgpack` を採用し、標準型のみサポートする。
- Option 4: `msgpack` を採用し、`ExtType` 機能を用いた「カスタム型登録システム」を実装する。

## Decision Outcome / 決定

Chosen option: Option 4.

シリアライザのバックエンドとして `msgpack` (MessagePack) を採用し、さらにユーザーが任意の型変換ロジックを注入できる `TypeRegistry` パターンを導入する。

1. 依存関係: `msgpack>=1.0.0` を `pyproject.toml` に追加する。
2. デフォルトの挙動:
   - `pickle` はデフォルトから廃止（またはオプトイン化）する。
   - `msgpack` を使用し、安全な型のみをシリアライズする。
3. カスタム型の登録:
   - `Project` クラスに `register_type(type, code, encoder, decoder)` メソッドを追加する。
   - ユーザーは、シリアライズできない型（例: カスタムクラス）に対して、一意なID (`code`) と変換関数を登録することで対応できる。
4. エラーハンドリング:
   - 未登録の型に遭遇した場合、標準の `TypeError` ではなく、具体的なオブジェクト情報と `register_type` の利用を促す親切なエラーメッセージ (`SerializationError`) を送出する。

### Technical Details / 技術詳細

`msgpack` の `default` (pack時) と `ext_hook` (unpack時) を活用し、拡張型を `msgpack.ExtType` としてラップする。

    # Implementation Sketch

    class Project:
        def __init__(self, ...):
            # ...
            self.serializer = MsgpackSerializer()

        def register_type(self, type_: type, code: int, encoder: Callable, decoder: Callable):
            """
            Register a custom serializer for a specific type.

            Args:
                type_: The class to handle (e.g. MyClass)
                code: Unique integer ID (0-127) for this type
                encoder: Function that converts obj -> bytes
                decoder: Function that converts bytes -> obj
            """
            self.serializer.register(type_, code, encoder, decoder)

    # --- Internal ---

    class MsgpackSerializer:
        def dump(self, obj):
            return msgpack.packb(obj, default=self._default_packer, use_bin_type=True)

        def load(self, data):
            return msgpack.unpackb(data, ext_hook=self._ext_hook, raw=False)

        def _default_packer(self, obj):
            # 登録済みの型なら ExtType に変換
            if type(obj) in self._encoders:
                code, encoder = self._encoders[type(obj)]
                return msgpack.ExtType(code, encoder(obj))

            # 未登録なら詳細なエラーを出す
            raise SerializationError(f"Object of type '{type(obj).__name__}' is not serializable...")

## Consequences / 決定

- Positive:
  - 安全性: `pickle` を排除することで、デフォルトでの脆弱性を解消できる。
  - パフォーマンス: `json` + Base64 よりも高速かつ省サイズでバイナリデータを扱える。
  - 柔軟性: ユーザーはライブラリをフォークすることなく、必要な型（Numpy, Pandas, Custom Class）の対応を追加できる。
- Negative:
  - 依存関係: 新たに `msgpack` パッケージへの依存が発生する（ただし軽量であるため許容範囲とする）。
  - 移行コスト: 既存の `pickle` で保存されたキャッシュデータ (`.pkl`) との互換性がなくなるため、v1.0.0 アップデート時にキャッシュクリア（DBリセット）が必要になる。

## Updates (2025-12-11)

See ADR-0009 for further refinements regarding `save_blob=False` behavior (Msgpack + Base64) and size guardrails.

# Customizable Serialization Strategy with Msgpack Default ## Context and Problem Statement / コンテキスト 現在、`beautyspot` はキャッシュデータのシリアライズ（保存）に Python 標準の `pickle` を使用している。 これには以下の重大な課題がある： 1. **セキュリティリスク (RCE):** `pickle` は信頼できないデータを読み込む際に任意のコード実行（RCE）を引き起こす可能性があり、「共有キャッシュ」としての利用にリスクがある。 2. **互換性:** Python のバージョンやライブラリのバージョンが変わると、クラス定義の不整合によりデシリアライズに失敗しやすい。 3. **代替案の欠点:** 標準ライブラリの `json` は安全だが、画像などのバイナリデータを効率的に扱えず（Base64化でサイズ増）、タプルなどのPython固有型が失われる。 v1.0.0 リリースにあたり、**「デフォルトで安全（Secure by Default）」** かつ **「拡張性（Extensibility）」** のあるシリアライズ戦略が必要である。 ## Decision Drivers / 要求 * **Security:** デフォルト状態で RCE のリスクを排除したい。 * **Performance:** 画像やNumpy配列などのバイナリデータを、オーバーヘッドなく高速に扱いたい。 * **Extensibility:** ユーザーが独自のクラスや特殊な型（例: `pandas.DataFrame`）を、ライブラリ側の変更なしに保存・復元できるようにしたい。 * **Developer Experience (DX):** シリアライズ失敗時に、「どのオブジェクトが原因か」と「どうすれば解決できるか」を明確に示したい。 ## Considered Options / 検討 * **Option 1:** 現状維持（`pickle` のまま）。警告文で責任をユーザーに委ねる。 * **Option 2:** 標準ライブラリ `json` に一本化する。バイナリは Base64 エンコードを強制する。 * **Option 3:** `msgpack` を採用し、標準型のみサポートする。 * **Option 4:** `msgpack` を採用し、`ExtType` 機能を用いた「カスタム型登録システム」を実装する。 ## Decision Outcome / 決定 Chosen option: **Option 4**. シリアライザのバックエンドとして `msgpack` (MessagePack) を採用し、さらにユーザーが任意の型変換ロジックを注入できる `TypeRegistry` パターンを導入する。 1. **依存関係:** `msgpack>=1.0.0` を `pyproject.toml` に追加する。 2. **デフォルトの挙動:** * `pickle` はデフォルトから廃止（またはオプトイン化）する。 * `msgpack` を使用し、安全な型のみをシリアライズする。 3. **カスタム型の登録:** * `Project` クラスに `register_type(type, code, encoder, decoder)` メソッドを追加する。 * ユーザーは、シリアライズできない型（例: カスタムクラス）に対して、一意なID (`code`) と変換関数を登録することで対応できる。 4. **エラーハンドリング:** * 未登録の型に遭遇した場合、標準の `TypeError` ではなく、具体的なオブジェクト情報と `register_type` の利用を促す親切なエラーメッセージ (`SerializationError`) を送出する。 ### Technical Details / 技術詳細 `msgpack` の `default` (pack時) と `ext_hook` (unpack時) を活用し、拡張型を `msgpack.ExtType` としてラップする。 ```python # Implementation Sketch class Project: def __init__(self, ...): # ... self.serializer = MsgpackSerializer() def register_type(self, type_: type, code: int, encoder: Callable, decoder: Callable): """ Register a custom serializer for a specific type. Args: type_: The class to handle (e.g. MyClass) code: Unique integer ID (0-127) for this type encoder: Function that converts obj -> bytes decoder: Function that converts bytes -> obj """ self.serializer.register(type_, code, encoder, decoder) # --- Internal --- class MsgpackSerializer: def dump(self, obj): return msgpack.packb(obj, default=self._default_packer, use_bin_type=True) def load(self, data): return msgpack.unpackb(data, ext_hook=self._ext_hook, raw=False) def _default_packer(self, obj): # 登録済みの型なら ExtType に変換 if type(obj) in self._encoders: code, encoder = self._encoders[type(obj)] return msgpack.ExtType(code, encoder(obj)) # 未登録なら詳細なエラーを出す raise SerializationError(f"Object of type '{type(obj).__name__}' is not serializable...") ``` ## Consequences / 決定 * **Positive:** * **安全性:** `pickle` を排除することで、デフォルトでの脆弱性を解消できる。 * **パフォーマンス:** `json` + Base64 よりも高速かつ省サイズでバイナリデータを扱える。 * **柔軟性:** ユーザーはライブラリをフォークすることなく、必要な型（Numpy, Pandas, Custom Class）の対応を追加できる。 * **Negative:** * **依存関係:** 新たに `msgpack` パッケージへの依存が発生する（ただし軽量であるため許容範囲とする）。 * **移行コスト:** 既存の `pickle` で保存されたキャッシュデータ (`.pkl`) との互換性がなくなるため、v1.0.0 アップデート時にキャッシュクリア（DBリセット）が必要になる。 ## Updates (2025-12-11) See [ADR-0009](0009-msgpack-everywhere-and-guardrails.md) for further refinements regarding `save_blob=False` behavior (Msgpack + Base64) and size guardrails.

親: REQ005

子: —

### ADR009 {h(g)} ✓ レビュー済 局所ビュー →

# Database Dependency Injection and Abstraction

## Context and Problem Statement / コンテキスト

これまでの `beautyspot` (`v0.x`) は、`Project` クラス内部で `TaskDB` (SQLite実装) をハードコードしてインスタンス化していた。

    # v0.x implementation
    self.db = TaskDB(self.db_path)

この設計には以下の課題がある：

1. 拡張性の欠如: SQLite 以外のデータベース（PostgreSQL, DuckDB, In-Memory DB等）を使いたくても、ライブラリのコードを書き換えない限り不可能である。
2. テストの制約: ユニットテスト時に、ファイルシステムに依存しないモックDBやオンメモリDBへの差し替えが困難である。

v1.0.0 では、ユーザー体験（DX）としての「手軽さ（パスを指定するだけ）」を維持しつつ、アーキテクチャレベルでの柔軟性を確保する必要がある。

## Decision Drivers / 要求

- Extensibility: ユーザーが独自の `TaskDB` 実装を注入 (Inject) できるようにする。
- Backward Compatibility / DX: 従来通りファイルパス（文字列）を渡すだけの初期化もサポートし、ライトユーザーの負担を増やさない。
- Testability: データベース層のモック化を容易にする。

## Considered Options / 検討

- Option 1: 現状維持（SQLiteを内部でハードコード）。
- Option 2: `db_path` は維持しつつ、内部のファクトリで実装を切り替える。
- Option 3: `Project` に DB インスタンスを直接渡す Dependency Injection (DI) パターン。

## Decision Outcome / 決定

Chosen option: Option 3.

1. 抽象化: `src/beautyspot/db.py` に抽象基底クラス `TaskDB` を定義し、インターフェース（`save`, `get`, `init_schema` 等）を強制する。
2. 具象化: 従来のSQLite実装を `SQLiteTaskDB` として再定義する。
3. 注入 (DI): `Project` クラスのコンストラクタ引数を `db_path: str` から `db: Union[str, TaskDB]` に変更する。
   - `str` が渡された場合: 内部で `SQLiteTaskDB(path)` を生成する（Convenience）。
   - `TaskDB` が渡された場合: そのインスタンスをそのまま使用する（Injection）。

## Consequences / 決定

- Positive:
  - PostgreSQL や MySQL などのアダプタをユーザー側で実装し、`Project(..., db=PostgresDB(...))` のように利用可能になる。
  - テスト時に `MemoryTaskDB` などを差し込むことで、高速かつクリーンなテストが可能になる。
- Negative:
  - `db_path` 引数が廃止（または `db` に統合）されるため、既存コードの引数名変更が必要になる（v1.0.0 での破壊的変更）。

# Database Dependency Injection and Abstraction ## Context and Problem Statement / コンテキスト これまでの `beautyspot` (`v0.x`) は、`Project` クラス内部で `TaskDB` (SQLite実装) をハードコードしてインスタンス化していた。 ```python # v0.x implementation self.db = TaskDB(self.db_path) ``` この設計には以下の課題がある： 1. **拡張性の欠如:** SQLite 以外のデータベース（PostgreSQL, DuckDB, In-Memory DB等）を使いたくても、ライブラリのコードを書き換えない限り不可能である。 2. **テストの制約:** ユニットテスト時に、ファイルシステムに依存しないモックDBやオンメモリDBへの差し替えが困難である。 v1.0.0 では、ユーザー体験（DX）としての「手軽さ（パスを指定するだけ）」を維持しつつ、アーキテクチャレベルでの柔軟性を確保する必要がある。 ## Decision Drivers / 要求 * **Extensibility:** ユーザーが独自の `TaskDB` 実装を注入 (Inject) できるようにする。 * **Backward Compatibility / DX:** 従来通りファイルパス（文字列）を渡すだけの初期化もサポートし、ライトユーザーの負担を増やさない。 * **Testability:** データベース層のモック化を容易にする。 ## Considered Options / 検討 * **Option 1**: 現状維持（SQLiteを内部でハードコード）。 * **Option 2**: `db_path` は維持しつつ、内部のファクトリで実装を切り替える。 * **Option 3**: `Project` に DB インスタンスを直接渡す Dependency Injection (DI) パターン。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 1. **抽象化:** `src/beautyspot/db.py` に抽象基底クラス `TaskDB` を定義し、インターフェース（`save`, `get`, `init_schema` 等）を強制する。 2. **具象化:** 従来のSQLite実装を `SQLiteTaskDB` として再定義する。 3. **注入 (DI):** `Project` クラスのコンストラクタ引数を `db_path: str` から `db: Union[str, TaskDB]` に変更する。 * `str` が渡された場合: 内部で `SQLiteTaskDB(path)` を生成する（Convenience）。 * `TaskDB` が渡された場合: そのインスタンスをそのまま使用する（Injection）。 ## Consequences / 決定 * **Positive:** * PostgreSQL や MySQL などのアダプタをユーザー側で実装し、`Project(..., db=PostgresDB(...))` のように利用可能になる。 * テスト時に `MemoryTaskDB` などを差し込むことで、高速かつクリーンなテストが可能になる。 * **Negative:** * `db_path` 引数が廃止（または `db` に統合）されるため、既存コードの引数名変更が必要になる（v1.0.0 での破壊的変更）。

親: REQ012

子: —

### ADR010 {h(g)} ✓ レビュー済 局所ビュー →

# Msgpack Everywhere with Native BLOB Support

## Context and Problem Statement / コンテキスト

ADR-0007 で `MsgpackSerializer` を導入しましたが、SQLiteへの保存方式について以下の課題が残っていました。

1. JSONの限界: 従来の `TEXT` カラム（JSON）ではバイナリデータを扱えず、Numpy配列などが保存できない。
2. Base64の非効率性: `TEXT` カラムに保存するために Msgpack を Base64 エンコードする案がありましたが、データサイズが約33%増加し、CPUコストもかかる。
3. 一貫性: 画像や動画などのバイナリデータも、可能な限り変換なしで「そのまま」扱いたい。

## Decision Drivers / 要求

- Performance: シリアライズ/デシリアライズ의 オーバーヘッド（Base64等）を最小化したい。
- Efficiency: ストレージ容量を無駄に消費したくない。
- Consistency: 小さいデータ（DB内）も大きいデータ（外部ファイル）も、同じ Msgpack フォーマットで統一したい。

## Considered Options / 検討

- Option 1: Msgpack バイナリを Base64 エンコードして、既存の `TEXT` カラムに保存する。
- Option 2: データベースに `BLOB` カラムを追加し、Msgpack バイナリをそのまま保存する。

## Decision Outcome / 決定

Chosen option: Option 2.

### 1. Schema Change: Add BLOB Column

`tasks` テーブルに、バイナリデータをそのまま格納するための `result_data` (BLOB) カラムを追加します。

- 既存の `result_value` (TEXT) カラムは、ファイルパスやレガシーなJSONデータの保持用に継続利用する。
- マイグレーションは `Project` 初期化時に `ALTER TABLE` で自動的に行われる。

### 2. Msgpack Everywhere Strategy

データの保存先に関わらず、常に `MsgpackSerializer` を通過させます。

- `save_blob=False` (Direct Mode):
  - Msgpackバイナリを `result_data` (BLOB) カラム にそのまま保存する。
  - `result_type` は `DIRECT_BLOB` とする。
- `save_blob=True` (Blob Mode):
  - Msgpackバイナリを外部ファイル（`.bin`）として保存。
  - ファイルパスを `result_value` (TEXT) カラム に保存する。
  - `result_type` は `FILE` とする。

### 3. Size Guardrails (Unchanged)

`save_blob=False` であっても、閾値（デフォルト: 1MB）を超えるデータが渡された場合は、警告ログ (`WARNING`) を出力して `save_blob=True` の利用を促します。

## Consequences / 決定

- Positive:
  - 最高効率: Base64エンコードが不要になり、データサイズとCPU負荷が最小化される。
  - 互換性: 既存の `result_value` カラムを維持するため、旧バージョンのキャッシュデータも読み込み可能。
- Negative:
  - データベースのスキーマ変更（マイグレーション処理）が必要になる。
  - `sqlite3` コマンドラインツール等で `SELECT` した際、中身がバイナリのため視認できない（ダッシュボード等のツールが必要）。

# Msgpack Everywhere with Native BLOB Support ## Context and Problem Statement / コンテキスト ADR-0007 で `MsgpackSerializer` を導入しましたが、SQLiteへの保存方式について以下の課題が残っていました。 1. **JSONの限界:** 従来の `TEXT` カラム（JSON）ではバイナリデータを扱えず、Numpy配列などが保存できない。 2. **Base64の非効率性:** `TEXT` カラムに保存するために Msgpack を Base64 エンコードする案がありましたが、データサイズが約33%増加し、CPUコストもかかる。 3. **一貫性:** 画像や動画などのバイナリデータも、可能な限り変換なしで「そのまま」扱いたい。 ## Decision Drivers / 要求 * **Performance:** シリアライズ/デシリアライズ의 オーバーヘッド（Base64等）を最小化したい。 * **Efficiency:** ストレージ容量を無駄に消費したくない。 * **Consistency:** 小さいデータ（DB内）も大きいデータ（外部ファイル）も、同じ Msgpack フォーマットで統一したい。 ## Considered Options / 検討 * **Option 1**: Msgpack バイナリを Base64 エンコードして、既存の `TEXT` カラムに保存する。 * **Option 2**: データベースに `BLOB` カラムを追加し、Msgpack バイナリをそのまま保存する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ### 1. Schema Change: Add BLOB Column `tasks` テーブルに、バイナリデータをそのまま格納するための **`result_data` (BLOB)** カラムを追加します。 * 既存の `result_value` (TEXT) カラムは、ファイルパスやレガシーなJSONデータの保持用に継続利用する。 * マイグレーションは `Project` 初期化時に `ALTER TABLE` で自動的に行われる。 ### 2. Msgpack Everywhere Strategy データの保存先に関わらず、常に `MsgpackSerializer` を通過させます。 * **`save_blob=False` (Direct Mode):** * Msgpackバイナリを **`result_data` (BLOB) カラム** にそのまま保存する。 * `result_type` は **`DIRECT_BLOB`** とする。 * **`save_blob=True` (Blob Mode):** * Msgpackバイナリを外部ファイル（`.bin`）として保存。 * ファイルパスを **`result_value` (TEXT) カラム** に保存する。 * `result_type` は `FILE` とする。 ### 3. Size Guardrails (Unchanged) `save_blob=False` であっても、閾値（デフォルト: 1MB）を超えるデータが渡された場合は、警告ログ (`WARNING`) を出力して `save_blob=True` の利用を促します。 ## Consequences / 決定 * **Positive**: * **最高効率:** Base64エンコードが不要になり、データサイズとCPU負荷が最小化される。 * **互換性:** 既存の `result_value` カラムを維持するため、旧バージョンのキャッシュデータも読み込み可能。 * **Negative**: * データベースのスキーマ変更（マイグレーション処理）が必要になる。 * `sqlite3` コマンドラインツール等で `SELECT` した際、中身がバイナリのため視認できない（ダッシュボード等のツールが必要）。

親: REQ003

子: —

### ADR011 {h(g)} ✓ レビュー済 局所ビュー →

# Canonical Input Serialization with Msgpack and SHA-256

## Context and Problem Statement / コンテキスト

`beautyspot` はこれまで、キャッシュキーの生成に `json.dumps(sort_keys=True)` と `MD5` を使用していました。 しかし、以下の課題が顕在化していました：

1. バイナリデータの非効率性: Numpy配列や画像データなどをJSON化する際、テキスト変換（`tolist()` や `str()`）による巨大なオーバーヘッドとメモリ消費が発生する。
2. ハッシュの衝突リスク: `str()` に依存したフォールバックでは、巨大なNumpy配列が省略表示（`...`）された際にハッシュが衝突し、誤ったキャッシュヒットを引き起こす危険性がある。
3. アルゴリズムの老朽化: `MD5` は現代のセキュリティ基準では非推奨とされており、コンプライアンス上の懸念がある。

## Decision Drivers / 要求

- Accuracy: 入力データが少しでも異なれば、確実に異なるキーを生成したい（特にNumpy配列）。
- Performance: バイナリデータを効率的に扱いたい。
- Consistency: 入力（キー生成）も出力（データ保存）も `msgpack` エコシステムで統一したい。

## Considered Options / 検討

- Option 1: `json.dumps(sort_keys=True)` と `MD5` を継続利用する。
- Option 2: `msgpack` を用いた独自正規化ロジックと `SHA-256` を採用する。

## Decision Outcome / 決定

Chosen option: Option 2.

キャッシュキー生成ロジックを以下のように刷新します：

1. Canonicalization (正規化):
   - 独自の正規化関数 `canonicalize(obj)` を実装する。
   - Dict: キーでソートされた「タプルのリスト `[[k, v], ...]`」に変換し、順序を固定する。
   - Set: ソートされたリストに変換する。
   - Numpy: `numpy` をインポートせず、Duck Typing（`shape`, `dtype`, `tobytes` 属性の確認）により検知し、生のバイト列を含むタプルに変換する。これにより完全な一意性を保証する。
2. Serialization:
   - 正規化されたオブジェクトを `msgpack` でシリアライズする。
3. Hashing:
   - ハッシュアルゴリズムを `SHA-256` に変更する。

## Consequences / 決定

- Positive:
  - Numpy配列などのバイナリデータに対するハッシュ生成が劇的に高速化・省メモリ化される。
  - 省略表示によるハッシュ衝突バグが解消される。
  - `pickle` を使用しないため、安全性（RCEフリー）が維持される。
- Negative:
  - 破壊的変更: 既存のキャッシュ（v1.x）とはキーが一致しなくなるため、v2.0.0 でのリリースが必要。
  - 巨大で深くネストした辞書データの場合、Python側での正規化処理により若干のオーバーヘッド増が発生する可能性がある。

# Canonical Input Serialization with Msgpack and SHA-256 ## Context and Problem Statement / コンテキスト `beautyspot` はこれまで、キャッシュキーの生成に `json.dumps(sort_keys=True)` と `MD5` を使用していました。 しかし、以下の課題が顕在化していました： 1. **バイナリデータの非効率性:** Numpy配列や画像データなどをJSON化する際、テキスト変換（`tolist()` や `str()`）による巨大なオーバーヘッドとメモリ消費が発生する。 2. **ハッシュの衝突リスク:** `str()` に依存したフォールバックでは、巨大なNumpy配列が省略表示（`...`）された際にハッシュが衝突し、誤ったキャッシュヒットを引き起こす危険性がある。 3. **アルゴリズムの老朽化:** `MD5` は現代のセキュリティ基準では非推奨とされており、コンプライアンス上の懸念がある。 ## Decision Drivers / 要求 * **Accuracy:** 入力データが少しでも異なれば、確実に異なるキーを生成したい（特にNumpy配列）。 * **Performance:** バイナリデータを効率的に扱いたい。 * **Consistency:** 入力（キー生成）も出力（データ保存）も `msgpack` エコシステムで統一したい。 ## Considered Options / 検討 * **Option 1**: `json.dumps(sort_keys=True)` と `MD5` を継続利用する。 * **Option 2**: `msgpack` を用いた独自正規化ロジックと `SHA-256` を採用する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. キャッシュキー生成ロジックを以下のように刷新します： 1. **Canonicalization (正規化):** * 独自の正規化関数 `canonicalize(obj)` を実装する。 * **Dict:** キーでソートされた「タプルのリスト `[[k, v], ...]`」に変換し、順序を固定する。 * **Set:** ソートされたリストに変換する。 * **Numpy:** `numpy` をインポートせず、Duck Typing（`shape`, `dtype`, `tobytes` 属性の確認）により検知し、生のバイト列を含むタプルに変換する。これにより完全な一意性を保証する。 2. **Serialization:** * 正規化されたオブジェクトを `msgpack` でシリアライズする。 3. **Hashing:** * ハッシュアルゴリズムを `SHA-256` に変更する。 ## Consequences / 決定 * **Positive**: * Numpy配列などのバイナリデータに対するハッシュ生成が劇的に高速化・省メモリ化される。 * 省略表示によるハッシュ衝突バグが解消される。 * `pickle` を使用しないため、安全性（RCEフリー）が維持される。 * **Negative**: * **破壊的変更:** 既存のキャッシュ（v1.x）とはキーが一致しなくなるため、v2.0.0 でのリリースが必要。 * 巨大で深くネストした辞書データの場合、Python側での正規化処理により若干のオーバーヘッド増が発生する可能性がある。

親: REQ002

子: —

### ADR012 {h(g)} ✓ レビュー済 局所ビュー →

# Imperative Execution, Smart Defaults, and Workspace Management

## Context and Problem Statement / コンテキスト

これまで `beautyspot` は、関数のキャッシュ化にデコレータ (`@project.task`) を使用する方法のみを提供していました。しかし、実際の利用シナリオにおいて以下の課題が浮き彫りになりました：

1. サードパーティライブラリの利用: ソースコードを変更できない関数（例: `pandas.read_csv`）や、外部APIクライアントのメソッドをキャッシュしたい場合、わざわざラッパー関数を定義する必要があり、記述が冗長になる。
2. アドホックな解析: ノートブック環境や一時的なスクリプトで試行錯誤する際、関数定義の手間がオーバーヘッドとなり、開発体験（DX）を損ねる。
3. リソース管理の曖昧さ: `Project` インスタンスが内部で保持する `ThreadPoolExecutor` が適切にシャットダウンされない場合、プロセスがハングするリスクがある。
4. ディレクトリの汚染: 生成されるデータベースファイル (`.db`) やBlobディレクトリ (`blobs/`) がプロジェクトルート直下に作成され、ディレクトリ構成が煩雑になる。
5. 設定の重複: 複数のタスクで「常にBlob保存したい」「特定のバージョンを一律適用したい」といった場合、都度引数を指定するのは DRY (Don't Repeat Yourself) 原則に反する。
6. `None` 結果のキャッシュ: `time.sleep` のように戻り値が `None` である関数をキャッシュしようとすると、キャッシュミスと誤認され再実行されてしまうバグが存在した。

## Decision Drivers / 要求

- Flexibility: ソースコードを変更できない外部関数やアドホックな処理に対しても、簡単にキャッシュを適用できること。
- Safety: 非同期タスクや Executor のリソース管理を確実に行い、プロセスのクリーンな終了を保証すること。
- Clutter-free: 生成されるキャッシュファイルを1箇所に集約し、プロジェクト構成をきれいに保つこと。
- Accuracy: `None` を返す関数も含め、あらゆる戻り値を正しくキャッシュできること。

## Considered Options / 検討

- Option 1: 現状のデコレータ方式のみを維持し、ユーザーにラッパー関数の作成を求める。
- Option 2: 命令的な `run` メソッドの導入、ワークスペースディレクトリ（`.beautyspot/`）による集約、コンテキストマネージャによるリソース管理を統合的に実装する。

## Decision Outcome / 決定

Chosen option: Option 2.

以下の包括的なアーキテクチャ変更を行います。

### 1. 命令的実行メソッド (`project.run`) の導入

デコレータを使用せず、関数オブジェクトとその引数を渡すことで即座にキャッシュ付き実行を行う `run` メソッドを `Project` クラスに追加します。

    result = project.run(func, arg1, arg2, _save_blob=True)

- 引数設計: キャッシュ制御用のオプション引数は、対象関数のキーワード引数 (`kwargs`) との衝突を避けるため、プレフィックス `_` を付与します（例: `_save_blob`, `_version`, `_content_type`）。

### 2. コンテキストマネージャとリソース管理

`Project` クラスをコンテキストマネージャとして設計し、`with` ブロック内での利用を推奨パターンとします。これにより、ブロック終了時に確実に `shutdown()` が呼ばれ、スレッドプール等のリソースが解放されることを保証します。

### 3. 設定の階層化とスマートデフォルト

`Project` 初期化時にデフォルト設定を受け入れ、個別の実行時にそれを継承・上書きできる仕組みを導入します。優先順位は「個別指定 > プロジェクトデフォルト > システムデフォルト」とします。

### 4. ワークスペースディレクトリによる集約

すべての生成アーティファクト（DB、Blob）を、デフォルトで隠しディレクトリ `.beautyspot/` 配下に集約します。また、初期化時に `.gitignore` を自動生成し、キャッシュファイルがバージョン管理に含まれるのを防ぎます。

### 5. 番兵オブジェクトによるキャッシュ判定

キャッシュヒットの判定において `None` を特別視せず、専用の番兵オブジェクト (`CACHE_MISS`) を導入することで、`None` を返す関数も正しくキャッシュ可能とします。

## Consequences / 決定

- Positive:
  - 高い柔軟性: 既存のコードやライブラリ呼び出しに対して、ボイラープレートなしで即座にキャッシュを適用可能になった。
  - 安全なリソース管理: `with` 文の使用により、プロセスハングのリスクが大幅に低減した。
  - クリーンな環境: プロジェクトルートが汚れず、Git管理からの除外も自動化されたため、ユーザーの負担が減った。
  - 正確なキャッシュ: 副作用のみを目的とした関数（戻り値なし）も扱えるようになり、適用範囲が広がった。
- Negative:
  - APIの複雑化: `project.run` の引数に特殊なプレフィックスルール（`_`）が導入されたため、ドキュメントでの周知が必要。
  - 後方互換性の注意: 引数処理の変更に伴い、内部実装での厳密なチェックが必要になった。

# Imperative Execution, Smart Defaults, and Workspace Management ## Context and Problem Statement / コンテキスト これまで `beautyspot` は、関数のキャッシュ化にデコレータ (`@project.task`) を使用する方法のみを提供していました。しかし、実際の利用シナリオにおいて以下の課題が浮き彫りになりました： 1. **サードパーティライブラリの利用**: ソースコードを変更できない関数（例: `pandas.read_csv`）や、外部APIクライアントのメソッドをキャッシュしたい場合、わざわざラッパー関数を定義する必要があり、記述が冗長になる。 2. **アドホックな解析**: ノートブック環境や一時的なスクリプトで試行錯誤する際、関数定義の手間がオーバーヘッドとなり、開発体験（DX）を損ねる。 3. **リソース管理の曖昧さ**: `Project` インスタンスが内部で保持する `ThreadPoolExecutor` が適切にシャットダウンされない場合、プロセスがハングするリスクがある。 4. **ディレクトリの汚染**: 生成されるデータベースファイル (`.db`) やBlobディレクトリ (`blobs/`) がプロジェクトルート直下に作成され、ディレクトリ構成が煩雑になる。 5. **設定の重複**: 複数のタスクで「常にBlob保存したい」「特定のバージョンを一律適用したい」といった場合、都度引数を指定するのは DRY (Don't Repeat Yourself) 原則に反する。 6. **`None` 結果のキャッシュ**: `time.sleep` のように戻り値が `None` である関数をキャッシュしようとすると、キャッシュミスと誤認され再実行されてしまうバグが存在した。 ## Decision Drivers / 要求 * **Flexibility**: ソースコードを変更できない外部関数やアドホックな処理に対しても、簡単にキャッシュを適用できること。 * **Safety**: 非同期タスクや Executor のリソース管理を確実に行い、プロセスのクリーンな終了を保証すること。 * **Clutter-free**: 生成されるキャッシュファイルを1箇所に集約し、プロジェクト構成をきれいに保つこと。 * **Accuracy**: `None` を返す関数も含め、あらゆる戻り値を正しくキャッシュできること。 ## Considered Options / 検討 * **Option 1**: 現状のデコレータ方式のみを維持し、ユーザーにラッパー関数の作成を求める。 * **Option 2**: 命令的な `run` メソッドの導入、ワークスペースディレクトリ（`.beautyspot/`）による集約、コンテキストマネージャによるリソース管理を統合的に実装する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 以下の包括的なアーキテクチャ変更を行います。 ### 1. 命令的実行メソッド (`project.run`) の導入 デコレータを使用せず、関数オブジェクトとその引数を渡すことで即座にキャッシュ付き実行を行う `run` メソッドを `Project` クラスに追加します。 ```python result = project.run(func, arg1, arg2, _save_blob=True) ``` * **引数設計**: キャッシュ制御用のオプション引数は、対象関数のキーワード引数 (`kwargs`) との衝突を避けるため、プレフィックス `_` を付与します（例: `_save_blob`, `_version`, `_content_type`）。 ### 2. コンテキストマネージャとリソース管理 `Project` クラスをコンテキストマネージャとして設計し、`with` ブロック内での利用を推奨パターンとします。これにより、ブロック終了時に確実に `shutdown()` が呼ばれ、スレッドプール等のリソースが解放されることを保証します。 ### 3. 設定の階層化とスマートデフォルト `Project` 初期化時にデフォルト設定を受け入れ、個別の実行時にそれを継承・上書きできる仕組みを導入します。優先順位は「個別指定 > プロジェクトデフォルト > システムデフォルト」とします。 ### 4. ワークスペースディレクトリによる集約 すべての生成アーティファクト（DB、Blob）を、デフォルトで隠しディレクトリ `.beautyspot/` 配下に集約します。また、初期化時に `.gitignore` を自動生成し、キャッシュファイルがバージョン管理に含まれるのを防ぎます。 ### 5. 番兵オブジェクトによるキャッシュ判定 キャッシュヒットの判定において `None` を特別視せず、専用の番兵オブジェクト (`CACHE_MISS`) を導入することで、`None` を返す関数も正しくキャッシュ可能とします。 ## Consequences / 決定 * **Positive**: * **高い柔軟性**: 既存のコードやライブラリ呼び出しに対して、ボイラープレートなしで即座にキャッシュを適用可能になった。 * **安全なリソース管理**: `with` 文の使用により、プロセスハングのリスクが大幅に低減した。 * **クリーンな環境**: プロジェクトルートが汚れず、Git管理からの除外も自動化されたため、ユーザーの負担が減った。 * **正確なキャッシュ**: 副作用のみを目的とした関数（戻り値なし）も扱えるようになり、適用範囲が広がった。 * **Negative**: * **APIの複雑化**: `project.run` の引数に特殊なプレフィックスルール（`_`）が導入されたため、ドキュメントでの周知が必要。 * **後方互換性の注意**: 引数処理の変更に伴い、内部実装での厳密なチェックが必要になった。

親: REQ001

子: —

### ADR013 {h(g)} ✓ レビュー済 局所ビュー →

# Rename Project to Spot and Task to Mark

## Context and Problem Statement / コンテキスト

現在、`beautyspot` のメインエントリーポイントとして `Project` クラスが、タスク定義のデコレータとして `@project.task` が使用されています。しかし、これらの名称には以下の課題があります。

1. `Project` の曖昧さ: ユーザーにとって "Project" は、自身のソースコード全体やリポジトリを指す言葉であることが多いです。ライブラリの管理オブジェクトを `Project` と呼ぶことで、ユーザーのメンタルモデルとの衝突（「プロジェクトの中にプロジェクトがある？」）を招いています。
2. `task` の不一致: `task` は「仕事・課題」を表す名詞ですが、デコレータの役割は「関数を永続化対象として登録・設定する」という動的な作用です。名詞としての命名は、宣言的なデコレータの性質と完全に一致していません。
3. ブランド・アイデンティティの欠如: 現在の API は汎用的すぎて、このライブラリ特有の「黒子」や「美点（Beauty Spot）」というコンセプトがコード上で表現されていません。

## Decision Drivers / 要求

- Mental Model Alignment: ユーザーのプロジェクト構成とライブラリの管理単位を混同させないこと。
- Declarative API: デコレータの役割が「設定の付与（マーキング）」であることを直感的に伝えること。
- Brand Consistency: ライブラリ名 `beautyspot` と API の命名に一貫性を持たせること。

## Considered Options / 検討

- Option 1: 現状維持（`Project` および `task`）。
- Option 2: `Project` を `Spot` に、`task` を `mark` にリネームする。

## Decision Outcome / 決定

Chosen option: Option 2.

ライブラリのコアとなる用語を再定義し、以下のリネームを行います。

### 1. Rename `Project` class to `Spot`

管理クラスの名前を `Spot` に変更します。これにより、「コード上の特定の場所（Spot）を管理する」というニュアンスを持たせ、ライブラリ名 `beautyspot` との一貫性を持たせます。

### 2. Rename `@task` decorator to `@mark`

デコレータ名を `@spot.mark` に変更します。これは "Marking a spot"（地点に印を付ける）というイディオムに基づいており、「この関数を管理対象としてマークする」という宣言的な意図を明確にします。

### 3. Keep `run` method as `spot.run`

命令的な実行メソッドである `run` は、名前を変更せずそのまま維持します。これは、「`mark`（宣言・静的）」と「`run`（実行・動的）」という役割分担を明確にするためです。

## Consequences / 決定

- Positive:
  - 直感的なメンタルモデル: `Spot` と `mark` の組み合わせにより、「コード上の重要な箇所に印を付けて管理する」という思想が伝わりやすくなる。
  - 名前空間の明確化: 変数名 `spot` を使用することで、それが `beautyspot` のインスタンスであることが一目で分かる。
  - 一貫性: ライブラリ名とクラス名が一致し、ブランドとしての統一感が生まれる。
- Negative:
  - 破壊的変更: 既存の v1.x 系コードとは互換性がなくなる。
  - 移行コスト: 既存ユーザーはコードの書き換えが必要となる。

# Rename Project to Spot and Task to Mark ## Context and Problem Statement / コンテキスト 現在、`beautyspot` のメインエントリーポイントとして `Project` クラスが、タスク定義のデコレータとして `@project.task` が使用されています。しかし、これらの名称には以下の課題があります。 1. **`Project` の曖昧さ:** ユーザーにとって "Project" は、自身のソースコード全体やリポジトリを指す言葉であることが多いです。ライブラリの管理オブジェクトを `Project` と呼ぶことで、ユーザーのメンタルモデルとの衝突（「プロジェクトの中にプロジェクトがある？」）を招いています。 2. **`task` の不一致:** `task` は「仕事・課題」を表す名詞ですが、デコレータの役割は「関数を永続化対象として登録・設定する」という動的な作用です。名詞としての命名は、宣言的なデコレータの性質と完全に一致していません。 3. **ブランド・アイデンティティの欠如:** 現在の API は汎用的すぎて、このライブラリ特有の「黒子」や「美点（Beauty Spot）」というコンセプトがコード上で表現されていません。 ## Decision Drivers / 要求 * **Mental Model Alignment**: ユーザーのプロジェクト構成とライブラリの管理単位を混同させないこと。 * **Declarative API**: デコレータの役割が「設定の付与（マーキング）」であることを直感的に伝えること。 * **Brand Consistency**: ライブラリ名 `beautyspot` と API の命名に一貫性を持たせること。 ## Considered Options / 検討 * **Option 1**: 現状維持（`Project` および `task`）。 * **Option 2**: `Project` を `Spot` に、`task` を `mark` にリネームする。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ライブラリのコアとなる用語を再定義し、以下のリネームを行います。 ### 1. Rename `Project` class to `Spot` 管理クラスの名前を `Spot` に変更します。これにより、「コード上の特定の場所（Spot）を管理する」というニュアンスを持たせ、ライブラリ名 `beautyspot` との一貫性を持たせます。 ### 2. Rename `@task` decorator to `@mark` デコレータ名を `@spot.mark` に変更します。これは "Marking a spot"（地点に印を付ける）というイディオムに基づいており、「この関数を管理対象としてマークする」という宣言的な意図を明確にします。 ### 3. Keep `run` method as `spot.run` 命令的な実行メソッドである `run` は、名前を変更せずそのまま維持します。これは、「`mark`（宣言・静的）」と「`run`（実行・動的）」という役割分担を明確にするためです。 ## Consequences / 決定 * **Positive**: * **直感的なメンタルモデル:** `Spot` と `mark` の組み合わせにより、「コード上の重要な箇所に印を付けて管理する」という思想が伝わりやすくなる。 * **名前空間の明確化:** 変数名 `spot` を使用することで、それが `beautyspot` のインスタンスであることが一目で分かる。 * **一貫性:** ライブラリ名とクラス名が一致し、ブランドとしての統一感が生まれる。 * **Negative**: * **破壊的変更:** 既存の v1.x 系コードとは互換性がなくなる。 * **移行コスト:** 既存ユーザーはコードの書き換えが必要となる。

親: REQ001

子: —

### ADR014 {h(g)} ✓ レビュー済 局所ビュー →

# Decorator-based Type Registration with Late Binding

## Context and Problem Statement / コンテキスト

これまでの `spot.register_type()` は命令的であり、クラス定義と登録ロジックが分離してしまうため、凝集度が低いという課題がありました。 また、Pydantic モデルのようなクラスメソッド（例: `cls.model_validate_json`）をデコーダとして登録したい場合、デコレータ評価時にはまだクラスが未定義（NameError）であるため、綺麗に記述できないという技術的な制約がありました。

## Decision Drivers / 要求

- Developer Experience (DX): クラス定義の直上にシリアライズ設定を記述したい（Co-location）。
- Support for Modern Libraries: Pydantic など、クラスメソッドをファクトリとして使用するパターンを容易にサポートしたい。
- Safety: 循環参照や未定義エラーを回避しつつ、型安全に登録を行いたい。

## Considered Options / 検討

- Option 1: 命令的な `register_type()` メソッドのみを提供する。
- Option 2: デコレータベースの `register` メソッドを追加し、`decoder_factory` による遅延バインディングを導入する。

## Decision Outcome / 決定

Chosen option: Option 2.

`Spot` クラスに `register` デコレータメソッドを追加し、`decoder_factory` による遅延バインディング を導入します。

### Technical Details / 技術詳細

    @spot.register(
        code=10,
        encoder=lambda obj: ...,
        decoder_factory=lambda cls: cls.deserialize  # Class is passed here after definition
    )
    class MyModel:
        ...

1. `register` デコレータは、対象クラスの定義完了後（デコレート時）に実行される。
2. `decoder_factory` が指定されている場合、生成された `cls` オブジェクトを引数としてファクトリを実行し、実際の `decoder` 関数を取得する。
3. 取得した `decoder` を用いて、既存の `register_type` バックエンドに登録する。

## Consequences / 決定

- Positive:
  - ユーザーはクラス定義と登録を1箇所にまとめられる。
  - 自己参照を含むクラス（自分自身を返すクラスメソッドなど）の登録が容易になる。
- Negative:
  - `decoder` と `decoder_factory` という2つの引数が増え、APIが少し複雑になる（ドキュメントでの補完が必要）。

# Decorator-based Type Registration with Late Binding ## Context and Problem Statement / コンテキスト これまでの `spot.register_type()` は命令的であり、クラス定義と登録ロジックが分離してしまうため、凝集度が低いという課題がありました。 また、Pydantic モデルのようなクラスメソッド（例: `cls.model_validate_json`）をデコーダとして登録したい場合、デコレータ評価時にはまだクラスが未定義（NameError）であるため、綺麗に記述できないという技術的な制約がありました。 ## Decision Drivers / 要求 * **Developer Experience (DX):** クラス定義の直上にシリアライズ設定を記述したい（Co-location）。 * **Support for Modern Libraries:** Pydantic など、クラスメソッドをファクトリとして使用するパターンを容易にサポートしたい。 * **Safety:** 循環参照や未定義エラーを回避しつつ、型安全に登録を行いたい。 ## Considered Options / 検討 * **Option 1**: 命令的な `register_type()` メソッドのみを提供する。 * **Option 2**: デコレータベースの `register` メソッドを追加し、`decoder_factory` による遅延バインディングを導入する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `Spot` クラスに `register` デコレータメソッドを追加し、**`decoder_factory` による遅延バインディング** を導入します。 ### Technical Details / 技術詳細 ```python @spot.register( code=10, encoder=lambda obj: ..., decoder_factory=lambda cls: cls.deserialize # Class is passed here after definition ) class MyModel: ... ``` 1. `register` デコレータは、対象クラスの定義完了後（デコレート時）に実行される。 2. `decoder_factory` が指定されている場合、生成された `cls` オブジェクトを引数としてファクトリを実行し、実際の `decoder` 関数を取得する。 3. 取得した `decoder` を用いて、既存の `register_type` バックエンドに登録する。 ## Consequences / 決定 * **Positive**: * ユーザーはクラス定義と登録を1箇所にまとめられる。 * 自己参照を含むクラス（自分自身を返すクラスメソッドなど）の登録が容易になる。 * **Negative**: * `decoder` と `decoder_factory` という2つの引数が増え、APIが少し複雑になる（ドキュメントでの補完が必要）。

親: REQ005

子: —

### ADR015 {h(g)} ✓ レビュー済 局所ビュー →

# Strict Scoping for Imperative Execution (Runtime Guard)

## Context and Problem Statement / コンテキスト

`spot.cached_run()` コンテキストマネージャを使用すると、ユーザーは一時的に関数のキャッシュ挙動を適用できます。しかし、Python の `with` 文のスコープルールにより、ブロック内でバインドされた変数（例: `with ... as task:`）はブロック終了後もアクセス可能です。

これにより、ユーザーが特定のコンテキスト設定（`version="v1"` や一時的なストレージ設定など）を持つ関数ラッパーを、意図しない場所で再利用してしまうリスクがあります。これは微妙なバグやリソースリークの原因となります。

## Decision Drivers / 要求

- Safety: 意図しないスコープ外での「古い」または「コンテキスト固有の」ラッパーの使用を排除すること。
- Clarity: キャッシュ挙動が厳密に一時的なものであるというメンタルモデルを強制すること。
- Resource Management: 短命な `spot` インスタンスや注入されたインスタンスをより安全に使用できるようにすること。

## Considered Options / 検討

- Option 1: ガードを実装せず、ユーザーの注意に任せる。
- Option 2: Runtime Guard パターンを導入し、ブロック外での呼び出しを制限する。

## Decision Outcome / 決定

Chosen option: Option 2.

`ScopedMark` コンテキストマネージャに Runtime Guard パターンを実装します。

1. State Tracking: コンテキストマネージャはアクティブ状態のフラグを保持する。
2. Wrapper Guard: `cached_run` から返される関数は、実行前にこのフラグをチェックするガードでラップされる。
3. Fail Fast: ラップされた関数が `with` ブロックの外で呼び出された場合、即座に `RuntimeError` を送出する。

## Consequences / 決定

- Positive:
  - 意図しないスコープ外での使用によるリスクを排除できる。
  - 「キャッシュマジック」は厳密に一時的なものであるという確信が持てる。
  - 関数参照が使用直後に無効になるため、リソース管理が安全になる。
- Negative:
  - ブロック内での関数呼び出しごとに、極小のオーバーヘッド（ブールフラグのチェック）が発生する。
  - ラップ処理の追加により、`ScopedMark` の実装がわずかに複雑になる。

# Strict Scoping for Imperative Execution (Runtime Guard) ## Context and Problem Statement / コンテキスト `spot.cached_run()` コンテキストマネージャを使用すると、ユーザーは一時的に関数のキャッシュ挙動を適用できます。しかし、Python の `with` 文のスコープルールにより、ブロック内でバインドされた変数（例: `with ... as task:`）はブロック終了後もアクセス可能です。 これにより、ユーザーが特定のコンテキスト設定（`version="v1"` や一時的なストレージ設定など）を持つ関数ラッパーを、意図しない場所で再利用してしまうリスクがあります。これは微妙なバグやリソースリークの原因となります。 ## Decision Drivers / 要求 * **Safety**: 意図しないスコープ外での「古い」または「コンテキスト固有の」ラッパーの使用を排除すること。 * **Clarity**: キャッシュ挙動が厳密に一時的なものであるというメンタルモデルを強制すること。 * **Resource Management**: 短命な `spot` インスタンスや注入されたインスタンスをより安全に使用できるようにすること。 ## Considered Options / 検討 * **Option 1**: ガードを実装せず、ユーザーの注意に任せる。 * **Option 2**: **Runtime Guard** パターンを導入し、ブロック外での呼び出しを制限する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `ScopedMark` コンテキストマネージャに **Runtime Guard** パターンを実装します。 1. **State Tracking**: コンテキストマネージャはアクティブ状態のフラグを保持する。 2. **Wrapper Guard**: `cached_run` から返される関数は、実行前にこのフラグをチェックするガードでラップされる。 3. **Fail Fast**: ラップされた関数が `with` ブロックの外で呼び出された場合、即座に `RuntimeError` を送出する。 ## Consequences / 決定 * **Positive**: * 意図しないスコープ外での使用によるリスクを排除できる。 * 「キャッシュマジック」は厳密に一時的なものであるという確信が持てる。 * 関数参照が使用直後に無効になるため、リソース管理が安全になる。 * **Negative**: * ブロック内での関数呼び出しごとに、極小のオーバーヘッド（ブールフラグのチェック）が発生する。 * ラップ処理の追加により、`ScopedMark` の実装がわずかに複雑になる。

親: REQ001

子: —

### ADR016 {h(g)} ✓ レビュー済 局所ビュー →

# Interactive Deletion and Cleanup Policy

## Context and Problem Statement / コンテキスト

`beautyspot` は「試行錯誤の高速化」を掲げていますが、ユーザーが失敗した実験結果（キャッシュ）を即座に破棄する手段が Dashboard 上に存在しませんでした。 また、キャッシュ削除のロジックが `cli.py` に直接実装されており、Core API (`Spot` クラス) として提供されていないため、プログラムからの制御やテストが困難な状態でした。

加えて、削除機能を実現するには `BlobStorageBase` (Storage backend) に物理ファイルを削除するインターフェースが必要ですが、これまでの定義には `save` と `load` しか存在しませんでした。

## Decision Drivers / 要求

- User Experience: Dashboard 上で「結果を見て、即座に消して、やり直す」というループを実現したい。
- Consistency: CLI, Dashboard, Python Script すべてで同一の削除ロジックを利用したい。
- Cleanliness: DBレコードを消した際に、対応する Blob ファイルも確実に消去したい。

## Considered Options / 検討

- Option 1: 手動での削除（OS コマンドや DB の直接編集）をユーザーに委ねる。
- Option 2: Core API に `delete` メソッドを追加し、Storage インターフェースを拡張して、Dashboard からも操作可能にする。

## Decision Outcome / 決定

Chosen option: Option 2.

1. Core API への `delete` メソッドの追加 `Spot.delete(cache_key)` を正式な API として追加します。このメソッドは、「DBレコードの削除」と「Blobファイルの削除」をアトミック（ベストエフォート）に行う責任を持ちます。

2. Storage Interface への拡張 `BlobStorageBase` 抽象基底クラスに `delete(location)` メソッドを追加します。

3. Breaking Change: ユーザーが独自の Storage Backend を実装している場合、`delete` メソッドの実装が必要となります。

4. Idempotency: ファイルが既に存在しない場合でもエラーを送出せず、静かに終了する（冪等な挙動）ことを推奨実装とします。

5. Dashboard への削除機能の露出 Dashboard に削除ボタンを配置します。誤操作を防ぐため、確認ダイアログを経由する UI とし、`Spot.delete` を呼び出します。

## Consequences / 決定

- Positive:
  - ユーザーは Dashboard から離れることなく、不要なキャッシュを整理できる。
  - キャッシュ削除ロジックが一元化され、メンテナンス性が向上する。
  - 将来的な「自動ローテーション」機能の基盤となる。
- Negative:
  - `BlobStorageBase` を継承したカスタムクラスを持つ既存ユーザーコードは、`delete` メソッドの実装が必要になる。
  - リリースノートでの周知が必須となる規模の変更である。

# Interactive Deletion and Cleanup Policy ## Context and Problem Statement / コンテキスト `beautyspot` は「試行錯誤の高速化」を掲げていますが、ユーザーが失敗した実験結果（キャッシュ）を即座に破棄する手段が Dashboard 上に存在しませんでした。 また、キャッシュ削除のロジックが `cli.py` に直接実装されており、Core API (`Spot` クラス) として提供されていないため、プログラムからの制御やテストが困難な状態でした。 加えて、削除機能を実現するには `BlobStorageBase` (Storage backend) に物理ファイルを削除するインターフェースが必要ですが、これまでの定義には `save` と `load` しか存在しませんでした。 ## Decision Drivers / 要求 * **User Experience:** Dashboard 上で「結果を見て、即座に消して、やり直す」というループを実現したい。 * **Consistency:** CLI, Dashboard, Python Script すべてで同一の削除ロジックを利用したい。 * **Cleanliness:** DBレコードを消した際に、対応する Blob ファイルも確実に消去したい。 ## Considered Options / 検討 * **Option 1**: 手動での削除（OS コマンドや DB の直接編集）をユーザーに委ねる。 * **Option 2**: Core API に `delete` メソッドを追加し、Storage インターフェースを拡張して、Dashboard からも操作可能にする。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 1. **Core API への `delete` メソッドの追加** `Spot.delete(cache_key)` を正式な API として追加します。このメソッドは、「DBレコードの削除」と「Blobファイルの削除」をアトミック（ベストエフォート）に行う責任を持ちます。 2. **Storage Interface への拡張** `BlobStorageBase` 抽象基底クラスに `delete(location)` メソッドを追加します。 * **Breaking Change:** ユーザーが独自の Storage Backend を実装している場合、`delete` メソッドの実装が必要となります。 * **Idempotency:** ファイルが既に存在しない場合でもエラーを送出せず、静かに終了する（冪等な挙動）ことを推奨実装とします。 3. **Dashboard への削除機能の露出** Dashboard に削除ボタンを配置します。誤操作を防ぐため、確認ダイアログを経由する UI とし、`Spot.delete` を呼び出します。 ## Consequences / 決定 * **Positive**: * ユーザーは Dashboard から離れることなく、不要なキャッシュを整理できる。 * キャッシュ削除ロジックが一元化され、メンテナンス性が向上する。 * 将来的な「自動ローテーション」機能の基盤となる。 * **Negative**: * `BlobStorageBase` を継承したカスタムクラスを持つ既存ユーザーコードは、`delete` メソッドの実装が必要になる。 * リリースノートでの周知が必須となる規模の変更である。

親: REQ010

子: —

### ADR017 {h(g)} ✓ レビュー済 局所ビュー →

title: Nested Msgpack Protocol via Serializer Wrapping status: Accepted date: 2026-02-02 context: Improving DX for Custom Types

# Nested Msgpack Protocol via Serializer Wrapping

## Context and Problem Statement / コンテキスト

これまでは、カスタム型のエンコーダは `ExtType` の仕様に合わせて「生のバイト列」を返す必要がありました。 これにより、ユーザーは `msgpack` ライブラリを直接操作する必要があり、Pydantic モデルなどの扱いが煩雑になっていました。

## Decision Drivers / 要求

- Simplicity: ユーザー関数は、ただの型変換（Obj <-> Dict）のみに集中すべきである。
- Encapsulation: バイナリ化（Pack/Unpack）の責務はライブラリ側が負うべきである。

## Considered Options / 検討

- Option 1: エンコーダが生のバイト列（`bytes`）を返し、デコーダがそれを受け取る（旧仕様）。
- Option 2: シリアライザラッピングを採用。エンコーダは Python オブジェクト（Dict 等）を返し、ライブラリが内部で Msgpack 化して保持する。

## Decision Outcome / 決定

Chosen option: Option 2.

Serializer Wrapping (Nested Protocol) を採用します。

1. `MsgpackSerializer` は、ユーザー定義のエンコーダ/デコーダに対するラッパー（Wrapper）として機能する。
2. Encode時: エンコーダが返したオブジェクト（中間表現）を、ライブラリが自動的に `packb` して `ExtType` に格納する。
3. Decode時: `ExtType` のデータを、ライブラリが自動的に `unpackb` してからデコーダに渡す。
4. これにより、ユーザーは `import msgpack` をする必要がなくなり、直感的な実装が可能になる。

## Consequences / 決定

- Positive:
  - Pydantic v2 や Dataclasses のサポートが極めて容易になる。
  - 内部的に再帰呼び出しを行うため、カスタム型のネストも自動的に解決される。
- Negative:
  - 内部での再帰的な Pack/Unpack による、わずかなパフォーマンス上のオーバーヘッドが発生する可能性がある。

title: Nested Msgpack Protocol via Serializer Wrapping status: Accepted date: 2026-02-02 context: Improving DX for Custom Types --- # Nested Msgpack Protocol via Serializer Wrapping ## Context and Problem Statement / コンテキスト これまでは、カスタム型のエンコーダは `ExtType` の仕様に合わせて「生のバイト列」を返す必要がありました。 これにより、ユーザーは `msgpack` ライブラリを直接操作する必要があり、Pydantic モデルなどの扱いが煩雑になっていました。 ## Decision Drivers / 要求 * **Simplicity:** ユーザー関数は、ただの型変換（Obj <-> Dict）のみに集中すべきである。 * **Encapsulation:** バイナリ化（Pack/Unpack）の責務はライブラリ側が負うべきである。 ## Considered Options / 検討 * **Option 1**: エンコーダが生のバイト列（`bytes`）を返し、デコーダがそれを受け取る（旧仕様）。 * **Option 2**: シリアライザラッピングを採用。エンコーダは Python オブジェクト（Dict 等）を返し、ライブラリが内部で Msgpack 化して保持する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. **Serializer Wrapping (Nested Protocol)** を採用します。 1. `MsgpackSerializer` は、ユーザー定義のエンコーダ/デコーダに対する**ラッパー（Wrapper）**として機能する。 2. **Encode時:** エンコーダが返したオブジェクト（中間表現）を、ライブラリが自動的に `packb` して `ExtType` に格納する。 3. **Decode時:** `ExtType` のデータを、ライブラリが自動的に `unpackb` してからデコーダに渡す。 4. これにより、ユーザーは `import msgpack` をする必要がなくなり、直感的な実装が可能になる。 ## Consequences / 決定 * **Positive**: * Pydantic v2 や Dataclasses のサポートが極めて容易になる。 * 内部的に再帰呼び出しを行うため、カスタム型のネストも自動的に解決される。 * **Negative**: * 内部での再帰的な Pack/Unpack による、わずかなパフォーマンス上のオーバーヘッドが発生する可能性がある。

親: REQ005

子: —

### ADR018 {h(g)} ✓ レビュー済 局所ビュー →

# Expansion of Testing Strategy: Introduction of High-Level Integration Tests

## Context and Problem Statement / コンテキスト

現在、`beautyspot` のテストスイート（`tests/`）は、主にユニットテストや特定の機能（シリアライザー、リミッターなど）に焦点を当てたテストで構成されている。これらは個々のコンポーネントの正確性を保証する上では有効だが、以下の課題がある：

1. 連携の検証不足: 複数のタスクが依存関係を持つパイプライン（Load -> Process -> Train）において、バージョニングやキャッシュの伝播が正しく行われるかどうかの検証が不十分である。
2. 実環境との乖離: 多くの場合、`CliRunner` やモックオブジェクトを使用しているため、実際のファイルシステムや SQLite データベースに対する副作用（ファイルの生成、削除、ロック競合など）を検証できていない。
3. ユーザー体験の保証: CLI コマンド（`stats`, `clean` など）と Python API による実行結果の整合性を、ユーザーの一連の操作フローとして検証する仕組みがない。

「個々の部品は正しいが、組み合わせると意図した通りに動かない」という統合レベルのバグを防ぐため、テスト戦略を見直す必要がある。

## Decision Drivers / 要求

- Reliability: ユーザーが実際に構築するような複雑なパイプラインにおいても、キャッシュや依存関係解決が正しく動作することを保証したい。
- Refactoring Safety: 内部実装（例: DBスキーマや保存ロジック）を変更した際に、ユーザーから見た挙動が変わっていないことを高いレベルで検知したい。
- Documentation: テストコード自体を「ユーザーがどのようにライブラリを使うべきか」を示す実行可能なドキュメント（Executable Documentation）として機能させたい。

## Considered Options / 検討

- Option 1: 現状維持（ユニットテストの拡充）。
  - Pros: 実行が高速。
  - Cons: コンポーネント間の連携ミスや、実IOに伴う問題を見逃すリスクが高い。
- Option 2: 手動QA手順書の作成。
  - Pros: 実装コストが低い。
  - Cons: 再現性が低く、CIで自動化できないため、リリースのボトルネックになる。
- Option 3: `tests/integration/` ディレクトリを新設し、E2E（End-to-End）シナリオテストを導入する。
  - Pros: 実際のユーザー利用に近い形で品質を保証できる。
  - Cons: IOを伴うため実行時間が長くなる。セットアップがやや複雑になる。

## Decision Outcome / 決定

Chosen option: Option 3.

テストピラミッドの上層として、以下の戦略でインテグレーションテストを導入する：

1. ディレクトリ構成: `tests/integration/` を新設し、ユニットテストとは明確に分離する。
2. テスト範囲 (Scope):
   - Pipeline E2E: データのロードから学習までの一連のタスクフローを定義し、初回の実行、キャッシュヒット、バージョニングによる部分再計算（依存関係の解決）を検証する。
   - CLI Integration: Python コードで生成された DB ファイルに対し、`beautyspot` CLI コマンド（`stats`, `list` 等）を実行し、出力が正しいことを検証する。
3. 技術的制約:
   - モックの使用は最小限に留め、原則として実ファイル（SQLite DB, Blobファイル）を使用する（`tmp_path` フィクスチャを活用）。
   - `beautyspot.Spot` クラスを実際にインスタンス化し、ユーザーコードと同じインターフェース経由で操作を行う。

### Example Scenario / 想定シナリオ

    def test_ml_pipeline_lifecycle(spot_env):
        # 1. Pipeline Definition (Load -> Process -> Train)
        # 2. Initial Run (Assert all executed)
        # 3. Cache Hit Run (Assert none executed)
        # 4. Version Upgrade of Middle Task (Assert downstream re-executed)
        # 5. CLI "stats" command check (Assert DB integrity)

## Consequences / 決定

- Positive:
  - リリース前の信頼性が大幅に向上する。特に「キャッシュが効きすぎて更新されない」「意図せず再計算される」といった複雑なバグを検知しやすくなる。
  - CLI と Python API の互換性が常に保証される。
- Negative:
  - テスト実行時間が増加する（ファイルIOを含むため）。
  - テストコードのメンテナンスコストが若干増加する。

# Expansion of Testing Strategy: Introduction of High-Level Integration Tests ## Context and Problem Statement / コンテキスト 現在、`beautyspot` のテストスイート（`tests/`）は、主にユニットテストや特定の機能（シリアライザー、リミッターなど）に焦点を当てたテストで構成されている。これらは個々のコンポーネントの正確性を保証する上では有効だが、以下の課題がある： 1. **連携の検証不足**: 複数のタスクが依存関係を持つパイプライン（Load -> Process -> Train）において、バージョニングやキャッシュの伝播が正しく行われるかどうかの検証が不十分である。 2. **実環境との乖離**: 多くの場合、`CliRunner` やモックオブジェクトを使用しているため、実際のファイルシステムや SQLite データベースに対する副作用（ファイルの生成、削除、ロック競合など）を検証できていない。 3. **ユーザー体験の保証**: CLI コマンド（`stats`, `clean` など）と Python API による実行結果の整合性を、ユーザーの一連の操作フローとして検証する仕組みがない。 「個々の部品は正しいが、組み合わせると意図した通りに動かない」という統合レベルのバグを防ぐため、テスト戦略を見直す必要がある。 ## Decision Drivers / 要求 * **Reliability**: ユーザーが実際に構築するような複雑なパイプラインにおいても、キャッシュや依存関係解決が正しく動作することを保証したい。 * **Refactoring Safety**: 内部実装（例: DBスキーマや保存ロジック）を変更した際に、ユーザーから見た挙動が変わっていないことを高いレベルで検知したい。 * **Documentation**: テストコード自体を「ユーザーがどのようにライブラリを使うべきか」を示す実行可能なドキュメント（Executable Documentation）として機能させたい。 ## Considered Options / 検討 * **Option 1**: 現状維持（ユニットテストの拡充）。 * *Pros*: 実行が高速。 * *Cons*: コンポーネント間の連携ミスや、実IOに伴う問題を見逃すリスクが高い。 * **Option 2**: 手動QA手順書の作成。 * *Pros*: 実装コストが低い。 * *Cons*: 再現性が低く、CIで自動化できないため、リリースのボトルネックになる。 * **Option 3**: `tests/integration/` ディレクトリを新設し、E2E（End-to-End）シナリオテストを導入する。 * *Pros*: 実際のユーザー利用に近い形で品質を保証できる。 * *Cons*: IOを伴うため実行時間が長くなる。セットアップがやや複雑になる。 ## Decision Outcome / 決定 Chosen option: **Option 3**. テストピラミッドの上層として、以下の戦略でインテグレーションテストを導入する： 1. **ディレクトリ構成**: `tests/integration/` を新設し、ユニットテストとは明確に分離する。 2. **テスト範囲 (Scope)**: * **Pipeline E2E**: データのロードから学習までの一連のタスクフローを定義し、初回の実行、キャッシュヒット、バージョニングによる部分再計算（依存関係の解決）を検証する。 * **CLI Integration**: Python コードで生成された DB ファイルに対し、`beautyspot` CLI コマンド（`stats`, `list` 等）を実行し、出力が正しいことを検証する。 3. **技術的制約**: * モックの使用は最小限に留め、原則として**実ファイル（SQLite DB, Blobファイル）**を使用する（`tmp_path` フィクスチャを活用）。 * `beautyspot.Spot` クラスを実際にインスタンス化し、ユーザーコードと同じインターフェース経由で操作を行う。 ### Example Scenario / 想定シナリオ ```python def test_ml_pipeline_lifecycle(spot_env): # 1. Pipeline Definition (Load -> Process -> Train) # 2. Initial Run (Assert all executed) # 3. Cache Hit Run (Assert none executed) # 4. Version Upgrade of Middle Task (Assert downstream re-executed) # 5. CLI "stats" command check (Assert DB integrity) ``` ## Consequences / 決定 * **Positive**: * リリース前の信頼性が大幅に向上する。特に「キャッシュが効きすぎて更新されない」「意図せず再計算される」といった複雑なバグを検知しやすくなる。 * CLI と Python API の互換性が常に保証される。 * **Negative**: * テスト実行時間が増加する（ファイルIOを含むため）。 * テストコードのメンテナンスコストが若干増加する。

親: REQ010

子: —

### ADR019 {h(g)} ✓ レビュー済 局所ビュー →

# Per-Task Serializer Override

## Context and Problem Statement / コンテキスト

プロジェクト全体としては安全性と互換性の観点から `msgpack` を標準シリアライザとしています。 しかし、探索的データ分析（EDA）やプロトタイピング、あるいは `msgpack` でのシリアライズが困難なサードパーティ製オブジェクトを扱う特定のタスクにおいては、Python標準の `pickle` のような柔軟なシリアライザを局所的に使用したいというニーズがあります。

## Decision Drivers / 要求

- Flexibility: 標準シリアライザで扱いにくい特殊なオブジェクトに対して、最適な保存方法を選択できること。
- Locality: プロジェクト全体の設定（セキュリティポリシー等）を損なうことなく、特定の箇所だけで設定を上書きできること。
- Fault Tolerance: オーバーライドされたシリアライザでエラーが発生しても、再計算によって実行を継続できること。

## Considered Options / 検討

- Option 1: グローバルなシリアライザ設定のみを提供し、個別の変更は認めない。
- Option 2: `Spot.mark()` および `Spot.cached_run()` に `serializer` 引数を追加し、局所的なオーバーライドを可能にする。

## Decision Outcome / 決定

Chosen option: Option 2.

1. Override Mechanism: `Spot.mark()` および `Spot.cached_run()` に、オプショナル引数 `serializer` を追加します。
2. Protocol: ユーザーは `dumps(obj) -> bytes` および `loads(bytes) -> obj` メソッドを持つ任意のオブジェクトを渡すことができます。
3. Fallback Behavior: 指定されたシリアライザでデシリアライズに失敗した場合、ADR-0003 の方針に従い、キャッシュ破損として扱い、タスクを再実行します。

## Consequences / 決定

- Positive:
  - ユーザーはプロジェクト全体の設定を変更することなく、必要な箇所だけで `pickle` 等の機能を利用できる。
  - キャッシュが読み込めなくなった場合も、自動的に再計算されるため、開発体験（DX）が損なわれない。
- Negative:
  - `pickle` を使用したタスクは、異なる環境間や長期間の保存において互換性が保証されない。
  - `Spot.register()` で登録したカスタム型変換は、オーバーライドされたシリアライザには適用されない。

# Per-Task Serializer Override ## Context and Problem Statement / コンテキスト プロジェクト全体としては安全性と互換性の観点から `msgpack` を標準シリアライザとしています。 しかし、探索的データ分析（EDA）やプロトタイピング、あるいは `msgpack` でのシリアライズが困難なサードパーティ製オブジェクトを扱う特定のタスクにおいては、Python標準の `pickle` のような柔軟なシリアライザを局所的に使用したいというニーズがあります。 ## Decision Drivers / 要求 * **Flexibility**: 標準シリアライザで扱いにくい特殊なオブジェクトに対して、最適な保存方法を選択できること。 * **Locality**: プロジェクト全体の設定（セキュリティポリシー等）を損なうことなく、特定の箇所だけで設定を上書きできること。 * **Fault Tolerance**: オーバーライドされたシリアライザでエラーが発生しても、再計算によって実行を継続できること。 ## Considered Options / 検討 * **Option 1**: グローバルなシリアライザ設定のみを提供し、個別の変更は認めない。 * **Option 2**: `Spot.mark()` および `Spot.cached_run()` に `serializer` 引数を追加し、局所的なオーバーライドを可能にする。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 1. **Override Mechanism:** `Spot.mark()` および `Spot.cached_run()` に、オプショナル引数 `serializer` を追加します。 2. **Protocol:** ユーザーは `dumps(obj) -> bytes` および `loads(bytes) -> obj` メソッドを持つ任意のオブジェクトを渡すことができます。 3. **Fallback Behavior:** 指定されたシリアライザでデシリアライズに失敗した場合、ADR-0003 の方針に従い、キャッシュ破損として扱い、タスクを再実行します。 ## Consequences / 決定 * **Positive**: * ユーザーはプロジェクト全体の設定を変更することなく、必要な箇所だけで `pickle` 等の機能を利用できる。 * キャッシュが読み込めなくなった場合も、自動的に再計算されるため、開発体験（DX）が損なわれない。 * **Negative**: * `pickle` を使用したタスクは、異なる環境間や長期間の保存において互換性が保証されない。 * `Spot.register()` で登録したカスタム型変換は、オーバーライドされたシリアライザには適用されない。

親: REQ005

子: —

### ADR020 {h(g)} ✓ レビュー済 局所ビュー →

# Tolerant Deletion Policy for Task Cleanup

## Context and Problem Statement / コンテキスト

`spot.delete(key)` 機能は、特定のタスクに関連する「キャッシュレコード（DB）」と「実データ（Blob/File）」の両方を削除することを目的としています。

ローカルファイルシステムやS3などの外部ストレージにおいて、Blobの削除操作は様々な理由（ネットワーク障害、一時的な権限エラー、ファイルが既に手動で削除されている等）で失敗する可能性があります。

このとき、厳密な整合性を求めて「Blob削除に失敗したらDBレコードの削除もロールバック（中断）する」という実装にすると、以下の問題が発生します：

1. ゾンビレコード: 物理ファイルが見つからないだけなのに、DBからレコードを消せず、ユーザーは永遠にそのタスクを「無効化」できない。
2. 再計算の阻害: 破損したキャッシュエントリが残り続けることで、新しい計算結果での上書きや、クリーンな状態からの再実行が妨げられる。

## Decision Drivers / 要求

- Guaranteed Invalidation: ストレージの状態に関わらず、ユーザーが「削除」を指示したタスクを確実に管理対象から外すこと。
- Workflow Continuity: 物理ファイルの欠落などの非本質的なエラーで、ユーザーのワークフローを中断させないこと。
- Consistency Management: DB とストレージの乖離を最小限に抑えつつ、システムの健全性を維持すること。

## Considered Options / 検討

- Option 1: 厳密な削除。Blob の削除に失敗した場合は例外を送出し、DB のレコード削除も行わない。
- Option 2: 寛容な削除（Tolerant Deletion）。DB レコードの削除を優先し、Blob 削除時のエラーはログ出力に留めて処理を継続する。

## Decision Outcome / 決定

Chosen option: Option 2.

削除操作において 「メタデータの削除を優先する」 ポリシーを採用します。

1. まず、Blob（実データ）の削除を試みる。
2. Blobの削除中に例外が発生した場合、処理を中断せず、`WARNING` レベルのログを出力してエラーを捕捉する。
3. Blob削除の成否に関わらず、DB上のタスクレコードの削除を必ず実行する。

## Consequences / 決定

- Positive:
  - ユーザーはストレージの状態に関わらず、`beautyspot` の管理下からタスクを確実に削除できる。
  - 「ファイルが見つからない」といった些細なエラーでワークフローが止まることを防げる。
- Negative:
  - 孤立ファイル (Orphaned Blobs): DBレコードは削除されたが、ストレージ上にファイルだけが残るケースが発生しうる。
  - 対策として、別途ガベージコレクション（GC）ツールやストレージ側のライフサイクルポリシーでの対処が必要になる場合がある。

# Tolerant Deletion Policy for Task Cleanup ## Context and Problem Statement / コンテキスト `spot.delete(key)` 機能は、特定のタスクに関連する「キャッシュレコード（DB）」と「実データ（Blob/File）」の両方を削除することを目的としています。 ローカルファイルシステムやS3などの外部ストレージにおいて、Blobの削除操作は様々な理由（ネットワーク障害、一時的な権限エラー、ファイルが既に手動で削除されている等）で失敗する可能性があります。 このとき、厳密な整合性を求めて「Blob削除に失敗したらDBレコードの削除もロールバック（中断）する」という実装にすると、以下の問題が発生します： 1. **ゾンビレコード**: 物理ファイルが見つからないだけなのに、DBからレコードを消せず、ユーザーは永遠にそのタスクを「無効化」できない。 2. **再計算の阻害**: 破損したキャッシュエントリが残り続けることで、新しい計算結果での上書きや、クリーンな状態からの再実行が妨げられる。 ## Decision Drivers / 要求 * **Guaranteed Invalidation**: ストレージの状態に関わらず、ユーザーが「削除」を指示したタスクを確実に管理対象から外すこと。 * **Workflow Continuity**: 物理ファイルの欠落などの非本質的なエラーで、ユーザーのワークフローを中断させないこと。 * **Consistency Management**: DB とストレージの乖離を最小限に抑えつつ、システムの健全性を維持すること。 ## Considered Options / 検討 * **Option 1**: 厳密な削除。Blob の削除に失敗した場合は例外を送出し、DB のレコード削除も行わない。 * **Option 2**: 寛容な削除（Tolerant Deletion）。DB レコードの削除を優先し、Blob 削除時のエラーはログ出力に留めて処理を継続する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 削除操作において **「メタデータの削除を優先する」** ポリシーを採用します。 1. まず、Blob（実データ）の削除を試みる。 2. Blobの削除中に例外が発生した場合、**処理を中断せず**、`WARNING` レベルのログを出力してエラーを捕捉する。 3. Blob削除の成否に関わらず、DB上のタスクレコードの削除を必ず実行する。 ## Consequences / 決定 * **Positive**: * ユーザーはストレージの状態に関わらず、`beautyspot` の管理下からタスクを確実に削除できる。 * 「ファイルが見つからない」といった些細なエラーでワークフローが止まることを防げる。 * **Negative**: * **孤立ファイル (Orphaned Blobs)**: DBレコードは削除されたが、ストレージ上にファイルだけが残るケースが発生しうる。 * 対策として、別途ガベージコレクション（GC）ツールやストレージ側のライフサイクルポリシーでの対処が必要になる場合がある。

親: REQ010

子: —

### ADR021 {h(g)} ✓ レビュー済 局所ビュー →

# Code Visualization and Quality Metrics Strategy

## Context and Problem Statement / コンテキスト

プロジェクトの規模拡大に伴い、以下の課題が発生しています：

1. 構造の把握困難: モジュール間の依存関係が複雑化し、全体像を掴みにくい。
2. 品質の定量化: コードの複雑さが主観で語られており、客観的なリファクタリング基準がない。
3. アーキテクチャの健全性: 「安定依存の原則（SDP）」が守られているか（不安定なモジュールが安定したモジュールに依存していないか）を確認する手段がない。

## Decision Drivers / 要求

- Objectivity: リファクタリングの優先順位を、数値に基づく客観的な基準で決定すること。
- Visibility: モジュール間の依存関係を視覚化し、設計上の「逆流」を容易に発見できること。
- Automation: 開発フロー（CI/CD や Makefile）に組み込み、常に最新のメトリクスを維持すること。

## Considered Options / 検討

- Option 1: 手動のコードレビューのみで品質を管理する。
- Option 2: Radon, Pydeps, および独自スクリプトを導入し、メトリクス計測と可視化を自動化する。

## Decision Outcome / 決定

Chosen option: Option 2.

以下のツールを導入し、開発フローに統合します。

1. Radon:
   - 循環的複雑度 (Cyclomatic Complexity) と保守性指数 (Maintainability Index) を計測する。
2. Pydeps:
   - モジュール間のインポート依存関係を可視化するグラフ（SVG）を生成する。
3. Custom Stability Analyzer:
   - 不安定度 ($I$) を算出する独自スクリプトを `tools/` に配置する。
   - Graphviz を用いて、安定度に基づいた色分け（青＝安定、赤＝不安定）を行ったアーキテクチャ図を生成する。

## Consequences / 決定

- Positive:
  - リファクタリングの優先順位を数値に基づいて決定できる。
  - 生成された図により、新規参画者のオンボーディングが高速化される。
  - 設計原則（SDP）違反を視覚的に検知できる。
- Negative:
  - 実行環境に `graphviz` (システムパッケージ) のインストールが必須となる。

# Code Visualization and Quality Metrics Strategy ## Context and Problem Statement / コンテキスト プロジェクトの規模拡大に伴い、以下の課題が発生しています： 1. **構造の把握困難**: モジュール間の依存関係が複雑化し、全体像を掴みにくい。 2. **品質の定量化**: コードの複雑さが主観で語られており、客観的なリファクタリング基準がない。 3. **アーキテクチャの健全性**: 「安定依存の原則（SDP）」が守られているか（不安定なモジュールが安定したモジュールに依存していないか）を確認する手段がない。 ## Decision Drivers / 要求 * **Objectivity**: リファクタリングの優先順位を、数値に基づく客観的な基準で決定すること。 * **Visibility**: モジュール間の依存関係を視覚化し、設計上の「逆流」を容易に発見できること。 * **Automation**: 開発フロー（CI/CD や Makefile）に組み込み、常に最新のメトリクスを維持すること。 ## Considered Options / 検討 * **Option 1**: 手動のコードレビューのみで品質を管理する。 * **Option 2**: Radon, Pydeps, および独自スクリプトを導入し、メトリクス計測と可視化を自動化する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 以下のツールを導入し、開発フローに統合します。 1. **Radon**: * 循環的複雑度 (Cyclomatic Complexity) と保守性指数 (Maintainability Index) を計測する。 2. **Pydeps**: * モジュール間のインポート依存関係を可視化するグラフ（SVG）を生成する。 3. **Custom Stability Analyzer**: * 不安定度 ($I$) を算出する独自スクリプトを `tools/` に配置する。 * Graphviz を用いて、安定度に基づいた色分け（青＝安定、赤＝不安定）を行ったアーキテクチャ図を生成する。 ## Consequences / 決定 * **Positive**: * リファクタリングの優先順位を数値に基づいて決定できる。 * 生成された図により、新規参画者のオンボーディングが高速化される。 * 設計原則（SDP）違反を視覚的に検知できる。 * **Negative**: * 実行環境に `graphviz` (システムパッケージ) のインストールが必須となる。

親: REQ010

子: —

### ADR022 {h(g)} ✓ レビュー済 局所ビュー →

# Refactoring Complex Modules with Single Dispatch

## Context and Problem Statement / コンテキスト

`quality_report` により、`src/beautyspot/cachekey.py` の `canonicalize` 関数が非常に高い循環的複雑度（ランク D）を持っていることが判明しました。この関数は、ハッシュ化のためのオブジェクト正規化において、`dict`, `list`, `set`, `numpy`, `type` など多岐にわたる型をチェックするために長い `if-elif-else` チェーンを使用していました。

この構造は開放閉鎖の原則 (Open-Closed Principle) に違反しており、新しい型のサポートを追加するたびにコア関数を修正する必要があるため、退行（デグレード）のリスクを高めていました。

## Decision Drivers / 要求

- Maintainability: 高い複雑度を解消し、各型の処理ロジックを独立させること。
- Extensibility: 既存のコードを変更することなく、新しい型への対応を容易にすること。
- Readability: 1つの関数が負うべき責任を明確にし、コードの可読性を向上させること。

## Considered Options / 検討

- Option 1: 現状の巨大な `if-elif` チェーンを維持する。
- Option 2: Python 標準ライブラリの `functools.singledispatch` を使用してリファクタリングする。

## Decision Outcome / 決定

Chosen option: Option 2.

`canonicalize` 関数を `functools.singledispatch` を用いて刷新します。

- Default Dispatch: プリミティブ型、フォールバックロジック (`str`)、およびダックタイピングによるチェック（Numpy 配列や `__dict__` を持つオブジェクト等）を処理する。
- Registered Handlers: `dict`, `list`, `tuple`, `set`, `frozenset`, `type` 等の特定のロジックは、装飾された独立した関数に移動する。

## Consequences / 決定

- Positive:
  - 複雑度の低減: メイン関数のロジックが、焦点を絞った小さなハンドラに分割される。
  - 拡張性: 既存コードを変更せずに、新しいハンドラを登録するだけで将来の型をサポートできる。
  - 可読性: 各ハンドラが単一の型に対する責任に集中できる。
- Negative:
  - ダックタイピングの制限: `singledispatch` は厳密な型に基づいているため、Numpy のようなダックタイピングによるチェックは依然としてデフォルトハンドラや基底層で行う必要がある。

# Refactoring Complex Modules with Single Dispatch ## Context and Problem Statement / コンテキスト `quality_report` により、`src/beautyspot/cachekey.py` の `canonicalize` 関数が非常に高い循環的複雑度（ランク D）を持っていることが判明しました。この関数は、ハッシュ化のためのオブジェクト正規化において、`dict`, `list`, `set`, `numpy`, `type` など多岐にわたる型をチェックするために長い `if-elif-else` チェーンを使用していました。 この構造は開放閉鎖の原則 (Open-Closed Principle) に違反しており、新しい型のサポートを追加するたびにコア関数を修正する必要があるため、退行（デグレード）のリスクを高めていました。 ## Decision Drivers / 要求 * **Maintainability**: 高い複雑度を解消し、各型の処理ロジックを独立させること。 * **Extensibility**: 既存のコードを変更することなく、新しい型への対応を容易にすること。 * **Readability**: 1つの関数が負うべき責任を明確にし、コードの可読性を向上させること。 ## Considered Options / 検討 * **Option 1**: 現状の巨大な `if-elif` チェーンを維持する。 * **Option 2**: Python 標準ライブラリの `functools.singledispatch` を使用してリファクタリングする。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `canonicalize` 関数を `functools.singledispatch` を用いて刷新します。 * **Default Dispatch**: プリミティブ型、フォールバックロジック (`str`)、およびダックタイピングによるチェック（Numpy 配列や `__dict__` を持つオブジェクト等）を処理する。 * **Registered Handlers**: `dict`, `list`, `tuple`, `set`, `frozenset`, `type` 等の特定のロジックは、装飾された独立した関数に移動する。 ## Consequences / 決定 * **Positive**: * **複雑度の低減**: メイン関数のロジックが、焦点を絞った小さなハンドラに分割される。 * **拡張性**: 既存コードを変更せずに、新しいハンドラを登録するだけで将来の型をサポートできる。 * **可読性**: 各ハンドラが単一の型に対する責任に集中できる。 * **Negative**: * **ダックタイピングの制限**: `singledispatch` は厳密な型に基づいているため、Numpy のようなダックタイピングによるチェックは依然としてデフォルトハンドラや基底層で行う必要がある。

親: REQ002

子: —

### ADR023 {h(g)} ✓ レビュー済 局所ビュー →

# Service Layer for Maintenance Operations

## Context and Problem Statement / コンテキスト

現在、`src/beautyspot/cli.py` には以下のビジネスロジックが直接記述されています： 1. Pruning: タイムスタンプに基づく古いタスクの削除。 2. Cleaning: 孤立した Blob ファイルの特定と削除（ガベージコレクション）。

これらのロジックは `sqlite3` ドライバに直接アクセスしたり、ファイルパスを操作したりしており、`TaskDB` や `BlobStorageBase` の抽象化をバイパスしています。これにより、CLI が SQLite やローカルファイルストレージの詳細に密結合し、他のインターフェース（スクリプトや Web UI）から同じロジックを再利用できないという問題が発生しています。また、S3 などの外部ストレージに対する「掃除」機能の実装も困難になっています。

## Decision Drivers / 要求

- Decoupling: CLI を特定のデータベース実装やファイルシステム詳細から切り離すこと。
- Reusability: メンテナンスロジック（整理や掃除）をプログラムから、あるいは Dashboard からも利用可能にすること。
- Abstraction Support: S3 などの異なるストレージバックエンドでも、一貫したガベージコレクションが行えるようにすること。

## Considered Options / 検討

- Option 1: メンテナンスロジックを CLI に残したままにする。
- Option 2: 全てのロジックを `Spot` クラスに追加する。
- Option 3: 専用のサービスレイヤー `MaintenanceService` を新設し、管理ロジックを分離する。

## Decision Outcome / 決定

Chosen option: Option 3.

メンテナンスロジックを `cli.py` から抽出し、専用の `MaintenanceService` (`src/beautyspot/maintenance.py`) に移動します。

`Spot` クラスはアプリケーションの実行コンテキストとキャッシュ制御に集中させ、メンテナンス（行政的タスク）は別の関心事として分離します。

- `MaintenanceService`: レコードの整理、孤立した Blob の掃除、履歴の照会などを担当。`TaskDB` と `BlobStorageBase` を調整（Orchestrate）する。
- `Spot`: 実行時の登録 (`mark`) と実行 (`run`) に専念する。

## Consequences / 決定

- Positive:
  - 関心の分離: 実行時ロジック (`Spot`) を軽量に保ちつつ、管理ロジックを独立して進化させることができる。
  - テスト容易性: CLI を起動することなく、メンテナンスロジックのユニットテストが可能になる。
  - 再利用性: 整理や掃除を他のスクリプトや Dashboard からトリガーできる。
  - 抽象化の恩恵: バックエンドが `list_keys` 等を実装していれば、S3 等でも自動的に GC がサポートされる。
- Negative:
  - インターフェースの拡張: メンテナンス支援のため、`TaskDB` や `BlobStorageBase` に新たなメソッド（`list_keys`, `get_blob_refs` 等）を追加する必要がある。

# Service Layer for Maintenance Operations ## Context and Problem Statement / コンテキスト 現在、`src/beautyspot/cli.py` には以下のビジネスロジックが直接記述されています： 1. **Pruning**: タイムスタンプに基づく古いタスクの削除。 2. **Cleaning**: 孤立した Blob ファイルの特定と削除（ガベージコレクション）。 これらのロジックは `sqlite3` ドライバに直接アクセスしたり、ファイルパスを操作したりしており、`TaskDB` や `BlobStorageBase` の抽象化をバイパスしています。これにより、CLI が SQLite やローカルファイルストレージの詳細に密結合し、他のインターフェース（スクリプトや Web UI）から同じロジックを再利用できないという問題が発生しています。また、S3 などの外部ストレージに対する「掃除」機能の実装も困難になっています。 ## Decision Drivers / 要求 * **Decoupling**: CLI を特定のデータベース実装やファイルシステム詳細から切り離すこと。 * **Reusability**: メンテナンスロジック（整理や掃除）をプログラムから、あるいは Dashboard からも利用可能にすること。 * **Abstraction Support**: S3 などの異なるストレージバックエンドでも、一貫したガベージコレクションが行えるようにすること。 ## Considered Options / 検討 * **Option 1**: メンテナンスロジックを CLI に残したままにする。 * **Option 2**: 全てのロジックを `Spot` クラスに追加する。 * **Option 3**: 専用のサービスレイヤー `MaintenanceService` を新設し、管理ロジックを分離する。 ## Decision Outcome / 決定 Chosen option: **Option 3**. メンテナンスロジックを `cli.py` から抽出し、専用の **`MaintenanceService`** (`src/beautyspot/maintenance.py`) に移動します。 `Spot` クラスはアプリケーションの実行コンテキストとキャッシュ制御に集中させ、メンテナンス（行政的タスク）は別の関心事として分離します。 * **`MaintenanceService`**: レコードの整理、孤立した Blob の掃除、履歴の照会などを担当。`TaskDB` と `BlobStorageBase` を調整（Orchestrate）する。 * **`Spot`**: 実行時の登録 (`mark`) と実行 (`run`) に専念する。 ## Consequences / 決定 * **Positive**: * **関心の分離**: 実行時ロジック (`Spot`) を軽量に保ちつつ、管理ロジックを独立して進化させることができる。 * **テスト容易性**: CLI を起動することなく、メンテナンスロジックのユニットテストが可能になる。 * **再利用性**: 整理や掃除を他のスクリプトや Dashboard からトリガーできる。 * **抽象化の恩恵**: バックエンドが `list_keys` 等を実装していれば、S3 等でも自動的に GC がサポートされる。 * **Negative**: * **インターフェースの拡張**: メンテナンス支援のため、`TaskDB` や `BlobStorageBase` に新たなメソッド（`list_keys`, `get_blob_refs` 等）を追加する必要がある。

親: REQ010

子: —

### ADR024 {h(g)} ✓ レビュー済 局所ビュー →

# Non-blocking Cache Persistence and Task Tracking

## Context and Problem Statement / コンテキスト

大規模なデータのシリアライズや、S3 等のリモートストレージへの保存処理は、ユーザーのメインロジックの実行を妨げる（ブロッキング）要因となっていました。「インフラとしてのキャッシュ処理がユーザーを待たせない」という設計思想を強化する必要があります。

## Decision Drivers / 要求

- Latency Reduction: キャッシュの保存処理（特に入出力の重い処理）が、メインスレッドの実行速度を低下させないこと。
- Data Integrity: バックグラウンド保存であっても、プログラムの終了前には全ての保存が完了していることを保証し、データロストを防ぐこと。
- API Reusability: `Spot` インスタンスを何度も再利用でき、かつリソースの適切なクリーンアップ（フラッシュ）が行えること。

## Considered Options / 検討

- Option 1: 全ての保存処理をメインスレッドで同期的に行う。
- Option 2: 保存処理をバックグラウンド（Fire-and-Forget）で実行し、タスク追跡と `with` ブロックによる自動フラッシュの仕組みを導入する。

## Decision Outcome / 決定

Chosen option: Option 2.

キャッシュの保存処理をバックグラウンドで実行し、かつリソースの整合性を保つためのライフサイクル管理を導入します。

1. `wait=False` モードの導入: `@spot.mark` や `cached_run` に `wait` オプションを追加し、保存完了を待たずにメインロジックに結果を返すことを可能にします。
2. タスク追跡メカニズム: 実行中のバックグラウンドタスク（Future）を内部のセットで追跡します。
3. Flush (非破壊的待機) の導入: `with spot:` ブロックを抜ける際に、全てのバックグラウンドタスクの完了を待機する（Flush）処理を実行します。
4. 再利用性の確保: コンテキストを抜けても Executor は即座にシャットダウンせず、`Spot` インスタンスを繰り返し安全に利用可能にします。

## Consequences / 決定

- Positive:
  - レイテンシの改善: 保存処理がバックグラウンド化され、ユーザーの体感速度が向上する。
  - データ整合性の保証: `with` ブロックやプロセス終了時に未完了タスクを必ず待機するため、データの整合性が保たれる。
  - 直感的な API: グローバルに定義したオブジェクトを、何度も安全に「フラッシュ」しながら使い回せる。
- Negative:
  - 例外ハンドリングの複雑化: バックグラウンドで発生したエラーは即座に呼び出し元へ伝播しないため、ログによる通知に依存することになる。

# Non-blocking Cache Persistence and Task Tracking ## Context and Problem Statement / コンテキスト 大規模なデータのシリアライズや、S3 等のリモートストレージへの保存処理は、ユーザーのメインロジックの実行を妨げる（ブロッキング）要因となっていました。「インフラとしてのキャッシュ処理がユーザーを待たせない」という設計思想を強化する必要があります。 ## Decision Drivers / 要求 * **Latency Reduction**: キャッシュの保存処理（特に入出力の重い処理）が、メインスレッドの実行速度を低下させないこと。 * **Data Integrity**: バックグラウンド保存であっても、プログラムの終了前には全ての保存が完了していることを保証し、データロストを防ぐこと。 * **API Reusability**: `Spot` インスタンスを何度も再利用でき、かつリソースの適切なクリーンアップ（フラッシュ）が行えること。 ## Considered Options / 検討 * **Option 1**: 全ての保存処理をメインスレッドで同期的に行う。 * **Option 2**: 保存処理をバックグラウンド（Fire-and-Forget）で実行し、タスク追跡と `with` ブロックによる自動フラッシュの仕組みを導入する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. キャッシュの保存処理をバックグラウンドで実行し、かつリソースの整合性を保つためのライフサイクル管理を導入します。 1. **`wait=False` モードの導入**: `@spot.mark` や `cached_run` に `wait` オプションを追加し、保存完了を待たずにメインロジックに結果を返すことを可能にします。 2. **タスク追跡メカニズム**: 実行中のバックグラウンドタスク（Future）を内部のセットで追跡します。 3. **Flush (非破壊的待機) の導入**: `with spot:` ブロックを抜ける際に、全てのバックグラウンドタスクの完了を待機する（Flush）処理を実行します。 4. **再利用性の確保**: コンテキストを抜けても Executor は即座にシャットダウンせず、`Spot` インスタンスを繰り返し安全に利用可能にします。 ## Consequences / 決定 * **Positive**: * **レイテンシの改善**: 保存処理がバックグラウンド化され、ユーザーの体感速度が向上する。 * **データ整合性の保証**: `with` ブロックやプロセス終了時に未完了タスクを必ず待機するため、データの整合性が保たれる。 * **直感的な API**: グローバルに定義したオブジェクトを、何度も安全に「フラッシュ」しながら使い回せる。 * **Negative**: * **例外ハンドリングの複雑化**: バックグラウンドで発生したエラーは即座に呼び出し元へ伝播しないため、ログによる通知に依存することになる。

親: REQ006

子: —

### ADR025 {h(g)} ✓ レビュー済 局所ビュー →

# Protocol-based Serializer Registration

## Context and Problem Statement / コンテキスト

`beautyspot` では、`@spot.register` デコレータを使用してカスタム型を登録できます。 当初の実装では、`Spot` クラス内でシリアライザが `MsgpackSerializer` のインスタンスであるかどうかを明示的にチェックしていました。

    if isinstance(self.serializer, MsgpackSerializer):
        self.serializer.register(...)

これは、高レベルの `Spot` クラスを特定の具象クラス (`MsgpackSerializer`) に結合させており、依存関係逆転の原則 (Dependency Inversion Principle) に違反していました。その結果、カスタム型登録をサポートする他のシリアライザ（将来的な `JsonSerializer` やカスタムラッパーなど）を使用することが不可能でした。

## Decision Drivers / 要求

- Decoupling: `Spot` クラスを特定のライブラリ（`msgpack`）や実装から切り離すこと。
- Extensibility: ユーザーが独自のシリアライザを実装し、かつカスタム型登録機能も維持できるようにすること。
- Interface Segregation: 基本的なシリアライズ機能と、型登録機能という異なる責務を明確に分離すること。

## Considered Options / 検討

- Option 1: 具象クラスに対する `isinstance` チェックを維持する。
- Option 2: 構造的部分型（Protocol）を導入し、インターフェースに基づくチェックに切り替える。

## Decision Outcome / 決定

Chosen option: Option 2.

`beautyspot.serializer` に `TypeRegistryProtocol` を導入します。

1. Define Protocol: 型登録に必要な `register` メソッドのシグネチャを規定するプロトコルを定義します。
2. Decoupling: `Spot` クラスは、具象クラスではなくこのプロトコルに対して適合性チェックを行うように変更します。
3. Interface Segregation: 基本的な `SerializerProtocol` (dump/load) と `TypeRegistryProtocol` を分離し、型登録をサポートしないシリアライザも許容できるようにします。

## Consequences / 決定

- Positive:
  - コアロジックが `msgpack` から解放され、`TypeRegistryProtocol` に従う任意のシリアライザを使用可能になった。
  - モックを使用したテストが容易になった。
- Negative:
  - シリアライザの実装者は、プロトコルで定義された `register` メソッドのシグネチャを正確に遵守する必要がある。

# Protocol-based Serializer Registration ## Context and Problem Statement / コンテキスト `beautyspot` では、`@spot.register` デコレータを使用してカスタム型を登録できます。 当初の実装では、`Spot` クラス内でシリアライザが `MsgpackSerializer` のインスタンスであるかどうかを明示的にチェックしていました。 ```python if isinstance(self.serializer, MsgpackSerializer): self.serializer.register(...) ``` これは、高レベルの `Spot` クラスを特定の具象クラス (`MsgpackSerializer`) に結合させており、依存関係逆転の原則 (Dependency Inversion Principle) に違反していました。その結果、カスタム型登録をサポートする他のシリアライザ（将来的な `JsonSerializer` やカスタムラッパーなど）を使用することが不可能でした。 ## Decision Drivers / 要求 * **Decoupling**: `Spot` クラスを特定のライブラリ（`msgpack`）や実装から切り離すこと。 * **Extensibility**: ユーザーが独自のシリアライザを実装し、かつカスタム型登録機能も維持できるようにすること。 * **Interface Segregation**: 基本的なシリアライズ機能と、型登録機能という異なる責務を明確に分離すること。 ## Considered Options / 検討 * **Option 1**: 具象クラスに対する `isinstance` チェックを維持する。 * **Option 2**: 構造的部分型（Protocol）を導入し、インターフェースに基づくチェックに切り替える。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `beautyspot.serializer` に **`TypeRegistryProtocol`** を導入します。 1. **Define Protocol**: 型登録に必要な `register` メソッドのシグネチャを規定するプロトコルを定義します。 2. **Decoupling**: `Spot` クラスは、具象クラスではなくこのプロトコルに対して適合性チェックを行うように変更します。 3. **Interface Segregation**: 基本的な `SerializerProtocol` (dump/load) と `TypeRegistryProtocol` を分離し、型登録をサポートしないシリアライザも許容できるようにします。 ## Consequences / 決定 * **Positive**: * コアロジックが `msgpack` から解放され、`TypeRegistryProtocol` に従う任意のシリアライザを使用可能になった。 * モックを使用したテストが容易になった。 * **Negative**: * シリアライザの実装者は、プロトコルで定義された `register` メソッドのシグネチャを正確に遵守する必要がある。

親: REQ005

子: —

### ADR026 {h(g)} ✓ レビュー済 局所ビュー →

# Factory Function for Default Dependency Injection

## Context and Problem Statement / コンテキスト

v2.0 以降、`beautyspot` は依存性注入 (DI) アーキテクチャを採用しました。`Spot` クラス（`core.py`）の初期化には、`TaskDB`, `Serializer`, `Storage` の各インスタンスを明示的に渡す必要があります。

これはテスト容易性と柔軟性の観点では優れていますが、単に「すぐに使い始めたい」だけのユーザーにとっては、初期化が非常に冗長になってしまうという課題がありました。

    # 一般的なスクリプトには冗長すぎる
    db = SQLiteTaskDB(...)
    storage = LocalStorage(...)
    serializer = MsgpackSerializer()
    spot = Spot(name="app", db=db, storage=storage, serializer=serializer)

## Decision Drivers / 要求

- Developer Experience (DX): ライトユーザーが最小限の設定（名前のみなど）で即座に開始できること。
- Clean Architecture: `core.py` の `Spot` クラスを具象クラス（SQLite 等）の詳細から保護し、ピュアな状態に保つこと。
- Flexibility: 高度なユーザーが引き続き任意のコンポーネントを注入できる余地を残すこと。

## Considered Options / 検討

- Option 1: ユーザーに常に全ての DI コンポーネントを手動で提供させる。
- Option 2: `Spot.__init__` の引数にデフォルト値（具象クラス）を設定する。
- Option 3: `__init__.py` でファクトリ関数 `Spot` を提供し、そこでデフォルトの組み立てを行う。

## Decision Outcome / 決定

Chosen option: Option 3.

`beautyspot/__init__.py` において、具象クラスのインスタンス化を伴うファクトリ関数 `Spot` を公開します。

    # beautyspot/__init__.py
    def Spot(name: str, db=None, ...):
        resolved_db = db or SQLiteTaskDB(...)
        # ... 他の解決ロジック ...
        return _Spot(name, db=resolved_db, ...)

これにより、ライブラリのトップレベルからインポートされる `Spot` は関数となり、内部で `core.py` の `_Spot` クラスを組み立てて返します。

## Consequences / 決定

- Positive:
  - "Opinionated Defaults, Flexible Internals": 初心者は1行で初期化でき、上級者はカスタムコンポーネントを注入できる。
  - コアとなる `_Spot` クラスが、`SQLiteTaskDB` などの具象実装をインポートする必要がなくなる（関心の分離）。
- Negative:
  - 公開シンボル `Spot` がクラスではなく関数になるため、`Spot` を直接継承することが難しくなる。継承が必要な場合は `core` から `_Spot` をインポートする必要があるが、本ライブラリでは継承よりもコンポジションを推奨するため、許容範囲とする。

# Factory Function for Default Dependency Injection ## Context and Problem Statement / コンテキスト v2.0 以降、`beautyspot` は依存性注入 (DI) アーキテクチャを採用しました。`Spot` クラス（`core.py`）の初期化には、`TaskDB`, `Serializer`, `Storage` の各インスタンスを明示的に渡す必要があります。 これはテスト容易性と柔軟性の観点では優れていますが、単に「すぐに使い始めたい」だけのユーザーにとっては、初期化が非常に冗長になってしまうという課題がありました。 ```python # 一般的なスクリプトには冗長すぎる db = SQLiteTaskDB(...) storage = LocalStorage(...) serializer = MsgpackSerializer() spot = Spot(name="app", db=db, storage=storage, serializer=serializer) ``` ## Decision Drivers / 要求 * **Developer Experience (DX)**: ライトユーザーが最小限の設定（名前のみなど）で即座に開始できること。 * **Clean Architecture**: `core.py` の `Spot` クラスを具象クラス（SQLite 等）の詳細から保護し、ピュアな状態に保つこと。 * **Flexibility**: 高度なユーザーが引き続き任意のコンポーネントを注入できる余地を残すこと。 ## Considered Options / 検討 * **Option 1**: ユーザーに常に全ての DI コンポーネントを手動で提供させる。 * **Option 2**: `Spot.__init__` の引数にデフォルト値（具象クラス）を設定する。 * **Option 3**: `__init__.py` でファクトリ関数 `Spot` を提供し、そこでデフォルトの組み立てを行う。 ## Decision Outcome / 決定 Chosen option: **Option 3**. `beautyspot/__init__.py` において、具象クラスのインスタンス化を伴う**ファクトリ関数 `Spot`** を公開します。 ```python # beautyspot/__init__.py def Spot(name: str, db=None, ...): resolved_db = db or SQLiteTaskDB(...) # ... 他の解決ロジック ... return _Spot(name, db=resolved_db, ...) ``` これにより、ライブラリのトップレベルからインポートされる `Spot` は関数となり、内部で `core.py` の `_Spot` クラスを組み立てて返します。 ## Consequences / 決定 * **Positive**: * **"Opinionated Defaults, Flexible Internals"**: 初心者は1行で初期化でき、上級者はカスタムコンポーネントを注入できる。 * コアとなる `_Spot` クラスが、`SQLiteTaskDB` などの具象実装をインポートする必要がなくなる（関心の分離）。 * **Negative**: * 公開シンボル `Spot` がクラスではなく関数になるため、`Spot` を直接継承することが難しくなる。継承が必要な場合は `core` から `_Spot` をインポートする必要があるが、本ライブラリでは継承よりもコンポジションを推奨するため、許容範囲とする。

親: REQ012

子: —

### ADR027 {h(g)} ✓ レビュー済 局所ビュー →

# Pragmatic CLI Refactoring Policy

## Context and Problem Statement / コンテキスト

`quality_report.md` において、`src/beautyspot/cli.py` 内の複数の関数（`show_cmd`, `prune_cmd`, `stats_cmd`）が循環的複雑度（CC）で Rank C の警告を受けています。一般的にプロジェクトでは CC の低減を推奨していますが、CLI モジュールに関してはその優先度と費用対効果を再検討する必要があります。

## Decision Drivers / 要求

- Efficiency: リファクタリングのリソースを、ユーザーに直接影響するコアロジックに優先的に配分すること。
- Readability: CLI コマンドの「上から下への命令的フロー」を維持し、挙動の追いやすさを損なわないこと。
- Architectural Impact: 依存関係の末端にある CLI モジュールの複雑さが、システム全体の安定性に与える影響を考慮すること。

## Considered Options / 検討

- Option 1: 厳格なリファクタリング。すべての Rank C 関数を Rank A/B になるまで分解する。
- Option 2: 戦略的許容。表示ロジックに起因する複雑さは許容し、現状の構造を維持する。

## Decision Outcome / 決定

Chosen option: Option 2.

CLI モジュールの Rank C スコアについては、以下の理由から「即時の抜本的なリファクタリングは行わず、現状の構造を維持する」ことを決定しました。

- 表示ロジックの特性: `cli.py` の複雑さは、データ取得後の表示フォーマット分岐（Rich ライブラリによる出力制御）に集中しており、純粋なビジネスロジックの複雑さとは性質が異なります。
- 設計上の影響の小ささ: `cli.py` は依存関係の末端に位置しており、他モジュールへの波及効果がありません。
- 可読性の維持: CLI コマンドは、過度に抽象化されるよりも命令的に記述されている方が挙動を追いやすい場合があります。

## Consequences / 決定

- Positive:
  - コアロジック（`cachekey.py` など）のリファクタリングにリソースを集中できる。
  - 不要な抽象化による「コードの追いづらさ」を回避できる。
- Negative:
  - 品質レポート上の警告は残り続ける。
  - 将来的にコマンドがさらに肥大化した場合には、改めて分割を検討する必要がある。

# Pragmatic CLI Refactoring Policy ## Context and Problem Statement / コンテキスト `quality_report.md` において、`src/beautyspot/cli.py` 内の複数の関数（`show_cmd`, `prune_cmd`, `stats_cmd`）が循環的複雑度（CC）で **Rank C** の警告を受けています。一般的にプロジェクトでは CC の低減を推奨していますが、CLI モジュールに関してはその優先度と費用対効果を再検討する必要があります。 ## Decision Drivers / 要求 * **Efficiency**: リファクタリングのリソースを、ユーザーに直接影響するコアロジックに優先的に配分すること。 * **Readability**: CLI コマンドの「上から下への命令的フロー」を維持し、挙動の追いやすさを損なわないこと。 * **Architectural Impact**: 依存関係の末端にある CLI モジュールの複雑さが、システム全体の安定性に与える影響を考慮すること。 ## Considered Options / 検討 * **Option 1**: 厳格なリファクタリング。すべての Rank C 関数を Rank A/B になるまで分解する。 * **Option 2**: 戦略的許容。表示ロジックに起因する複雑さは許容し、現状の構造を維持する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. CLI モジュールの Rank C スコアについては、以下の理由から**「即時の抜本的なリファクタリングは行わず、現状の構造を維持する」**ことを決定しました。 * **表示ロジックの特性**: `cli.py` の複雑さは、データ取得後の表示フォーマット分岐（Rich ライブラリによる出力制御）に集中しており、純粋なビジネスロジックの複雑さとは性質が異なります。 * **設計上の影響の小ささ**: `cli.py` は依存関係の末端に位置しており、他モジュールへの波及効果がありません。 * **可読性の維持**: CLI コマンドは、過度に抽象化されるよりも命令的に記述されている方が挙動を追いやすい場合があります。 ## Consequences / 決定 * **Positive**: * コアロジック（`cachekey.py` など）のリファクタリングにリソースを集中できる。 * 不要な抽象化による「コードの追いづらさ」を回避できる。 * **Negative**: * 品質レポート上の警告は残り続ける。 * 将来的にコマンドがさらに肥大化した場合には、改めて分割を検討する必要がある。

親: REQ011

子: —

### ADR028 {h(g)} ✓ レビュー済 局所ビュー →

# CLI Scope Definition and Explicit Storage Linkage

## Context and Problem Statement / コンテキスト

`beautyspot` の `clean` コマンドやガベージコレクション機能は、DBファイルとBlobストレージディレクトリの対応関係が「自明」であることを前提としています。標準構成（`.beautyspot/` 配下）ではこの前提は成立しますが、ユーザーがカスタムパスを設定したり、外部バックエンド（S3等）を使用した場合、CLIツールは安全に依存関係を特定できません。この状態で推測に基づく削除を行うと、誤って無関係なデータを削除するリスクがあります。

## Decision Drivers / 要求

- Data Safety: 管理対象外のデータや、他のプロジェクトが使用しているデータを誤って削除しないこと。
- Predictability: CLI ツールがどの範囲までを安全に管理できるかを明確に定義すること。
- Extensibility: 将来的にカスタム構成であっても安全にメンテナンスが行えるような道筋を立てること。

## Considered Options / 検討

- Option 1: パス構造や設定から対応関係を推測し、ベストエフォートでクリーンアップを試みる。
- Option 2: CLI の自動クリーンアップ機能を標準構成に限定し、カスタム構成についてはユーザー管理とする。あわせてメタデータによるリンクを将来的に導入する。

## Decision Outcome / 決定

Chosen option: Option 2.

### 1. CLI Scope Limitation (Policy)

CLIが提供する「ストレージの自動クリーニング・削除機能」のサポート範囲を、標準ディレクトリ構成（`.beautyspot/` 配下）を使用しているプロジェクト に限定します。カスタム構成を利用している場合は、CLI による自動削除は保証されず、ユーザー自身の責任で管理を行う必要がある旨を明記します。

### 2. Explicit Storage Linkage (Roadmap)

「対応関係が自明でない」問題を根本解決するため、将来のバージョンで TaskDB内に使用しているStorageの情報をメタデータとして記録する 仕組みを導入します。DB初期化時に `storage_uri` を保存し、メンテナンス時に「操作対象のDBが参照しているストレージと、今操作しようとしている実体が一致するか」を検証可能にします。

## Consequences / 決定

- Positive:
  - 安全性: 推測による誤削除を確実に防ぐことができる。
  - 明確な責任分界点: ツールが面倒を見る範囲と、ユーザーが管理すべき範囲が明確になる。
  - 将来の拡張性: メタデータ記録が実装されれば、カスタム構成でも安全なGCが可能になる。
- Negative:
  - ユーザーの負担: カスタム構成を利用する上級ユーザーは、`beautyspot clean` などの恩恵をフルには受けられない場合がある。

# CLI Scope Definition and Explicit Storage Linkage ## Context and Problem Statement / コンテキスト `beautyspot` の `clean` コマンドやガベージコレクション機能は、DBファイルとBlobストレージディレクトリの対応関係が「自明」であることを前提としています。標準構成（`.beautyspot/` 配下）ではこの前提は成立しますが、ユーザーがカスタムパスを設定したり、外部バックエンド（S3等）を使用した場合、CLIツールは安全に依存関係を特定できません。この状態で推測に基づく削除を行うと、誤って無関係なデータを削除するリスクがあります。 ## Decision Drivers / 要求 * **Data Safety**: 管理対象外のデータや、他のプロジェクトが使用しているデータを誤って削除しないこと。 * **Predictability**: CLI ツールがどの範囲までを安全に管理できるかを明確に定義すること。 * **Extensibility**: 将来的にカスタム構成であっても安全にメンテナンスが行えるような道筋を立てること。 ## Considered Options / 検討 * **Option 1**: パス構造や設定から対応関係を推測し、ベストエフォートでクリーンアップを試みる。 * **Option 2**: CLI の自動クリーンアップ機能を標準構成に限定し、カスタム構成についてはユーザー管理とする。あわせてメタデータによるリンクを将来的に導入する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ### 1. CLI Scope Limitation (Policy) CLIが提供する「ストレージの自動クリーニング・削除機能」のサポート範囲を、**標準ディレクトリ構成（`.beautyspot/` 配下）を使用しているプロジェクト** に限定します。カスタム構成を利用している場合は、CLI による自動削除は保証されず、ユーザー自身の責任で管理を行う必要がある旨を明記します。 ### 2. Explicit Storage Linkage (Roadmap) 「対応関係が自明でない」問題を根本解決するため、将来のバージョンで **TaskDB内に使用しているStorageの情報をメタデータとして記録する** 仕組みを導入します。DB初期化時に `storage_uri` を保存し、メンテナンス時に「操作対象のDBが参照しているストレージと、今操作しようとしている実体が一致するか」を検証可能にします。 ## Consequences / 決定 * **Positive**: * **安全性:** 推測による誤削除を確実に防ぐことができる。 * **明確な責任分界点:** ツールが面倒を見る範囲と、ユーザーが管理すべき範囲が明確になる。 * **将来の拡張性:** メタデータ記録が実装されれば、カスタム構成でも安全なGCが可能になる。 * **Negative**: * **ユーザーの負担:** カスタム構成を利用する上級ユーザーは、`beautyspot clean` などの恩恵をフルには受けられない場合がある。

親: REQ011

子: —

### ADR029 {h(g)} ✓ レビュー済 局所ビュー →

# Recursive Storage Cleanup and Zombie Project Collection

## Context and Problem Statement / コンテキスト

これまでの `beautyspot clean` コマンドの実装には、以下の課題がありました。

1. ディレクトリの残留: `clean` コマンドは個別のファイル削除のみを行い、空になったディレクトリを削除していませんでした。
2. 隠しファイルの阻害: macOS の `.DS_Store` などのシステムファイルが存在する場合、ディレクトリが空とみなされず、削除できないケースがありました。
3. ゾンビプロジェクト: ユーザーが手動で DB ファイル (`.db`) のみを削除した場合、対応する Blob ディレクトリが管理外のゴミ（ゾンビプロジェクト）として残り続け、削除する手段がありませんでした。

## Decision Drivers / 要求

- Complete Cleanup: 不要なファイルだけでなく、空のディレクトリ構造も含めて完全に削除し、ファイルシステムをクリーンに保つこと。
- Robustness: システム生成ファイル（`.DS_Store` 等）に惑わされず、意図した通りにクリーンアップを実行できること。
- Garbage Collection: メタデータ（DB）を失った実データ（Blob）を検出し、安全に一括削除できること。

## Considered Options / 検討

- Option 1: ファイル削除のみを継続し、ディレクトリの整理はユーザーに任せる。
- Option 2: 2段階のクリーンアップ戦略（ファイル削除 ＋ 再帰的な空ディレクトリ整理）と、ゾンビプロジェクト回収用の `gc` コマンドを導入する。

## Decision Outcome / 決定

Chosen option: Option 2.

ストレージのクリーンアップ戦略を以下のように刷新します。

### 1. Two-Phase Cleanup Strategy (clean command)

`clean` コマンドを2段階に分割します。 * Phase 1 (File Deletion): DB参照のない孤立ファイルを削除。 * Phase 2 (Directory Pruning): `LocalStorage` に `prune_empty_dirs()` メソッドを追加し、再帰的に空ディレクトリを一括削除。

### 2. Robust Directory Pruning

システム生成ファイル（`.DS_Store`, `Thumbs.db`, `desktop.ini`）のみが存在する場合、それらを「実質的な空」とみなし、強制的に削除した上で親ディレクトリを削除します。

### 3. Zombie Project Garbage Collection (gc command)

DBファイルが失われたプロジェクトを回収するための `gc` コマンドを実装します。対応する `.db` ファイルが存在しないディレクトリを「ゾンビプロジェクト」と判定し、`shutil.rmtree` で強制的に一括削除します。

## Consequences / 決定

- Positive:
  - 完全なクリーンアップ: ユーザーはコマンド一つで不要なディレクトリ構造まで一掃できる。
  - 運用性の向上: 手動操作で発生した残骸を簡単に解消できる。
  - 責務の分離: ファイル削除とディレクトリ整理を分離し、OS 固有の処理を `Storage` クラス内に隠蔽できた。
- Negative:
  - IOオーバーヘッド: `os.walk` による再帰探索を行うため、ファイル数が膨大な場合にオーバーヘッドが発生する。

# Recursive Storage Cleanup and Zombie Project Collection ## Context and Problem Statement / コンテキスト これまでの `beautyspot clean` コマンドの実装には、以下の課題がありました。 1. **ディレクトリの残留:** `clean` コマンドは個別のファイル削除のみを行い、空になったディレクトリを削除していませんでした。 2. **隠しファイルの阻害:** macOS の `.DS_Store` などのシステムファイルが存在する場合、ディレクトリが空とみなされず、削除できないケースがありました。 3. **ゾンビプロジェクト:** ユーザーが手動で DB ファイル (`.db`) のみを削除した場合、対応する Blob ディレクトリが管理外のゴミ（ゾンビプロジェクト）として残り続け、削除する手段がありませんでした。 ## Decision Drivers / 要求 * **Complete Cleanup**: 不要なファイルだけでなく、空のディレクトリ構造も含めて完全に削除し、ファイルシステムをクリーンに保つこと。 * **Robustness**: システム生成ファイル（`.DS_Store` 等）に惑わされず、意図した通りにクリーンアップを実行できること。 * **Garbage Collection**: メタデータ（DB）を失った実データ（Blob）を検出し、安全に一括削除できること。 ## Considered Options / 検討 * **Option 1**: ファイル削除のみを継続し、ディレクトリの整理はユーザーに任せる。 * **Option 2**: 2段階のクリーンアップ戦略（ファイル削除 ＋ 再帰的な空ディレクトリ整理）と、ゾンビプロジェクト回収用の `gc` コマンドを導入する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ストレージのクリーンアップ戦略を以下のように刷新します。 ### 1. Two-Phase Cleanup Strategy (clean command) `clean` コマンドを2段階に分割します。 * **Phase 1 (File Deletion):** DB参照のない孤立ファイルを削除。 * **Phase 2 (Directory Pruning):** `LocalStorage` に `prune_empty_dirs()` メソッドを追加し、再帰的に空ディレクトリを一括削除。 ### 2. Robust Directory Pruning システム生成ファイル（`.DS_Store`, `Thumbs.db`, `desktop.ini`）のみが存在する場合、それらを「実質的な空」とみなし、強制的に削除した上で親ディレクトリを削除します。 ### 3. Zombie Project Garbage Collection (gc command) DBファイルが失われたプロジェクトを回収するための `gc` コマンドを実装します。対応する `.db` ファイルが存在しないディレクトリを「ゾンビプロジェクト」と判定し、`shutil.rmtree` で強制的に一括削除します。 ## Consequences / 決定 * **Positive**: * **完全なクリーンアップ:** ユーザーはコマンド一つで不要なディレクトリ構造まで一掃できる。 * **運用性の向上:** 手動操作で発生した残骸を簡単に解消できる。 * **責務の分離:** ファイル削除とディレクトリ整理を分離し、OS 固有の処理を `Storage` クラス内に隠蔽できた。 * **Negative**: * **IOオーバーヘッド:** `os.walk` による再帰探索を行うため、ファイル数が膨大な場合にオーバーヘッドが発生する。

親: REQ010

子: —

### ADR030 {h(g)} ✓ レビュー済 局所ビュー →

# Declarative Storage Policy

## Context and Problem Statement / コンテキスト

これまでの `beautyspot` では、関数の実行結果を Blob ストレージ（ファイル）に保存するか、DB のレコードに直接埋め込むかは、ユーザーが `save_blob=True` フラグで明示的に指定する必要がありました。 また、データサイズが大きい場合に警告を出す機能はありましたが、自動的に対処する機能はありませんでした。これにより、ユーザーはデータのサイズを予測してフラグを管理するという負担を強いられていました。

## Decision Drivers / 要求

- Automation: データサイズに応じて最適な保存先（DB または Blob）を自動的に選択し、ユーザーの負担を軽減すること。
- Declarative Configuration: 「1MB以上はファイルに保存する」といったルールを宣言的に記述できるようにすること。
- Separation of Concerns: キャッシュキー生成（Identity）と保存方式（Persistence）の責務を明確に分離すること。

## Considered Options / 検討

- Option 1: 引き続きユーザーによる手動の `save_blob` フラグ管理に任せる。
- Option 2: 宣言的な `StoragePolicy` プロトコルを導入し、データ内容に基づいて自動的に保存方式を決定する仕組みを構築する。

## Decision Outcome / 決定

Chosen option: Option 2.

ストレージ保存方式の決定ロジックを抽象化した `StoragePolicy` プロトコルを導入します。

1. StoragePolicy Interface:

   - `should_save_as_blob(data: bytes) -> bool` を持つプロトコルを定義します。
   - 配置場所は `src/beautyspot/storage.py` とし、ストレージ関連の責務を凝集させます。

2. Standard Implementations:

   - `ThresholdStoragePolicy`: 指定したバイト数を超えた場合に Blob 保存を選択する（推奨デフォルト）。
   - `WarningOnlyPolicy`: 従来動作互換。Blob 化はせず、閾値超えでログ警告のみ行う。

3. Precedence (優先順位):

   - 優先度1: 関数ごとの明示的指定 (`@mark(save_blob=...)`)。
   - 優先度2: ポリシーによる自動判定。

## Consequences / 決定

- Positive:
  - ユーザーは個々の関数のデータサイズを気にせず、全体的なルールを定義するだけで最適化が可能になる。
  - DB の肥大化を自動的に防げる。
  - 責務の分離により、モジュールの保守性が向上する。
- Negative:
  - シリアライズ後のデータサイズを見てから判定するため、メモリ上に全データを一時的に展開する必要がある。
  - `Spot` クラスのコンストラクタ引数が増え、旧引数（`blob_warning_threshold` 等）との互換性管理が必要になる。

# Declarative Storage Policy ## Context and Problem Statement / コンテキスト これまでの `beautyspot` では、関数の実行結果を Blob ストレージ（ファイル）に保存するか、DB のレコードに直接埋め込むかは、ユーザーが `save_blob=True` フラグで明示的に指定する必要がありました。 また、データサイズが大きい場合に警告を出す機能はありましたが、自動的に対処する機能はありませんでした。これにより、ユーザーはデータのサイズを予測してフラグを管理するという負担を強いられていました。 ## Decision Drivers / 要求 * **Automation**: データサイズに応じて最適な保存先（DB または Blob）を自動的に選択し、ユーザーの負担を軽減すること。 * **Declarative Configuration**: 「1MB以上はファイルに保存する」といったルールを宣言的に記述できるようにすること。 * **Separation of Concerns**: キャッシュキー生成（Identity）と保存方式（Persistence）の責務を明確に分離すること。 ## Considered Options / 検討 * **Option 1**: 引き続きユーザーによる手動の `save_blob` フラグ管理に任せる。 * **Option 2**: 宣言的な `StoragePolicy` プロトコルを導入し、データ内容に基づいて自動的に保存方式を決定する仕組みを構築する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ストレージ保存方式の決定ロジックを抽象化した `StoragePolicy` プロトコルを導入します。 1. **StoragePolicy Interface**: * `should_save_as_blob(data: bytes) -> bool` を持つプロトコルを定義します。 * 配置場所は `src/beautyspot/storage.py` とし、ストレージ関連の責務を凝集させます。 2. **Standard Implementations**: * `ThresholdStoragePolicy`: 指定したバイト数を超えた場合に Blob 保存を選択する（推奨デフォルト）。 * `WarningOnlyPolicy`: 従来動作互換。Blob 化はせず、閾値超えでログ警告のみ行う。 3. **Precedence (優先順位)**: * 優先度1: 関数ごとの明示的指定 (`@mark(save_blob=...)`)。 * 優先度2: ポリシーによる自動判定。 ## Consequences / 決定 * **Positive**: * ユーザーは個々の関数のデータサイズを気にせず、全体的なルールを定義するだけで最適化が可能になる。 * DB の肥大化を自動的に防げる。 * 責務の分離により、モジュールの保守性が向上する。 * **Negative**: * シリアライズ後のデータサイズを見てから判定するため、メモリ上に全データを一時的に展開する必要がある。 * `Spot` クラスのコンストラクタ引数が増え、旧引数（`blob_warning_threshold` 等）との互換性管理が必要になる。

親: REQ004

子: —

### ADR031 {h(g)} ✓ レビュー済 局所ビュー →

# Limiter Dependency Injection

## Context and Problem Statement / コンテキスト

以前、`core.py` の `Spot` クラスは `TokenBucket` 実装と密結合していました。`Spot` は `tpm` (tokens per minute) 整数引数を受け取り、内部で `TokenBucket` をインスタンス化していました。

この設計にはいくつかの制限がありました： 1. Testing: ユニットテストが `TokenBucket` 内の本物の `time.sleep` 呼び出しに依存するため、実行が遅くなっていました。 2. Extensibility: ユーザーがカスタムレートリミッター（例：Redisベースの分散リミッター）や異なるアルゴリズムを提供できませんでした。 3. Separation of Concerns: `core.Spot` がリミッターのライフサイクルと設定を管理しており、単一責任の原則に違反していました。

## Decision Drivers / 要求

- Testability: テスト中にスリープ時間を排除するために、`MockLimiter` や `NoOpLimiter` を注入できるようにすること。
- Flexibility: コアロジックを修正することなく、ユーザーがカスタムリミッターを実装できるようにすること。
- Clean Core: `core.Spot` を簡素化し、リミッターの初期化処理を排除すること。

## Considered Options / 検討

- Option 1: 引き続き `Spot` クラス内でリミッターの実装をハードコードする。
- Option 2: 抽象プロトコル `LimiterProtocol` を導入し、依存性注入 (DI) を使用してリミッターを外部から提供する。

## Decision Outcome / 決定

Chosen option: Option 2.

レートリミッターを `core.Spot` から切り離し、依存性注入 (DI) を使用します。

1. Protocol Definition: `beautyspot.limiter` に `consume(cost: int)` と `consume_async(cost: int)` メソッドを規定する `LimiterProtocol` を定義します。
2. Explicit Inheritance: デフォルトの `TokenBucket` 実装は、型安全性と明確さのために `LimiterProtocol` を明示的に継承します。
3. Injection: `core.Spot.__init__` を、`tpm` の代わりに `limiter: LimiterProtocol` インスタンスを受け取るように変更します。
4. Factory Responsibility: `__init__.py` の `Spot` ファクトリ関数が、カスタムリミッターが提供されない場合のデフォルトの `TokenBucket` 作成を担当します。

## Consequences / 決定

- Positive:
  - テスト容易性: `MockLimiter` などを注入することで、テストの高速化が可能になった。
  - 柔軟性: 外部ストアを利用したリミッターなどを、コアを汚さずに実装可能になった。
  - クリーンなコア: `Spot` クラスの責務が削減され、コードが簡素化された。
- Negative:
  - 内部クラス `_Spot` のシグネチャが変更されるため、直接インスタンス化していたコードに影響がある（公開されている `Spot` ファクトリ経由であれば影響なし）。

# Limiter Dependency Injection ## Context and Problem Statement / コンテキスト 以前、`core.py` の `Spot` クラスは `TokenBucket` 実装と密結合していました。`Spot` は `tpm` (tokens per minute) 整数引数を受け取り、内部で `TokenBucket` をインスタンス化していました。 この設計にはいくつかの制限がありました： 1. **Testing**: ユニットテストが `TokenBucket` 内の本物の `time.sleep` 呼び出しに依存するため、実行が遅くなっていました。 2. **Extensibility**: ユーザーがカスタムレートリミッター（例：Redisベースの分散リミッター）や異なるアルゴリズムを提供できませんでした。 3. **Separation of Concerns**: `core.Spot` がリミッターのライフサイクルと設定を管理しており、単一責任の原則に違反していました。 ## Decision Drivers / 要求 * **Testability**: テスト中にスリープ時間を排除するために、`MockLimiter` や `NoOpLimiter` を注入できるようにすること。 * **Flexibility**: コアロジックを修正することなく、ユーザーがカスタムリミッターを実装できるようにすること。 * **Clean Core**: `core.Spot` を簡素化し、リミッターの初期化処理を排除すること。 ## Considered Options / 検討 * **Option 1**: 引き続き `Spot` クラス内でリミッターの実装をハードコードする。 * **Option 2**: 抽象プロトコル `LimiterProtocol` を導入し、依存性注入 (DI) を使用してリミッターを外部から提供する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. レートリミッターを `core.Spot` から切り離し、依存性注入 (DI) を使用します。 1. **Protocol Definition**: `beautyspot.limiter` に `consume(cost: int)` と `consume_async(cost: int)` メソッドを規定する `LimiterProtocol` を定義します。 2. **Explicit Inheritance**: デフォルトの `TokenBucket` 実装は、型安全性と明確さのために `LimiterProtocol` を明示的に継承します。 3. **Injection**: `core.Spot.__init__` を、`tpm` の代わりに `limiter: LimiterProtocol` インスタンスを受け取るように変更します。 4. **Factory Responsibility**: `__init__.py` の `Spot` ファクトリ関数が、カスタムリミッターが提供されない場合のデフォルトの `TokenBucket` 作成を担当します。 ## Consequences / 決定 * **Positive**: * **テスト容易性**: `MockLimiter` などを注入することで、テストの高速化が可能になった。 * **柔軟性**: 外部ストアを利用したリミッターなどを、コアを汚さずに実装可能になった。 * **クリーンなコア**: `Spot` クラスの責務が削減され、コードが簡素化された。 * **Negative**: * 内部クラス `_Spot` のシグネチャが変更されるため、直接インスタンス化していたコードに影響がある（公開されている `Spot` ファクトリ経由であれば影響なし）。

親: REQ008

子: —

### ADR032 {h(g)} ✓ レビュー済 局所ビュー →

# Declarative Lifecycle Policy

## Context and Problem Statement / コンテキスト

機械学習や生成AIの実験プロセスにおいて、生成されるデータの重要度は均一ではありません。数ヶ月保持すべき「最終モデル」もあれば、数時間で不要になる「一時的なデバッグ出力」もあります。

現在、これらの古いデータを削除するには、ユーザーが `MaintenanceService.prune(older_than=...)` を呼び出すスクリプトを自作し、定期実行する必要があります。これはユーザーにとって負担であり、設定を忘れるとディスク容量を圧迫する原因となります。また、`beautyspot` は常駐プロセスを持たないため、クリーンアップのトリガー設計が課題となります。

## Decision Drivers / 要求

- Cognitive Load Reduction: ユーザーが「Toil（苦役）」から解放され、データの寿命を宣言するだけで管理が完結すること。
- Predictable Performance: 有効期限の判定や削除処理が、メインの実行パスのレイテンシに悪影響を与えないこと。
- Safety: デフォルトではデータを無期限に保持し、意図しないデータ消失を防ぐこと。

## Considered Options / 検討

- Option 1: 命令的なアプローチ。ユーザーが必要に応じて削除スクリプトを記述・実行する。
- Option 2: 宣言的な `retention` パラメータとデータベースでの有効期限管理を導入し、アクセス時のチェック（Lazy Expiration）と CLI による一括削除を組み合わせる。

## Decision Outcome / 決定

Chosen option: Option 2.

ユーザーが「データの寿命（What）」を宣言するだけで済むよう、以下の仕組みを導入します。

### 1. `retention` Parameter for `@mark`

デコレータおよび `run` メソッドに `retention` 引数を追加します（例: `"7d"`, `"1h"`, `None`）。

### 2. Database Schema Change (`expires_at`)

タスク作成時に寿命を計算し、`tasks` テーブルの `expires_at` カラムに保存します。

### 3. Lazy Expiration (Access-time Check)

キャッシュ取得時（`spot.db.get`）に、`expires_at < current_time` であれば「キャッシュミス」とみなして `None` を返します。この時点では物理削除は行わず、レイテンシへの影響を最小限にします。

### 4. Explicit Garbage Collection (CLI)

期限切れデータの物理削除は、CLI コマンド `$ beautyspot gc --expired` によって一括で行います。

## Consequences / 決定

- Positive:
  - 認知負荷の低減: 関数定義時に寿命を決めるだけで、後片付けを気にする必要がなくなる。
  - パフォーマンス: 有効期限の判定は TIMESTAMP 比較のみで高速。
  - 安全性: デフォルトは無期限であるため、安全。
- Negative:
  - 容量解放の遅延: CLI コマンドが実行されるまで、期限切れデータもディスクに残り続ける。
  - マイグレーション: 既存テーブルへのカラム追加が必要。

# Declarative Lifecycle Policy ## Context and Problem Statement / コンテキスト 機械学習や生成AIの実験プロセスにおいて、生成されるデータの重要度は均一ではありません。数ヶ月保持すべき「最終モデル」もあれば、数時間で不要になる「一時的なデバッグ出力」もあります。 現在、これらの古いデータを削除するには、ユーザーが `MaintenanceService.prune(older_than=...)` を呼び出すスクリプトを自作し、定期実行する必要があります。これはユーザーにとって負担であり、設定を忘れるとディスク容量を圧迫する原因となります。また、`beautyspot` は常駐プロセスを持たないため、クリーンアップのトリガー設計が課題となります。 ## Decision Drivers / 要求 * **Cognitive Load Reduction**: ユーザーが「Toil（苦役）」から解放され、データの寿命を宣言するだけで管理が完結すること。 * **Predictable Performance**: 有効期限の判定や削除処理が、メインの実行パスのレイテンシに悪影響を与えないこと。 * **Safety**: デフォルトではデータを無期限に保持し、意図しないデータ消失を防ぐこと。 ## Considered Options / 検討 * **Option 1**: 命令的なアプローチ。ユーザーが必要に応じて削除スクリプトを記述・実行する。 * **Option 2**: 宣言的な `retention` パラメータとデータベースでの有効期限管理を導入し、アクセス時のチェック（Lazy Expiration）と CLI による一括削除を組み合わせる。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ユーザーが「データの寿命（What）」を宣言するだけで済むよう、以下の仕組みを導入します。 ### 1. `retention` Parameter for `@mark` デコレータおよび `run` メソッドに `retention` 引数を追加します（例: `"7d"`, `"1h"`, `None`）。 ### 2. Database Schema Change (`expires_at`) タスク作成時に寿命を計算し、`tasks` テーブルの `expires_at` カラムに保存します。 ### 3. Lazy Expiration (Access-time Check) キャッシュ取得時（`spot.db.get`）に、`expires_at < current_time` であれば「キャッシュミス」とみなして `None` を返します。この時点では物理削除は行わず、レイテンシへの影響を最小限にします。 ### 4. Explicit Garbage Collection (CLI) 期限切れデータの物理削除は、CLI コマンド `$ beautyspot gc --expired` によって一括で行います。 ## Consequences / 決定 * **Positive**: * **認知負荷の低減**: 関数定義時に寿命を決めるだけで、後片付けを気にする必要がなくなる。 * **パフォーマンス**: 有効期限の判定は TIMESTAMP 比較のみで高速。 * **安全性**: デフォルトは無期限であるため、安全。 * **Negative**: * **容量解放の遅延**: CLI コマンドが実行されるまで、期限切れデータもディスクに残り続ける。 * **マイグレーション**: 既存テーブルへのカラム追加が必要。

親: REQ007

子: —

### ADR033 {h(g)} ✓ レビュー済 局所ビュー →

# 非同期保存処理におけるエラー可視化とコンテキストの導入

## Context and Problem Statement / コンテキスト

beautyspot では、関数の実行結果をキャッシュに保存する際、`wait=False` を指定することでバックグラウンドスレッドでの非同期保存を行うことができます。しかし、バックグラウンドでの保存処理中にエラーが発生した場合、これまではログが出力されるのみ（サイレントフェイル）でした。

メインスレッドをクラッシュさせないという点では安全ですが、ユーザーからすると「なぜかキャッシュが効かない」という原因究明が困難な状態に陥るリスクがありました。また、監視ツールへのエラー通知や、カスタムのリカバリ処理を行うためのフックが存在しませんでした。

## Decision Drivers / 要求

- Observability: バックグラウンドでの保存失敗を、ユーザーが確実に検知し、適切に（監視システムへの通知など）対処できること。
- Developer Experience (DX): エラー情報の取得において、型補完が効く安全なインターフェースを提供すること。
- Safety: エラーハンドリング自体がメインロジックの安定性やパフォーマンスを損なわないこと。

## Considered Options / 検討

- Option 1: 現状維持（ログ出力のみ）。可観測性が低い。
- Option 2: メインスレッドへ例外を伝播させる。アプリケーションの安定性を損なうため不採用。
- Option 3: コールバック関数 (`on_background_error`) の導入。エラー発生時に任意の処理を実行可能にする。

## Decision Outcome / 決定

Chosen option: Option 3.

以下の設計方針で実装します。

1. `on_background_error` 引数の追加: `Spot.__init__` にコールバックを受け付ける引数を追加します。
2. 型安全なコンテキストオブジェクト (`SaveErrorContext`) の導入: ハンドラーには、エラー時の詳細情報（対象の関数名、キャッシュキー、戻り値等）を含む専用のデータクラスを渡します。
3. `result` (評価済みオブジェクト) の包含: デバッグを容易にするため、キャッシュしようとした実際のオブジェクトを含めます。ただしメモリリークのリスクがあるため、Docstring にて警告を記載します。
4. コールバック内の例外保護: コールバック関数自体が失敗した場合でも、バックグラウンドスレッドをクラッシュさせないよう `try-except` で保護します。

## Consequences / 決定

- Positive:
  - ユーザーはバックグラウンド保存の失敗を確実に検知し、監視システムへの通知等が可能になる。
  - 型付きコンテキストにより、安全かつ直感的にエラー詳細へアクセスできる。
  - メインロジックのパフォーマンスや安定性を維持したまま、高い可観測性を実現できる。
- Negative:
  - `SaveErrorContext` で巨大なオブジェクトの参照を保持し続けた場合、メモリを圧迫する可能性がある。

# 非同期保存処理におけるエラー可視化とコンテキストの導入 ## Context and Problem Statement / コンテキスト beautyspot では、関数の実行結果をキャッシュに保存する際、`wait=False` を指定することでバックグラウンドスレッドでの非同期保存を行うことができます。しかし、バックグラウンドでの保存処理中にエラーが発生した場合、これまではログが出力されるのみ（サイレントフェイル）でした。 メインスレッドをクラッシュさせないという点では安全ですが、ユーザーからすると「なぜかキャッシュが効かない」という原因究明が困難な状態に陥るリスクがありました。また、監視ツールへのエラー通知や、カスタムのリカバリ処理を行うためのフックが存在しませんでした。 ## Decision Drivers / 要求 * **Observability**: バックグラウンドでの保存失敗を、ユーザーが確実に検知し、適切に（監視システムへの通知など）対処できること。 * **Developer Experience (DX)**: エラー情報の取得において、型補完が効く安全なインターフェースを提供すること。 * **Safety**: エラーハンドリング自体がメインロジックの安定性やパフォーマンスを損なわないこと。 ## Considered Options / 検討 * **Option 1**: 現状維持（ログ出力のみ）。可観測性が低い。 * **Option 2**: メインスレッドへ例外を伝播させる。アプリケーションの安定性を損なうため不採用。 * **Option 3**: コールバック関数 (`on_background_error`) の導入。エラー発生時に任意の処理を実行可能にする。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 以下の設計方針で実装します。 1. **`on_background_error` 引数の追加:** `Spot.__init__` にコールバックを受け付ける引数を追加します。 2. **型安全なコンテキストオブジェクト (`SaveErrorContext`) の導入:** ハンドラーには、エラー時の詳細情報（対象の関数名、キャッシュキー、戻り値等）を含む専用のデータクラスを渡します。 3. **`result` (評価済みオブジェクト) の包含:** デバッグを容易にするため、キャッシュしようとした実際のオブジェクトを含めます。ただしメモリリークのリスクがあるため、Docstring にて警告を記載します。 4. **コールバック内の例外保護:** コールバック関数自体が失敗した場合でも、バックグラウンドスレッドをクラッシュさせないよう `try-except` で保護します。 ## Consequences / 決定 * **Positive**: * ユーザーはバックグラウンド保存の失敗を確実に検知し、監視システムへの通知等が可能になる。 * 型付きコンテキストにより、安全かつ直感的にエラー詳細へアクセスできる。 * メインロジックのパフォーマンスや安定性を維持したまま、高い可観測性を実現できる。 * **Negative**: * `SaveErrorContext` で巨大なオブジェクトの参照を保持し続けた場合、メモリを圧迫する可能性がある。

親: REQ006

子: —

### ADR034 {h(g)} ✓ レビュー済 局所ビュー →

# cached_run スコープ制限の廃止

## Context and Problem Statement / コンテキスト

ADR-0014 では、`cached_run` が返すラッパー関数を `with` ブロック外から呼び出した場合に `RuntimeError` を送出する Runtime Guard パターンを導入しました。しかし、ADR-0023 で `Spot` インスタンス自体の再利用性（`with spot:` はフラッシュのみを目的とし、インスタンスの無効化ではない）が確立されたことにより、`cached_run` のラッパーについても同様にスコープ外での利用を妨げない方針が自然な帰結となりました。

また、Runtime Guard の実装において `ContextVar` を使用していましたが、呼び出しごとに新規の `ContextVar` を作成する構造になっており、コンテキスト分離の恩恵が得られていませんでした。さらに、asyncio の一部のパターンにおいてガードが機能しないバグも内包していました。

## Decision Drivers / 要求

- Consistency: `Spot` インスタンスのライフサイクル管理方針と、`cached_run` ラッパーの挙動に一貫性を持たせること。
- Simplicity: 複雑でバグを含みやすい `ContextVar` によるガードロジックを排除し、コードを簡素化すること。
- Predictability: asyncio を含む非同期コンテキストにおいても、一貫した挙動を提供すること。

## Considered Options / 検討

- Option 1: ADR-0014 の方針を維持し、`ContextVar` の実装を修正してガードを強化する。
- Option 2: スコープ制限（ガード）を廃止し、ラッパー関数をブロック外でも呼び出し可能とする。

## Decision Outcome / 決定

Chosen option: Option 2.

`cached_run` の Runtime Guard を廃止し、返されたラッパー関数は `with` ブロック外でも呼び出し可能とします。

1. 簡略化: `ContextVar`, `is_active`, `make_scoped_guard` などの関連ロジックを全て削除します。
2. 実装の純粋化: `cached_run` は単に `self.mark()` を対象関数に適用して返すだけの、直感的な実装に変更します。

## Consequences / 決定

- Positive:
  - シンプルな実装: ガード層がなくなり、コードの保守性が向上した。
  - バグの除去: `ContextVar` の誤用や非同期タスクでの不整合リスクが解消された。
  - 一貫性: `Spot` インスタンスと同様、ラッパーもスコープに縛られず柔軟に利用可能になった。
- Negative:
  - 誤用の検出不可: `with` ブロック外でのラッパー使用がランタイムで検出されなくなる。ただし、ラッパー自体は `@spot.mark` 相当の正当な関数であり、副作用は生じないため問題視しない。

# cached_run スコープ制限の廃止 ## Context and Problem Statement / コンテキスト ADR-0014 では、`cached_run` が返すラッパー関数を `with` ブロック外から呼び出した場合に `RuntimeError` を送出する Runtime Guard パターンを導入しました。しかし、ADR-0023 で `Spot` インスタンス自体の再利用性（`with spot:` はフラッシュのみを目的とし、インスタンスの無効化ではない）が確立されたことにより、`cached_run` のラッパーについても同様にスコープ外での利用を妨げない方針が自然な帰結となりました。 また、Runtime Guard の実装において `ContextVar` を使用していましたが、呼び出しごとに新規の `ContextVar` を作成する構造になっており、コンテキスト分離の恩恵が得られていませんでした。さらに、asyncio の一部のパターンにおいてガードが機能しないバグも内包していました。 ## Decision Drivers / 要求 * **Consistency**: `Spot` インスタンスのライフサイクル管理方針と、`cached_run` ラッパーの挙動に一貫性を持たせること。 * **Simplicity**: 複雑でバグを含みやすい `ContextVar` によるガードロジックを排除し、コードを簡素化すること。 * **Predictability**: asyncio を含む非同期コンテキストにおいても、一貫した挙動を提供すること。 ## Considered Options / 検討 * **Option 1**: ADR-0014 の方針を維持し、`ContextVar` の実装を修正してガードを強化する。 * **Option 2**: スコープ制限（ガード）を廃止し、ラッパー関数をブロック外でも呼び出し可能とする。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `cached_run` の Runtime Guard を廃止し、返されたラッパー関数は `with` ブロック外でも呼び出し可能とします。 1. **簡略化**: `ContextVar`, `is_active`, `make_scoped_guard` などの関連ロジックを全て削除します。 2. **実装の純粋化**: `cached_run` は単に `self.mark()` を対象関数に適用して返すだけの、直感的な実装に変更します。 ## Consequences / 決定 * **Positive**: * **シンプルな実装**: ガード層がなくなり、コードの保守性が向上した。 * **バグの除去**: `ContextVar` の誤用や非同期タスクでの不整合リスクが解消された。 * **一貫性**: `Spot` インスタンスと同様、ラッパーもスコープに縛られず柔軟に利用可能になった。 * **Negative**: * **誤用の検出不可**: `with` ブロック外でのラッパー使用がランタイムで検出されなくなる。ただし、ラッパー自体は `@spot.mark` 相当の正当な関数であり、副作用は生じないため問題視しない。

親: REQ001

子: —

### ADR035 {h(g)} ✓ レビュー済 局所ビュー →

# クラスベース・フックシステムと並列実行サポート

## Context and Problem Statement / コンテキスト

`beautyspot` のコアロジックを汚染することなく、ユーザーが「トークン計算」や「レイテンシ計測」などのカスタムメトリクスを収集できる仕組みが必要です。また、並列実行環境においても、競合状態を避けつつ安全に状態を共有・更新できることが求められます。

## Decision Drivers / 要求

- Extensibility: コアコードを修正することなく、実行の各フェーズにユーザー定義の処理を差し込めること。
- Thread Safety: マルチスレッド環境下でも、ユーザーが複雑なロック制御を意識することなく安全に状態を集計できること。
- Type Safety & DX: 実行フェーズ（実行前、ヒット時、ミス時）ごとに利用可能なデータを明確にし、IDE での型補完を最大限に活用できること。

## Considered Options / 検討

- Option 1: 単純な関数ベースのコールバック。状態保持が難しく、スレッドセーフティの確保がユーザー任せになる。
- Option 2: クラスベースのインターフェース (`HookBase`) と、フェーズ別に分離された専用コンテキストオブジェクトの導入。

## Decision Outcome / 決定

Chosen option: Option 2.

### 1. クラスベースのフックインターフェース (`HookBase`)

状態（開始時間や累計カウンタ）を保持しやすいよう、クラスベースのインターフェースを採用します。

### 2. 並列実行のための自動ロック機構 (`ThreadSafeHookBase`)

サブクラスでオーバーライドされたメソッドを自動でロックラッパーで包む仕組みを導入します。これにより、ユーザーは明示的なロック制御なしでスレッドセーフな集計が可能になります。

### 3. 型安全なコンテキストオブジェクトの分離

実行フェーズごとに最適化された 3つの専用コンテキストクラス を提供します。 * `PreExecuteContext`: 関数実行前。 * `CacheHitContext`: キャッシュ取得成功時。 * `CacheMissContext`: 関数実行完了時。

### 4. ゼロオーバーヘッドの原則

フックが登録されていない場合は、コンテキストオブジェクトの生成を完全にスキップします。

## Consequences / 決定

- Positive:
  - 優れた DX: 専用コンテキストにより、IDE でのプロパティ補完が正確に機能し、開発時のミスを防げる。
  - 並列処理の安全保障: 複雑なマルチスレッドプログラミングの知識がなくても安全な拡張が可能になる。
- Negative:
  - 実装の複雑性: ライブラリ内部で複数のコンテキストクラスを管理する必要がある。
  - ボイラープレート: ユーザーはクラスを継承して実装する手間があるが、型安全性のメリットが上回る。

# クラスベース・フックシステムと並列実行サポート ## Context and Problem Statement / コンテキスト `beautyspot` のコアロジックを汚染することなく、ユーザーが「トークン計算」や「レイテンシ計測」などのカスタムメトリクスを収集できる仕組みが必要です。また、並列実行環境においても、競合状態を避けつつ安全に状態を共有・更新できることが求められます。 ## Decision Drivers / 要求 * **Extensibility**: コアコードを修正することなく、実行の各フェーズにユーザー定義の処理を差し込めること。 * **Thread Safety**: マルチスレッド環境下でも、ユーザーが複雑なロック制御を意識することなく安全に状態を集計できること。 * **Type Safety & DX**: 実行フェーズ（実行前、ヒット時、ミス時）ごとに利用可能なデータを明確にし、IDE での型補完を最大限に活用できること。 ## Considered Options / 検討 * **Option 1**: 単純な関数ベースのコールバック。状態保持が難しく、スレッドセーフティの確保がユーザー任せになる。 * **Option 2**: クラスベースのインターフェース (`HookBase`) と、フェーズ別に分離された専用コンテキストオブジェクトの導入。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ### 1. クラスベースのフックインターフェース (`HookBase`) 状態（開始時間や累計カウンタ）を保持しやすいよう、クラスベースのインターフェースを採用します。 ### 2. 並列実行のための自動ロック機構 (`ThreadSafeHookBase`) サブクラスでオーバーライドされたメソッドを自動でロックラッパーで包む仕組みを導入します。これにより、ユーザーは明示的なロック制御なしでスレッドセーフな集計が可能になります。 ### 3. 型安全なコンテキストオブジェクトの分離 実行フェーズごとに最適化された **3つの専用コンテキストクラス** を提供します。 * **`PreExecuteContext`**: 関数実行前。 * **`CacheHitContext`**: キャッシュ取得成功時。 * **`CacheMissContext`**: 関数実行完了時。 ### 4. ゼロオーバーヘッドの原則 フックが登録されていない場合は、コンテキストオブジェクトの生成を完全にスキップします。 ## Consequences / 決定 * **Positive**: * **優れた DX**: 専用コンテキストにより、IDE でのプロパティ補完が正確に機能し、開発時のミスを防げる。 * **並列処理の安全保障**: 複雑なマルチスレッドプログラミングの知識がなくても安全な拡張が可能になる。 * **Negative**: * **実装の複雑性**: ライブラリ内部で複数のコンテキストクラスを管理する必要がある。 * **ボイラープレート**: ユーザーはクラスを継承して実装する手間があるが、型安全性のメリットが上回る。

親: REQ009

子: —

### ADR036 {h(g)} ✓ レビュー済 局所ビュー →

# バックグラウンドループのライフサイクル管理とGC時のデータロスト対策

## Context and Problem Statement / コンテキスト

`Spot` クラスは非同期のキャッシュ保存を構造的に直列化するため、内部で専用の `asyncio` ループを実行するスレッド (`_BackgroundLoop`) を保持しています。インスタンスがガベージコレクション (GC) によって破棄される際、未保存のキャッシュデータを厳密に待機してしまうと、メインスレッドを予測不能なタイミングでフリーズさせたり、デッドロックを引き起こす危険性があります。

そのため、これまでは安全を優先し、GC 時にはタスクをキャンセルしてリソースを解放する設計となっていました。しかし、この設計では一時的なスコープで `Spot` を使用した場合、GC のタイミングによってキャッシュデータが失われるという課題がありました。

## Decision Drivers / 要求

- Safety: GC 時の予期せぬブロッキングやデッドロックを回避し、アプリケーションの安定性を損なわないこと。
- Data Integrity: バックグラウンドに投入された保存タスクを、可能な限り完了させること。
- Predictability: ユーザーがライフサイクルを制御できる手段を提供し、挙動を明確にすること。

## Considered Options / 検討

- Option 1: GC 時に常に保存完了を待機する。フリーズやデッドロックのリスクが高い。
- Option 2: GC 時に即座にタスクをキャンセルする。データロストのリスクがある。
- Option 3: `atexit` によるグローバルな安全網を導入し、プロセス終了時に未完了タスクをドレインする。あわせて明示的な管理を推奨する。

## Decision Outcome / 決定

Chosen option: Option 3.

1. `atexit` によるグローバルな安全網の追加: `weakref.WeakSet` を用いて起動中のバックグラウンドループを追跡し、プロセス終了時 (`atexit`) に `wait=True` でドレイン（残存タスクの処理）を行うフックを実装します。プロセス終了時であれば、メインスレッドをブロックしてもデッドロックの危険性が低いためです。
2. 明示的なリソース管理の推奨: `Spot` インスタンスは長寿命なオブジェクトとして扱うか、コンテキストマネージャ (`with`) または `shutdown(wait=True)` による明示的な管理を行うべきであることを周知します。

## Consequences / 決定

- Positive:
  - 一時的なスコープで `Spot` を使った場合でも、アプリ終了時まではデータが救済される可能性が高まる。
  - メインスレッドの安全性（GC 時のフリーズ回避）と、キャッシュ保存の確実性のバランスが取れる。
- Negative:
  - `atexit` 時に大量の未完了タスクがあると、プロセス終了に時間がかかる場合がある。

# バックグラウンドループのライフサイクル管理とGC時のデータロスト対策 ## Context and Problem Statement / コンテキスト `Spot` クラスは非同期のキャッシュ保存を構造的に直列化するため、内部で専用の `asyncio` ループを実行するスレッド (`_BackgroundLoop`) を保持しています。インスタンスがガベージコレクション (GC) によって破棄される際、未保存のキャッシュデータを厳密に待機してしまうと、メインスレッドを予測不能なタイミングでフリーズさせたり、デッドロックを引き起こす危険性があります。 そのため、これまでは安全を優先し、GC 時にはタスクをキャンセルしてリソースを解放する設計となっていました。しかし、この設計では一時的なスコープで `Spot` を使用した場合、GC のタイミングによってキャッシュデータが失われるという課題がありました。 ## Decision Drivers / 要求 * **Safety**: GC 時の予期せぬブロッキングやデッドロックを回避し、アプリケーションの安定性を損なわないこと。 * **Data Integrity**: バックグラウンドに投入された保存タスクを、可能な限り完了させること。 * **Predictability**: ユーザーがライフサイクルを制御できる手段を提供し、挙動を明確にすること。 ## Considered Options / 検討 * **Option 1**: GC 時に常に保存完了を待機する。フリーズやデッドロックのリスクが高い。 * **Option 2**: GC 時に即座にタスクをキャンセルする。データロストのリスクがある。 * **Option 3**: `atexit` によるグローバルな安全網を導入し、プロセス終了時に未完了タスクをドレインする。あわせて明示的な管理を推奨する。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 1. **`atexit` によるグローバルな安全網の追加**: `weakref.WeakSet` を用いて起動中のバックグラウンドループを追跡し、プロセス終了時 (`atexit`) に `wait=True` でドレイン（残存タスクの処理）を行うフックを実装します。プロセス終了時であれば、メインスレッドをブロックしてもデッドロックの危険性が低いためです。 2. **明示的なリソース管理の推奨**: `Spot` インスタンスは長寿命なオブジェクトとして扱うか、コンテキストマネージャ (`with`) または `shutdown(wait=True)` による明示的な管理を行うべきであることを周知します。 ## Consequences / 決定 * **Positive**: * 一時的なスコープで `Spot` を使った場合でも、アプリ終了時まではデータが救済される可能性が高まる。 * メインスレッドの安全性（GC 時のフリーズ回避）と、キャッシュ保存の確実性のバランスが取れる。 * **Negative**: * `atexit` 時に大量の未完了タスクがあると、プロセス終了に時間がかかる場合がある。

親: REQ006

子: —

### ADR037 {h(g)} ✓ レビュー済 局所ビュー →

# GC時のデータロスト防止とバックグラウンドループのシャットダウン戦略

## Context and Problem Statement / コンテキスト

`beautyspot` の `Spot` クラスは、非同期でのキャッシュ保存タスクを構造的に直列化するため、内部で専用の `asyncio` ループを実行するバックグラウンドスレッド (`_BackgroundLoop`) を保持しています。インスタンスのライフサイクル終了時、これらのバックグラウンドタスクを安全に終了させる必要がありますが、シャットダウンのトリガー（明示的呼び出し、アプリ終了、GC）によって、メインスレッドへの影響とプロセスの振る舞いが大きく異なるという課題がありました。

特に、GC は予測不能なタイミングで発生します。ここでタスク完了を待機（スレッドを `join`）してしまうと、メインスレッドがフリーズしたりデッドロックを引き起こす危険性があります。一方で、待機せずにタスクを強制キャンセルすると、保存待ちだったキャッシュデータが失われてしまいます。

## Decision Drivers / 要求

- Stability: GC 発生時にメイン処理をブロックせず、アプリの安定性とパフォーマンスを維持すること。
- Data Reliability: インスタンスがスコープを抜けた後でも、投入された保存タスクを可能な限り完了させること。
- Safety: プロセス終了時には確実にデータをドレイン（排出）し、不完全な書き込みを防ぐこと。

## Considered Options / 検討

- Option 1: 常に同期的にシャットダウンする（GC 時のフリーズリスク）。
- Option 2: 常に非同期でシャットダウンし、タスクをキャンセルする（データロストリスク）。
- Option 3: シャットダウン方式を状況（明示的、GC、終了時）に応じて使い分ける多段的な戦略を導入する。

## Decision Outcome / 決定

Chosen option: Option 3.

以下の3つの方式を実装し、状況に応じて使い分けます。

1. `stop(wait=True)` (完全同期シャットダウン)
   - 用途: 明示的な `shutdown()`、`with` ブロックの終了、`atexit`。
   - 理由: データの確定を保証するため。プロセス終了時はメインスレッドをブロックしないと OS に強制終了されるため。
2. `stop_gracefully_no_wait()` (非同期ドレイン / スレッド切り離し)
   - 用途: `weakref.finalize` による GC 時。
   - 理由: メインスレッドのフリーズを回避しつつ、既存タスクを完了させてから自発的に終了させるため。
3. `stop(wait=False)` (強制停止)
   - 用途: エラー発生時の緊急停止など。

## Consequences / 決定

- Positive:
  - 堅牢性の向上: `Spot` が一時的なスコープでインスタンス化され GC された場合でも、データロストのリスクがほぼ解消された。
  - 安全性の担保: GC 発生時にメイン処理がブロックされない。
- Negative:
  - 実装が複雑になり、バックグラウンドスレッドの状態管理（状態遷移）が重要になる。

# GC時のデータロスト防止とバックグラウンドループのシャットダウン戦略 ## Context and Problem Statement / コンテキスト `beautyspot` の `Spot` クラスは、非同期でのキャッシュ保存タスクを構造的に直列化するため、内部で専用の `asyncio` ループを実行するバックグラウンドスレッド (`_BackgroundLoop`) を保持しています。インスタンスのライフサイクル終了時、これらのバックグラウンドタスクを安全に終了させる必要がありますが、シャットダウンのトリガー（明示的呼び出し、アプリ終了、GC）によって、メインスレッドへの影響とプロセスの振る舞いが大きく異なるという課題がありました。 特に、GC は予測不能なタイミングで発生します。ここでタスク完了を待機（スレッドを `join`）してしまうと、メインスレッドがフリーズしたりデッドロックを引き起こす危険性があります。一方で、待機せずにタスクを強制キャンセルすると、保存待ちだったキャッシュデータが失われてしまいます。 ## Decision Drivers / 要求 * **Stability**: GC 発生時にメイン処理をブロックせず、アプリの安定性とパフォーマンスを維持すること。 * **Data Reliability**: インスタンスがスコープを抜けた後でも、投入された保存タスクを可能な限り完了させること。 * **Safety**: プロセス終了時には確実にデータをドレイン（排出）し、不完全な書き込みを防ぐこと。 ## Considered Options / 検討 * **Option 1**: 常に同期的にシャットダウンする（GC 時のフリーズリスク）。 * **Option 2**: 常に非同期でシャットダウンし、タスクをキャンセルする（データロストリスク）。 * **Option 3**: シャットダウン方式を状況（明示的、GC、終了時）に応じて使い分ける多段的な戦略を導入する。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 以下の3つの方式を実装し、状況に応じて使い分けます。 1. **`stop(wait=True)`** (完全同期シャットダウン) * **用途:** 明示的な `shutdown()`、`with` ブロックの終了、`atexit`。 * **理由:** データの確定を保証するため。プロセス終了時はメインスレッドをブロックしないと OS に強制終了されるため。 2. **`stop_gracefully_no_wait()`** (非同期ドレイン / スレッド切り離し) * **用途:** `weakref.finalize` による GC 時。 * **理由:** メインスレッドのフリーズを回避しつつ、既存タスクを完了させてから自発的に終了させるため。 3. **`stop(wait=False)`** (強制停止) * **用途:** エラー発生時の緊急停止など。 ## Consequences / 決定 * **Positive**: * **堅牢性の向上**: `Spot` が一時的なスコープでインスタンス化され GC された場合でも、データロストのリスクがほぼ解消された。 * **安全性の担保**: GC 発生時にメイン処理がブロックされない。 * **Negative**: * 実装が複雑になり、バックグラウンドスレッドの状態管理（状態遷移）が重要になる。

親: REQ006

子: —

### ADR038 {h(g)} ✓ レビュー済 局所ビュー →

# Probabilistic Auto-Eviction for Storage Maintenance

## Context and Problem Statement / コンテキスト

`beautyspot` はキャッシュの TTL をサポートしており、期限切れデータは論理的に無効化されます。しかし、ユーザーが明示的にクリーンアップを呼び出さない限り、有効期限切れのメタデータや巨大な孤立 Blob ファイルは物理的に削除されず、ストレージが肥大化し続けるという問題がありました。

ユーザーにインフラ管理を意識させない「黒子」としての哲学を維持しつつ、自動でガベージコレクション（エビクション）を行う仕組みが必要になりました。

## Decision Drivers / 要求

- Autonomy: ユーザーが手動でメンテナンススクリプトを書くことなく、ストレージの肥大化が自動的に抑制されること。
- Zero Latency Impact: エビクション処理がアプリケーションのメインロジックの応答速度（レイテンシ）を低下させないこと。
- Simplicity: 複雑な容量計算や頻繁な DB 更新を避け、実装と保守のコストを最小限に抑えること。

## Considered Options / 検討

1. Capacity-based LRU (Least Recently Used) Policy:
   - ストレージ容量やレコード数の上限に達した際に古いデータを削除する。
   - 評価: 却下。キャッシュヒットのたびにタイムスタンプを更新する必要があり、DB の書き込み競合やレイテンシ低下を招く。
2. Load-Adaptive Background Eviction:
   - アイドル状態のときにのみクリーンアップタスクを投入する。
   - 評価: 却下。実装が複雑になりすぎる（オーバーエンジニアリング）。
3. Probabilistic Auto-Eviction (確率的自動実行):
   - 新しい結果の保存直後、設定された確率に基づいてバックグラウンドでクリーンアップタスクを投入する。
   - 評価: 採用。 シンプルかつ低コスト。

## Decision Outcome / 決定

Chosen option: Option 3.

確率的自動エビクションを採用します。

- `Spot` の初期化パラメータに `eviction_rate: float = 0.0` を追加し、オプトイン方式とします。
- エビクション処理は、メインスレッドやイベントループをブロックしないよう、`_BackgroundLoop` にオフロードします。
- ロック競合を防ぐため、掃除タスクの内部に非ブロッキングロックを導入し、重複実行を安全にスキップさせます。

## Consequences / 決定

- Positive:
  - ユーザーは設定一つで、メインロジックの性能を犠牲にすることなくストレージの肥大化を防げる。
  - 実装がシンプルで保守コストが低い。
- Negative:
  - 確率に依存するため、決定論的な容量解放の保証はない。厳密な管理が必要な場合は引き続き手動での呼び出しが必要。

# Probabilistic Auto-Eviction for Storage Maintenance ## Context and Problem Statement / コンテキスト `beautyspot` はキャッシュの TTL をサポートしており、期限切れデータは論理的に無効化されます。しかし、ユーザーが明示的にクリーンアップを呼び出さない限り、有効期限切れのメタデータや巨大な孤立 Blob ファイルは物理的に削除されず、ストレージが肥大化し続けるという問題がありました。 ユーザーにインフラ管理を意識させない「黒子」としての哲学を維持しつつ、自動でガベージコレクション（エビクション）を行う仕組みが必要になりました。 ## Decision Drivers / 要求 * **Autonomy**: ユーザーが手動でメンテナンススクリプトを書くことなく、ストレージの肥大化が自動的に抑制されること。 * **Zero Latency Impact**: エビクション処理がアプリケーションのメインロジックの応答速度（レイテンシ）を低下させないこと。 * **Simplicity**: 複雑な容量計算や頻繁な DB 更新を避け、実装と保守のコストを最小限に抑えること。 ## Considered Options / 検討 1. **Capacity-based LRU (Least Recently Used) Policy:** * ストレージ容量やレコード数の上限に達した際に古いデータを削除する。 * **評価:** 却下。キャッシュヒットのたびにタイムスタンプを更新する必要があり、DB の書き込み競合やレイテンシ低下を招く。 2. **Load-Adaptive Background Eviction:** * アイドル状態のときにのみクリーンアップタスクを投入する。 * **評価:** 却下。実装が複雑になりすぎる（オーバーエンジニアリング）。 3. **Probabilistic Auto-Eviction (確率的自動実行):** * 新しい結果の保存直後、設定された確率に基づいてバックグラウンドでクリーンアップタスクを投入する。 * **評価:** **採用。** シンプルかつ低コスト。 ## Decision Outcome / 決定 Chosen option: **Option 3**. **確率的自動エビクション**を採用します。 * `Spot` の初期化パラメータに `eviction_rate: float = 0.0` を追加し、オプトイン方式とします。 * エビクション処理は、メインスレッドやイベントループをブロックしないよう、`_BackgroundLoop` にオフロードします。 * ロック競合を防ぐため、掃除タスクの内部に非ブロッキングロックを導入し、重複実行を安全にスキップさせます。 ## Consequences / 決定 * **Positive**: * ユーザーは設定一つで、メインロジックの性能を犠牲にすることなくストレージの肥大化を防げる。 * 実装がシンプルで保守コストが低い。 * **Negative**: * 確率に依存するため、決定論的な容量解放の保証はない。厳密な管理が必要な場合は引き続き手動での呼び出しが必要。

親: REQ007

子: —

### ADR039 {h(g)} ✓ レビュー済 局所ビュー →

# Bounded LRU Cache for Subclass Resolution in Serializer

## Context and Problem Statement / コンテキスト

`MsgpackSerializer` の `_default_packer` において、クラスの解決結果を保存するキャッシュ辞書が、動的に型が生成される環境（動的な Pydantic モデル等）で無制限に肥大化し、メモリリークを引き起こす懸念がありました。 また、この辞書へのアクセスは非常に高頻度であるため、厳密なスレッドセーフティ（ロック）を導入すると、シリアライズ性能のボトルネック（ロック競合）を招く恐れがありました。

## Decision Drivers / 要求

- Memory Safety: 動的な型生成が繰り返される環境下でも、キャッシュサイズが一定の上限を超えないようにし、メモリリークを防止すること。
- High Performance: シリアライズのホットパス（実行頻度の高い経路）において、ロックのオーバーヘッドを最小限に抑えること。
- Zero Dependencies: 標準ライブラリのみで完結させること。

## Considered Options / 検討

- Option 1: 現状維持（無制限の `dict`）。メモリリークのリスクがある。
- Option 2: 厳密なスレッドセーフな LRU キャッシュの導入。パフォーマンス低下のリスクがある。
- Option 3: `OrderedDict` を利用した上限付き LRU キャッシュを、楽観的（ロックフリー）なアプローチで実装する。

## Decision Outcome / 決定

Chosen option: Option 3.

標準ライブラリの `collections.OrderedDict` を利用し、上限サイズ（デフォルト 1024）を持つ LRU (Least Recently Used) キャッシュ を導入します。マルチスレッド環境下でのアクセスに対しては、意図的にロックフリーなアプローチを維持します。

## Consequences / 決定

- Positive:
  - キャッシュサイズが上限に保たれ、メモリ肥大化を確実に防止できる。
  - ロックのオーバーヘッドが発生しないため、高いシリアライズ性能を維持できる。
- Negative:
  - スレッドセーフではないため、マルチスレッド下で稀に LRU の順序が乱れたり、重複した解決処理が走る可能性がある。ただし、プロセスクラッシュには至らず、実用上の影響も軽微であるため許容する。

# Bounded LRU Cache for Subclass Resolution in Serializer ## Context and Problem Statement / コンテキスト `MsgpackSerializer` の `_default_packer` において、クラスの解決結果を保存するキャッシュ辞書が、動的に型が生成される環境（動的な Pydantic モデル等）で無制限に肥大化し、メモリリークを引き起こす懸念がありました。 また、この辞書へのアクセスは非常に高頻度であるため、厳密なスレッドセーフティ（ロック）を導入すると、シリアライズ性能のボトルネック（ロック競合）を招く恐れがありました。 ## Decision Drivers / 要求 * **Memory Safety**: 動的な型生成が繰り返される環境下でも、キャッシュサイズが一定の上限を超えないようにし、メモリリークを防止すること。 * **High Performance**: シリアライズのホットパス（実行頻度の高い経路）において、ロックのオーバーヘッドを最小限に抑えること。 * **Zero Dependencies**: 標準ライブラリのみで完結させること。 ## Considered Options / 検討 * **Option 1**: 現状維持（無制限の `dict`）。メモリリークのリスクがある。 * **Option 2**: 厳密なスレッドセーフな LRU キャッシュの導入。パフォーマンス低下のリスクがある。 * **Option 3**: `OrderedDict` を利用した上限付き LRU キャッシュを、楽観的（ロックフリー）なアプローチで実装する。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 標準ライブラリの `collections.OrderedDict` を利用し、上限サイズ（デフォルト 1024）を持つ **LRU (Least Recently Used) キャッシュ** を導入します。マルチスレッド環境下でのアクセスに対しては、意図的にロックフリーなアプローチを維持します。 ## Consequences / 決定 * **Positive**: * キャッシュサイズが上限に保たれ、メモリ肥大化を確実に防止できる。 * ロックのオーバーヘッドが発生しないため、高いシリアライズ性能を維持できる。 * **Negative**: * スレッドセーフではないため、マルチスレッド下で稀に LRU の順序が乱れたり、重複した解決処理が走る可能性がある。ただし、プロセスクラッシュには至らず、実用上の影響も軽微であるため許容する。

親: REQ005

子: —

### ADR040 {h(g)} ✓ レビュー済 局所ビュー →

# Background Loop Shutdown Strategy and Threading Model

## Context and Problem Statement / コンテキスト

バックグラウンドでの非同期 IO タスクを処理するイベントループにおいて、単純に `daemon=True` スレッドを使用すると、メインスレッド終了時にプロセスが即座に強制終了され、データ破損やキャッシュのロストが発生する危険がありました。一方で、`daemon=False` にして `atexit` で待機を試みるアプローチでは、タスクがハングした場合にプロセス全体が永遠に終了しなくなるデッドロックの罠が存在していました。

## Decision Drivers / 要求

- Data Durability: プロセス終了時に、インフライト（実行中）の IO タスクが確実に完了し、ディスクへ永続化されること。
- Reliability: タスクのスケジュールとシャットダウンの間の競合状態 (Race Condition) を排除すること。
- Liveness: ネットワーク障害等でタスクがスタックしても、一定時間後にプロセスが確実に終了し、ゾンビ化を防ぐこと。

## Considered Options / 検討

- Option 1: `daemon=False` スレッドを使用し、`atexit` で待機する。デッドロックやハングのリスクが高い。
- Option 2: `daemon=True` スレッドを維持しつつ、明示的なタスク追跡と猶予期間（Grace Period）付きの `atexit` ハンドラを実装する。

## Decision Outcome / 決定

Chosen option: Option 2.

真の耐障害性を持つ Graceful Shutdown を実現するため、以下のアーキテクチャを採用します。

1. デーモンスレッドの維持: スレッドは `daemon=True` で起動し、プロセス終了時の無限ハングを OS の力で防ぎます。
2. 明示的なタスク追跡とロック: ロックとアクティブタスクカウンタを用いたステートマシンを導入し、`submit` 時と `atexit` 時の競合状態を完全に排除します。
3. Grace Period（猶予期間）の導入: `atexit` フック内でメインスレッドをブロックし、タイムアウト付き（デフォルト5秒）で `_thread.join()` を実行することで、IO タスクが完了するための猶予を与えます。

## Consequences / 決定

- Positive:
  - ユーザーが明示的な終了処理を意識せずとも、プロセス終了時に安全にデータが永続化される。
  - タスクが無限にスタックしても、タイムアウト後にプロセスが終了するため、アプリがハングアップしない。
- Negative:
  - プロセス終了時に、バックグラウンド処理が終わるまで最大 5 秒のブロックが発生する。

# Background Loop Shutdown Strategy and Threading Model ## Context and Problem Statement / コンテキスト バックグラウンドでの非同期 IO タスクを処理するイベントループにおいて、単純に `daemon=True` スレッドを使用すると、メインスレッド終了時にプロセスが即座に強制終了され、データ破損やキャッシュのロストが発生する危険がありました。一方で、`daemon=False` にして `atexit` で待機を試みるアプローチでは、タスクがハングした場合にプロセス全体が永遠に終了しなくなるデッドロックの罠が存在していました。 ## Decision Drivers / 要求 * **Data Durability**: プロセス終了時に、インフライト（実行中）の IO タスクが確実に完了し、ディスクへ永続化されること。 * **Reliability**: タスクのスケジュールとシャットダウンの間の競合状態 (Race Condition) を排除すること。 * **Liveness**: ネットワーク障害等でタスクがスタックしても、一定時間後にプロセスが確実に終了し、ゾンビ化を防ぐこと。 ## Considered Options / 検討 * **Option 1**: `daemon=False` スレッドを使用し、`atexit` で待機する。デッドロックやハングのリスクが高い。 * **Option 2**: `daemon=True` スレッドを維持しつつ、明示的なタスク追跡と猶予期間（Grace Period）付きの `atexit` ハンドラを実装する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 真の耐障害性を持つ Graceful Shutdown を実現するため、以下のアーキテクチャを採用します。 1. **デーモンスレッドの維持**: スレッドは `daemon=True` で起動し、プロセス終了時の無限ハングを OS の力で防ぎます。 2. **明示的なタスク追跡とロック**: ロックとアクティブタスクカウンタを用いたステートマシンを導入し、`submit` 時と `atexit` 時の競合状態を完全に排除します。 3. **Grace Period（猶予期間）の導入**: `atexit` フック内でメインスレッドをブロックし、タイムアウト付き（デフォルト5秒）で `_thread.join()` を実行することで、IO タスクが完了するための猶予を与えます。 ## Consequences / 決定 * **Positive**: * ユーザーが明示的な終了処理を意識せずとも、プロセス終了時に安全にデータが永続化される。 * タスクが無限にスタックしても、タイムアウト後にプロセスが終了するため、アプリがハングアップしない。 * **Negative**: * プロセス終了時に、バックグラウンド処理が終わるまで最大 5 秒のブロックが発生する。

親: REQ006

子: —

### ADR041 {h(g)} ✓ レビュー済 局所ビュー →

# Garbage Collection Strategy for Abandoned Temporary Files

## Context and Problem Statement / コンテキスト

Blob データの保存時、アトミックな書き込みを実現するために一時ファイルを作成し、完了後にリネームする設計を採用しています。しかし、アンチウイルスソフトやバックアッププロセスの介入によりリネームが失敗するエッジケースが存在します。この際、一時ファイルの削除もロックにより失敗すると、一時ファイルがストレージ上に永久に残留し続ける（ストレージリーク）という問題がありました。

## Decision Drivers / 要求

- Self-Healing: 書き込み失敗やクラッシュによって残された一時ファイルを、ユーザーの介入なしに自動でクリーンアップすること。
- Safety: 現在書き込み中の別プロセスのファイルを誤って削除しないよう、適切な猶予期間を設けること。
- Traceability: 管理対象の一時ファイルを、通常のキャッシュファイルや他のファイルと容易に区別できるようにすること。

## Considered Options / 検討

- Option 1: 失敗した一時ファイルを無視する（ストレージリークを許容）。
- Option 2: 失敗時に即座にループで再試行する。長時間ロックされている場合にプロセスを停滞させるリスクがある。
- Option 3: 専用のサフィックスを付与し、`MaintenanceService` による遅延ガベージコレクション (GC) を導入する。

## Decision Outcome / 決定

Chosen option: Option 3.

アトミック書き込みのフェイルセーフとして、以下の機構を導入します。

1. 専用サフィックスの導入: 一時ファイルの生成時に `.spot_tmp` という専用のサフィックスを付与し、追跡を容易にします。
2. 遅延ガベージコレクション (GC): `MaintenanceService.clean_garbage` に、一時ファイルの削除処理を統合します。
3. Grace Period (猶予期間) の設定: 更新時刻が指定時間（デフォルト24時間）を経過しているもののみを削除対象とし、並行プロセスへの影響を回避します。

## Consequences / 決定

- Positive:
  - アプリのクラッシュやファイルロックが発生しても、自動エビクションによってストレージが自己修復される。
  - ユーザーはストレージリークを意識することなく安全に運用できる。
- Negative:
  - 失敗した一時ファイルは最大24時間ストレージ容量を占有し続けるが、実用上の影響は皆無である。

# Garbage Collection Strategy for Abandoned Temporary Files ## Context and Problem Statement / コンテキスト Blob データの保存時、アトミックな書き込みを実現するために一時ファイルを作成し、完了後にリネームする設計を採用しています。しかし、アンチウイルスソフトやバックアッププロセスの介入によりリネームが失敗するエッジケースが存在します。この際、一時ファイルの削除もロックにより失敗すると、一時ファイルがストレージ上に永久に残留し続ける（ストレージリーク）という問題がありました。 ## Decision Drivers / 要求 * **Self-Healing**: 書き込み失敗やクラッシュによって残された一時ファイルを、ユーザーの介入なしに自動でクリーンアップすること。 * **Safety**: 現在書き込み中の別プロセスのファイルを誤って削除しないよう、適切な猶予期間を設けること。 * **Traceability**: 管理対象の一時ファイルを、通常のキャッシュファイルや他のファイルと容易に区別できるようにすること。 ## Considered Options / 検討 * **Option 1**: 失敗した一時ファイルを無視する（ストレージリークを許容）。 * **Option 2**: 失敗時に即座にループで再試行する。長時間ロックされている場合にプロセスを停滞させるリスクがある。 * **Option 3**: 専用のサフィックスを付与し、`MaintenanceService` による遅延ガベージコレクション (GC) を導入する。 ## Decision Outcome / 決定 Chosen option: **Option 3**. アトミック書き込みのフェイルセーフとして、以下の機構を導入します。 1. **専用サフィックスの導入**: 一時ファイルの生成時に `.spot_tmp` という専用のサフィックスを付与し、追跡を容易にします。 2. **遅延ガベージコレクション (GC)**: `MaintenanceService.clean_garbage` に、一時ファイルの削除処理を統合します。 3. **Grace Period (猶予期間) の設定**: 更新時刻が指定時間（デフォルト24時間）を経過しているもののみを削除対象とし、並行プロセスへの影響を回避します。 ## Consequences / 決定 * **Positive**: * アプリのクラッシュやファイルロックが発生しても、自動エビクションによってストレージが自己修復される。 * ユーザーはストレージリークを意識することなく安全に運用できる。 * **Negative**: * 失敗した一時ファイルは最大24時間ストレージ容量を占有し続けるが、実用上の影響は皆無である。

親: REQ010

子: —

### ADR042 {h(g)} ✓ レビュー済 局所ビュー →

# Thread-safe LRU Cache for Serializer

## Context and Problem Statement / コンテキスト

`MsgpackSerializer` は動的型生成時のメモリリークを防ぐために、内部で `OrderedDict` を用いた LRU キャッシュを運用しています。しかし、バックグラウンド保存や Web フレームワークでの利用など、マルチスレッド環境からの並行アクセスが日常的に発生します。ロックを持たない `OrderedDict` への並行操作は、`RuntimeError` やデータの破損を引き起こす致命的なバグの温床となっていました。

## Decision Drivers / 要求

- Stability: マルチスレッド環境下での並行アクセスによるクラッシュやデータ破損を完全に排除すること。
- Reliability: デバッグ困難な稀に発生する競合状態を未然に防ぎ、ライブラリの信頼性を高めること。
- Performance: ロックの範囲を最小限に留め、シリアライズ処理全体のスループットへの影響を最小化すること。

## Considered Options / 検討

- Option 1: 引き続きロックフリーなアプローチをとり、エラー発生時にリトライする。
- Option 2: `threading.Lock()` を導入し、キャッシュ操作を排他的に行う。

## Decision Outcome / 決定

Chosen option: Option 2.

`MsgpackSerializer` の内部状態（キャッシュおよびレジストリ）に対するすべての読み書き操作を `threading.Lock()` で保護します。

1. 安定性の優先: 確実な排他制御により、マルチスレッド下での信頼性を担保します。
2. 限定的なロック範囲: ロックの範囲を「型の解決とキャッシュの更新」というメモリアクセス領域に限定し、重いシリアライズ処理（I/O や計算）はロック外で実行します。

## Consequences / 決定

- Positive:
  - `MsgpackSerializer` をシングルトンとして安全にマルチスレッド環境で使い回せるようになった。
  - 競合状態に起因する非決定的なクラッシュを排除できた。
- Negative:
  - キャッシュアクセスごとにロック取得のオーバーヘッドが追加されるが、実用上のパフォーマンスインパクトは極めて軽微である。

# Thread-safe LRU Cache for Serializer ## Context and Problem Statement / コンテキスト `MsgpackSerializer` は動的型生成時のメモリリークを防ぐために、内部で `OrderedDict` を用いた LRU キャッシュを運用しています。しかし、バックグラウンド保存や Web フレームワークでの利用など、マルチスレッド環境からの並行アクセスが日常的に発生します。ロックを持たない `OrderedDict` への並行操作は、`RuntimeError` やデータの破損を引き起こす致命的なバグの温床となっていました。 ## Decision Drivers / 要求 * **Stability**: マルチスレッド環境下での並行アクセスによるクラッシュやデータ破損を完全に排除すること。 * **Reliability**: デバッグ困難な稀に発生する競合状態を未然に防ぎ、ライブラリの信頼性を高めること。 * **Performance**: ロックの範囲を最小限に留め、シリアライズ処理全体のスループットへの影響を最小化すること。 ## Considered Options / 検討 * **Option 1**: 引き続きロックフリーなアプローチをとり、エラー発生時にリトライする。 * **Option 2**: `threading.Lock()` を導入し、キャッシュ操作を排他的に行う。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `MsgpackSerializer` の内部状態（キャッシュおよびレジストリ）に対するすべての読み書き操作を `threading.Lock()` で保護します。 1. **安定性の優先**: 確実な排他制御により、マルチスレッド下での信頼性を担保します。 2. **限定的なロック範囲**: ロックの範囲を「型の解決とキャッシュの更新」というメモリアクセス領域に限定し、重いシリアライズ処理（I/O や計算）はロック外で実行します。 ## Consequences / 決定 * **Positive**: * `MsgpackSerializer` をシングルトンとして安全にマルチスレッド環境で使い回せるようになった。 * 競合状態に起因する非決定的なクラッシュを排除できた。 * **Negative**: * キャッシュアクセスごとにロック取得のオーバーヘッドが追加されるが、実用上のパフォーマンスインパクトは極めて軽微である。

親: REQ005

子: —

### ADR043 {h(g)} ✓ レビュー済 局所ビュー →

# Serialize SQLite Writes with a Writer Queue

## Context and Problem Statement / コンテキスト

`beautyspot` は `wait=False` のバックグラウンド保存や `async` 経路で、 複数スレッドから同時に SQLite へ書き込みを行う可能性がある。 WAL モードを有効化していても SQLite の書き込みは単一ライター制約があり、 高並行時に `database is locked` が発生し得る。

一方で、アプリケーションの関数実行自体は失敗させたくない。 また、シャットダウン時には必ず書き込みをフラッシュしたい。

## Decision Drivers / 要求

- Reliability: プロセス内の書き込み競合を回避し、安定して保存する。
- Non-blocking API: キャッシュ保存の失敗で関数実行を失敗させない。
- Flush Guarantee: `shutdown(wait=True)` で書き込みを確実にフラッシュする。
- Minimal Surface Change: 公開 API の破壊的変更を避ける。

## Considered Options / 検討

- Option 1: 現状維持（都度接続 + timeout）。
- Option 2: コネクションプールを導入。
- Option 3: プロセス内ロックで `save/delete` を直列化（単一接続は持たない）。
- Option 4: 専用 writer スレッド + キューで直列化（単一接続を保持）。
- Option 5: 外部 DB 利用を推奨（SQLite を事実上やめる）。

## Decision Outcome / 決定

Chosen option: Option 4.

`SQLiteTaskDB` に writer スレッドとキューを実装し、 書き込み系操作を単一接続で直列化する。 シャットダウン時は `shutdown(wait=True)` によりキューを drain し、 未処理の書き込みが残らないようにする。

さらに、保存失敗は関数実行を失敗させず、 ログと `on_background_error` で通知する。

### Implementation Sketch / 実装概要

- `SQLiteTaskDB` 内部に `queue.Queue` と writer スレッドを保持。
- 書き込み系メソッドは `_enqueue_write()` 経由でシリアライズ。
- `TaskDBBase.shutdown()` を追加し、`SQLiteTaskDB.shutdown(wait=True)` でフラッシュ。
- `Spot.shutdown(wait=True)` から `db.shutdown(wait=True)` を呼び出す。
- `wait=True` 保存時の失敗も例外として外に出さず、ログとコールバックで通知。

## Consequences / 決定

- Positive:
- プロセス内の書き込み競合が解消され、`database is locked` の発生率が下がる。
- `shutdown(wait=True)` で書き込みを確実にフラッシュできる。

- 保存失敗がアプリケーションの関数実行に影響しない。

- Negative:

- 書き込みは直列化されるため、ピーク時の書き込み待ちが増える可能性がある。
- writer スレッドがハングした場合、`shutdown(wait=True)` がブロックし続ける。
- 他プロセスによる競合は依然として発生し得る。

## Notes / 補足

本 ADR はプロセス内の直列化を対象とする。複数プロセスが同一 SQLite を共有する 運用では、依然としてロック競合が起こり得るため注意する。

# Serialize SQLite Writes with a Writer Queue ## Context and Problem Statement / コンテキスト `beautyspot` は `wait=False` のバックグラウンド保存や `async` 経路で、 複数スレッドから同時に SQLite へ書き込みを行う可能性がある。 WAL モードを有効化していても SQLite の書き込みは単一ライター制約があり、 高並行時に `database is locked` が発生し得る。 一方で、アプリケーションの関数実行自体は失敗させたくない。 また、シャットダウン時には必ず書き込みをフラッシュしたい。 ## Decision Drivers / 要求 * **Reliability**: プロセス内の書き込み競合を回避し、安定して保存する。 * **Non-blocking API**: キャッシュ保存の失敗で関数実行を失敗させない。 * **Flush Guarantee**: `shutdown(wait=True)` で書き込みを確実にフラッシュする。 * **Minimal Surface Change**: 公開 API の破壊的変更を避ける。 ## Considered Options / 検討 * **Option 1**: 現状維持（都度接続 + timeout）。 * **Option 2**: コネクションプールを導入。 * **Option 3**: プロセス内ロックで `save/delete` を直列化（単一接続は持たない）。 * **Option 4**: 専用 writer スレッド + キューで直列化（単一接続を保持）。 * **Option 5**: 外部 DB 利用を推奨（SQLite を事実上やめる）。 ## Decision Outcome / 決定 Chosen option: **Option 4**. `SQLiteTaskDB` に writer スレッドとキューを実装し、 書き込み系操作を単一接続で直列化する。 シャットダウン時は `shutdown(wait=True)` によりキューを drain し、 未処理の書き込みが残らないようにする。 さらに、保存失敗は関数実行を失敗させず、 ログと `on_background_error` で通知する。 ### Implementation Sketch / 実装概要 * `SQLiteTaskDB` 内部に `queue.Queue` と writer スレッドを保持。 * 書き込み系メソッドは `_enqueue_write()` 経由でシリアライズ。 * `TaskDBBase.shutdown()` を追加し、`SQLiteTaskDB.shutdown(wait=True)` でフラッシュ。 * `Spot.shutdown(wait=True)` から `db.shutdown(wait=True)` を呼び出す。 * `wait=True` 保存時の失敗も例外として外に出さず、ログとコールバックで通知。 ## Consequences / 決定 * **Positive**: * プロセス内の書き込み競合が解消され、`database is locked` の発生率が下がる。 * `shutdown(wait=True)` で書き込みを確実にフラッシュできる。 * 保存失敗がアプリケーションの関数実行に影響しない。 * **Negative**: * 書き込みは直列化されるため、ピーク時の書き込み待ちが増える可能性がある。 * writer スレッドがハングした場合、`shutdown(wait=True)` がブロックし続ける。 * 他プロセスによる競合は依然として発生し得る。 ## Notes / 補足 本 ADR はプロセス内の直列化を対象とする。複数プロセスが同一 SQLite を共有する 運用では、依然としてロック競合が起こり得るため注意する。

親: REQ003

子: —

### ADR044 {h(g)} ✓ レビュー済 局所ビュー →

# Drop Absolute Path Support in LocalStorage

## Context and Problem Statement / コンテキスト

v1.x 系の `LocalStorage` では、ファイルパス（`location`）として絶対パスを許容・解決する挙動が含まれていました。v2.0 の開発において、パストラバーサル脆弱性を防ぐために `base_dir` に対する厳密なセキュリティチェックを導入しましたが、これが旧来の絶対パスの挙動と競合し、意図せぬクラッシュを引き起こす可能性がありました。

## Decision Drivers / 要求

- Security: ディレクトリトラバーサル攻撃のリスクを完全に排除し、ストレージ層の堅牢性を確保すること。
- Simplicity: 例外的なパス解決ロジックを排除し、ストレージバックエンドの実装を単純で予測可能なものに保つこと。
- Predictability: すべてのファイルアクセスが、指定された `base_dir` の管理下に限定されることを保証すること。

## Considered Options / 検討

- Option 1: 絶対パスの場合はセキュリティチェックをバイパスし、後方互換性を維持する。セキュリティ上のリスクが残る。
- Option 2: 絶対パスのサポートを公式に放棄し、全てのパスを `base_dir` からの相対パスとして厳密に解釈する。

## Decision Outcome / 決定

Chosen option: Option 2.

`LocalStorage.load()` および関連するファイルアクセスにおいて、絶対パスによる後方互換性の維持を公式に 放棄 します。

すべての `location` パラメータは `base_dir` に対する相対パスとしてのみ解釈されます。絶対パスの形式であっても、それが `base_dir` のサブディレクトリ内に解決されない限り、セキュリティチェックにより `ValueError` として処理されます。

## Consequences / 決定

- Positive:
  - ストレージ層の堅牢性が向上し、予期せぬディレクトリへのアクセスリスクが排除された。
  - コードの責務が単純化された。
- Negative:
  - v1.x からの移行ユーザーで、DB に絶対パスが記録されている場合、キャッシュの読み込みに失敗する。
- Mitigation:
  - この変更を v2 系の破壊的変更としてリリースノートに明記する。移行時にはキャッシュのクリアまたは DB のマイグレーションを推奨する。

# Drop Absolute Path Support in LocalStorage ## Context and Problem Statement / コンテキスト v1.x 系の `LocalStorage` では、ファイルパス（`location`）として絶対パスを許容・解決する挙動が含まれていました。v2.0 の開発において、パストラバーサル脆弱性を防ぐために `base_dir` に対する厳密なセキュリティチェックを導入しましたが、これが旧来の絶対パスの挙動と競合し、意図せぬクラッシュを引き起こす可能性がありました。 ## Decision Drivers / 要求 * **Security**: ディレクトリトラバーサル攻撃のリスクを完全に排除し、ストレージ層の堅牢性を確保すること。 * **Simplicity**: 例外的なパス解決ロジックを排除し、ストレージバックエンドの実装を単純で予測可能なものに保つこと。 * **Predictability**: すべてのファイルアクセスが、指定された `base_dir` の管理下に限定されることを保証すること。 ## Considered Options / 検討 * **Option 1**: 絶対パスの場合はセキュリティチェックをバイパスし、後方互換性を維持する。セキュリティ上のリスクが残る。 * **Option 2**: 絶対パスのサポートを公式に放棄し、全てのパスを `base_dir` からの相対パスとして厳密に解釈する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `LocalStorage.load()` および関連するファイルアクセスにおいて、絶対パスによる後方互換性の維持を公式に **放棄** します。 すべての `location` パラメータは `base_dir` に対する相対パスとしてのみ解釈されます。絶対パスの形式であっても、それが `base_dir` のサブディレクトリ内に解決されない限り、セキュリティチェックにより `ValueError` として処理されます。 ## Consequences / 決定 * **Positive**: * ストレージ層の堅牢性が向上し、予期せぬディレクトリへのアクセスリスクが排除された。 * コードの責務が単純化された。 * **Negative**: * v1.x からの移行ユーザーで、DB に絶対パスが記録されている場合、キャッシュの読み込みに失敗する。 * **Mitigation**: * この変更を v2 系の破壊的変更としてリリースノートに明記する。移行時にはキャッシュのクリアまたは DB のマイグレーションを推奨する。

親: REQ004

子: —

### ADR045 {h(g)} ✓ レビュー済 局所ビュー →

# Introduce Explicit DB Queue Flushing

## Context and Problem Statement / コンテキスト

`SQLiteTaskDB` は、ライタースレッドとキューを用いた非同期書き込みを行っています。しかし、メインプロセスの終了時や `Spot.flush()` 実行時に、ストレージへの I/O は待機するものの、DB への書き込みキューのフラッシュ（全タスクの完了）を明示的に待機する仕組みがありませんでした。これにより、短命なスクリプト等で DB へのデータ書き込みが完了する前にプロセスが終了し、メタデータが消失するリスクがありました。

## Decision Drivers / 要求

- Persistence Guarantee: アプリケーションの終了前、あるいは明示的な `flush()` 実行時に、全ての DB 書き込みタスクが完了していることを保証すること。
- Thread Safety: 既に走っているライタースレッドの処理を妨げたり、デッドロックを引き起こしたりすることなく、安全に待機できること。
- Consistency: ストレージへの保存（Blob）と DB への保存（メタデータ）の完了タイミングを一致させること。

## Considered Options / 検討

- Option 1: 明示的なフラッシュを実装せず、OS のシャットダウン時のバッファリングに任せる。データロストのリスクが高い。
- Option 2: `TaskDBBase` に `flush(timeout)` を追加する。`SQLiteTaskDB` では「No-op タスク」をキューに投入し、その完了を待機する方式を採用する。

## Decision Outcome / 決定

Chosen option: Option 2.

`TaskDBBase` に `flush(timeout)` インターフェースを追加し、`SQLiteTaskDB` で実装します。

`SQLiteTaskDB` の実装では、「何もしない (No-op) 書き込みタスク」をエンキューし、そのタスクの完了イベントを待機するというアプローチを採用します。この `flush` メソッドを `Spot.flush()` から呼び出すことで、ストレージと DB の両方の完了を確実に待機させます。

## Consequences / 決定

- Positive:
  - アプリケーション終了時にキャッシュデータが確実に永続化されることが保証された。
  - ロックや追加の状態変数を導入することなく、既存のタスク処理ループの中で安全に待機を実現できた。
- Negative:
  - `flush()` 呼び出し時に、キューに積まれた既存タスクの量に応じて待機時間が発生する。
  - `timeout` を超えた場合の挙動（ログ出力等）を適切に定義・管理する必要がある。

# Introduce Explicit DB Queue Flushing ## Context and Problem Statement / コンテキスト `SQLiteTaskDB` は、ライタースレッドとキューを用いた非同期書き込みを行っています。しかし、メインプロセスの終了時や `Spot.flush()` 実行時に、ストレージへの I/O は待機するものの、DB への書き込みキューのフラッシュ（全タスクの完了）を明示的に待機する仕組みがありませんでした。これにより、短命なスクリプト等で DB へのデータ書き込みが完了する前にプロセスが終了し、メタデータが消失するリスクがありました。 ## Decision Drivers / 要求 * **Persistence Guarantee**: アプリケーションの終了前、あるいは明示的な `flush()` 実行時に、全ての DB 書き込みタスクが完了していることを保証すること。 * **Thread Safety**: 既に走っているライタースレッドの処理を妨げたり、デッドロックを引き起こしたりすることなく、安全に待機できること。 * **Consistency**: ストレージへの保存（Blob）と DB への保存（メタデータ）の完了タイミングを一致させること。 ## Considered Options / 検討 * **Option 1**: 明示的なフラッシュを実装せず、OS のシャットダウン時のバッファリングに任せる。データロストのリスクが高い。 * **Option 2**: `TaskDBBase` に `flush(timeout)` を追加する。`SQLiteTaskDB` では「No-op タスク」をキューに投入し、その完了を待機する方式を採用する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `TaskDBBase` に `flush(timeout)` インターフェースを追加し、`SQLiteTaskDB` で実装します。 `SQLiteTaskDB` の実装では、**「何もしない (No-op) 書き込みタスク」をエンキューし、そのタスクの完了イベントを待機する**というアプローチを採用します。この `flush` メソッドを `Spot.flush()` から呼び出すことで、ストレージと DB の両方の完了を確実に待機させます。 ## Consequences / 決定 * **Positive**: * アプリケーション終了時にキャッシュデータが確実に永続化されることが保証された。 * ロックや追加の状態変数を導入することなく、既存のタスク処理ループの中で安全に待機を実現できた。 * **Negative**: * `flush()` 呼び出し時に、キューに積まれた既存タスクの量に応じて待機時間が発生する。 * `timeout` を超えた場合の挙動（ログ出力等）を適切に定義・管理する必要がある。

親: REQ003

子: —

### ADR046 {h(g)} ✓ レビュー済 局所ビュー →

# Delegate Workspace Initialization to Storage Backends

## Context and Problem Statement / コンテキスト

これまで `beautyspot` では、 Factory 関数 `Spot()` の初期化時において、デフォルトのキャッシュディレクトリ（`.beautyspot/`）の作成と `.gitignore` の配置を一律で行っていました。しかし、ユーザーがカスタムのパスを指定した場合でも、意図せずカレントディレクトリに `.beautyspot/` が作成されてしまうという課題がありました。また、コンポーネント（DB、ストレージ）が自身の永続化先の詳細を自己管理できておらず、関心の分離の観点で不完全な設計となっていました。

## Decision Drivers / 要求

- Separation of Concerns: コアロジックや Factory 関数を物理的なディレクトリ構造の詳細から解放し、純粋な依存性注入（DI）に専念させること。
- Zero Side Effects: ユーザーが指定していないパス（デフォルトパスなど）に対して、不要なディレクトリやファイルを生成しないこと。
- Extensibility: クラウドストレージなど、ローカルディレクトリを必要としないバックエンドの実装を容易にすること。

## Considered Options / 検討

- Option 1: Factory 関数 `Spot()` 内で、全てのコンポーネントの設定を読み取り、一括でディレクトリを作成する。
- Option 2: ワークスペースの初期化ロジックをコアから削除し、各バックエンドコンポーネントにその責務を委譲する。

## Decision Outcome / 決定

Chosen option: Option 2.

`__init__.py` および `core.py` からワークスペース初期化ロジック（`_setup_workspace`）を完全に削除します。代わりに、ローカルファイルシステムに依存する各バックエンドコンポーネント（`LocalStorage` および `SQLiteTaskDB`）の初期化処理内で、自身が使用するディレクトリの作成と `.gitignore` の配置を行うように責務を委譲します。

## Consequences / 決定

- Positive:
  - ユーザーがカスタムパスを指定した際の挙動が直感的になり、クリーンなファイルシステムが保たれる。
  - 各コンポーネントが独立して動作可能になり、テストや単体利用が容易になる。
- Negative:
  - カスタムのローカルバックエンドを実装するユーザーは、必要に応じて自身でディレクトリ作成や `.gitignore` 配置のロジックを書く必要がある。
- Mitigation:
  - 抽象基底クラスの Docstring および実装ガイドにおいて、インフラ管理の責務について明記する。

# Delegate Workspace Initialization to Storage Backends ## Context and Problem Statement / コンテキスト これまで `beautyspot` では、 Factory 関数 `Spot()` の初期化時において、デフォルトのキャッシュディレクトリ（`.beautyspot/`）の作成と `.gitignore` の配置を一律で行っていました。しかし、ユーザーがカスタムのパスを指定した場合でも、意図せずカレントディレクトリに `.beautyspot/` が作成されてしまうという課題がありました。また、コンポーネント（DB、ストレージ）が自身の永続化先の詳細を自己管理できておらず、関心の分離の観点で不完全な設計となっていました。 ## Decision Drivers / 要求 * **Separation of Concerns**: コアロジックや Factory 関数を物理的なディレクトリ構造の詳細から解放し、純粋な依存性注入（DI）に専念させること。 * **Zero Side Effects**: ユーザーが指定していないパス（デフォルトパスなど）に対して、不要なディレクトリやファイルを生成しないこと。 * **Extensibility**: クラウドストレージなど、ローカルディレクトリを必要としないバックエンドの実装を容易にすること。 ## Considered Options / 検討 * **Option 1**: Factory 関数 `Spot()` 内で、全てのコンポーネントの設定を読み取り、一括でディレクトリを作成する。 * **Option 2**: ワークスペースの初期化ロジックをコアから削除し、各バックエンドコンポーネントにその責務を委譲する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `__init__.py` および `core.py` からワークスペース初期化ロジック（`_setup_workspace`）を完全に削除します。代わりに、ローカルファイルシステムに依存する各バックエンドコンポーネント（`LocalStorage` および `SQLiteTaskDB`）の初期化処理内で、自身が使用するディレクトリの作成と `.gitignore` の配置を行うように責務を委譲します。 ## Consequences / 決定 * **Positive**: * ユーザーがカスタムパスを指定した際の挙動が直感的になり、クリーンなファイルシステムが保たれる。 * 各コンポーネントが独立して動作可能になり、テストや単体利用が容易になる。 * **Negative**: * カスタムのローカルバックエンドを実装するユーザーは、必要に応じて自身でディレクトリ作成や `.gitignore` 配置のロジックを書く必要がある。 * **Mitigation**: * 抽象基底クラスの Docstring および実装ガイドにおいて、インフラ管理の責務について明記する。

親: REQ004

子: —

### ADR047 {h(g)} ✓ レビュー済 局所ビュー →

# Background Loop Shutdown Strategy on Garbage Collection

## Context and Problem Statement / コンテキスト

`beautyspot` はキャッシュの永続化をバックグラウンドスレッドにオフロードしています。コンテキストマネージャを使用した場合は安全にタスクがドレインされますが、インスタンスをグローバルに生成して使い捨てた場合など、ガベージコレクション (GC) によってインスタンスが破棄される際の振る舞いが課題となっていました。GC のタイミングで未完了の I/O タスクを待機すべきか、それとも破棄すべきかを決定する必要があります。

## Decision Drivers / 要求

- Responsiveness: GC は任意のタイミングで発生するため、メインスレッドを予期せずフリーズさせないこと。
- Predictability: リソース管理の責務を明確にし、マジックに頼りすぎない予測可能なライフサイクルを提供すること。
- Observability: 適切でない管理によってデータが破棄された場合に、開発者がそれを検知できるようにすること。

## Considered Options / 検討

- Option 1: GC 時にもタスクの完了を待機する。メインスレッドがフリーズするリスクがある。
- Option 2: 未完了タスクを即座に破棄（キャンセル）し、警告を発行する。メインスレッドの安定性を最優先する。

## Decision Outcome / 決定

Chosen option: Option 2.

ガベージコレクション (`weakref.finalize`) によるインスタンス破棄時には、未完了のタスクを待機せず、即座に破棄する（Fail-fast & Non-blocking） ことを決定しました。

同時に、タスクが破棄された場合は `logger.warning` および `ResourceWarning` を発行し、ユーザーに対してコンテキストマネージャや明示的な `shutdown()` の使用を強く促します。

## Consequences / 決定

- Positive:
  - `beautyspot` が原因でアプリケーションの GC やシャットダウンがハングアップしないことが保証される。
  - ライフサイクル管理の重要性をユーザーに明示的に伝えることができる。
- Negative:
  - 不適切な使い方をした場合、キャッシュが永続化されない（データロスト）事象が発生する。
- Mitigation:
  - ドキュメントにてコンテキストマネージャの利用をベストプラクティスとして周知し、安全な管理方法を強調する。

# Background Loop Shutdown Strategy on Garbage Collection ## Context and Problem Statement / コンテキスト `beautyspot` はキャッシュの永続化をバックグラウンドスレッドにオフロードしています。コンテキストマネージャを使用した場合は安全にタスクがドレインされますが、インスタンスをグローバルに生成して使い捨てた場合など、ガベージコレクション (GC) によってインスタンスが破棄される際の振る舞いが課題となっていました。GC のタイミングで未完了の I/O タスクを待機すべきか、それとも破棄すべきかを決定する必要があります。 ## Decision Drivers / 要求 * **Responsiveness**: GC は任意のタイミングで発生するため、メインスレッドを予期せずフリーズさせないこと。 * **Predictability**: リソース管理の責務を明確にし、マジックに頼りすぎない予測可能なライフサイクルを提供すること。 * **Observability**: 適切でない管理によってデータが破棄された場合に、開発者がそれを検知できるようにすること。 ## Considered Options / 検討 * **Option 1**: GC 時にもタスクの完了を待機する。メインスレッドがフリーズするリスクがある。 * **Option 2**: 未完了タスクを即座に破棄（キャンセル）し、警告を発行する。メインスレッドの安定性を最優先する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ガベージコレクション (`weakref.finalize`) によるインスタンス破棄時には、**未完了のタスクを待機せず、即座に破棄する（Fail-fast & Non-blocking）** ことを決定しました。 同時に、タスクが破棄された場合は `logger.warning` および `ResourceWarning` を発行し、ユーザーに対してコンテキストマネージャや明示的な `shutdown()` の使用を強く促します。 ## Consequences / 決定 * **Positive**: * `beautyspot` が原因でアプリケーションの GC やシャットダウンがハングアップしないことが保証される。 * ライフサイクル管理の重要性をユーザーに明示的に伝えることができる。 * **Negative**: * 不適切な使い方をした場合、キャッシュが永続化されない（データロスト）事象が発生する。 * **Mitigation**: * ドキュメントにてコンテキストマネージャの利用をベストプラクティスとして周知し、安全な管理方法を強調する。

親: REQ006

子: —

### ADR048 {h(g)} ✓ レビュー済 局所ビュー →

# SQLite ライタースレッドの死活監視におけるポーリング間隔の維持

## Context and Problem Statement / コンテキスト

`SQLiteTaskDB` は、専用のライタースレッドを用いて書き込みを直列化しています。メインスレッドが書き込み完了を待機する際（または `flush` 実行時）、ライタースレッドが予期せず死亡（セグメンテーションフォールトや深刻なエラー等）した場合に、メインスレッドが永久にハングアップするのを防ぐ必要があります。

## Decision Drivers / 要求

- Liveness: ライタースレッドが異常終了した場合でも、メインスレッドがそれを検知して確実に解放されること。
- Zero Latency (Normal Case): 正常な動作時において、監視ロジックが書き込み完了の通知を遅延させないこと。
- Efficiency: 待機中の CPU リソースや電力の浪費を抑えること。
- Simplicity: デッドロックのリスクを避け、実装を簡潔に保つこと。

## Considered Options / 検討

- Option 1: 純粋なイベントベースの通知。ライタースレッドが通知前に死亡するとメインスレッドが永久にハングする。
- Option 2: 極端に短い間隔（0.01秒等）でのポーリング。CPU リソースを浪費する。
- Option 3: バランスの取れた間隔（0.5秒）でのポーリングと、スレッドの生存確認（`is_alive()`）を組み合わせる。

## Decision Outcome / 決定

Chosen option: Option 3.

0.5 秒というポーリング間隔を維持し、ループ内で `task.event.wait(timeout=0.5)` とスレッドの生存確認を行う設計を継続します。

1. 安全装置: ライタースレッドが異常終了しても、最大 0.5 秒以内に異常を検知してメインスレッドを解放できます。
2. 正常系パフォーマンス: `threading.Event.wait` はイベントがセットされた瞬間に即座に復帰するため、正常な書き込み時には 0.5 秒という数値はレイテンシに一切影響しません。
3. リソース効率: 間隔を適切に保つことで、不要なコンテキストスイッチや CPU 浪費を防ぎます。

## Consequences / 決定

- Positive:
  - 実装が簡潔でデッドロックのリスクが低く、かつスレッド死亡時にも確実にリカバリできる。
  - 正常系のパフォーマンスを一切損なうことなく、異常系への耐性を獲得できる。
- Negative:
  - スレッドが死亡した際、異常を認識するまでに最大 0.5 秒のタイムラグが発生する。しかし、稀な異常事態におけるこの程度の遅延は許容範囲内である。

# SQLite ライタースレッドの死活監視におけるポーリング間隔の維持 ## Context and Problem Statement / コンテキスト `SQLiteTaskDB` は、専用のライタースレッドを用いて書き込みを直列化しています。メインスレッドが書き込み完了を待機する際（または `flush` 実行時）、ライタースレッドが予期せず死亡（セグメンテーションフォールトや深刻なエラー等）した場合に、メインスレッドが永久にハングアップするのを防ぐ必要があります。 ## Decision Drivers / 要求 * **Liveness**: ライタースレッドが異常終了した場合でも、メインスレッドがそれを検知して確実に解放されること。 * **Zero Latency (Normal Case)**: 正常な動作時において、監視ロジックが書き込み完了の通知を遅延させないこと。 * **Efficiency**: 待機中の CPU リソースや電力の浪費を抑えること。 * **Simplicity**: デッドロックのリスクを避け、実装を簡潔に保つこと。 ## Considered Options / 検討 * **Option 1**: 純粋なイベントベースの通知。ライタースレッドが通知前に死亡するとメインスレッドが永久にハングする。 * **Option 2**: 極端に短い間隔（0.01秒等）でのポーリング。CPU リソースを浪費する。 * **Option 3**: バランスの取れた間隔（0.5秒）でのポーリングと、スレッドの生存確認（`is_alive()`）を組み合わせる。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 0.5 秒というポーリング間隔を維持し、ループ内で `task.event.wait(timeout=0.5)` とスレッドの生存確認を行う設計を継続します。 1. **安全装置**: ライタースレッドが異常終了しても、最大 0.5 秒以内に異常を検知してメインスレッドを解放できます。 2. **正常系パフォーマンス**: `threading.Event.wait` はイベントがセットされた瞬間に即座に復帰するため、正常な書き込み時には 0.5 秒という数値はレイテンシに一切影響しません。 3. **リソース効率**: 間隔を適切に保つことで、不要なコンテキストスイッチや CPU 浪費を防ぎます。 ## Consequences / 決定 * **Positive**: * 実装が簡潔でデッドロックのリスクが低く、かつスレッド死亡時にも確実にリカバリできる。 * 正常系のパフォーマンスを一切損なうことなく、異常系への耐性を獲得できる。 * **Negative**: * スレッドが死亡した際、異常を認識するまでに最大 0.5 秒のタイムラグが発生する。しかし、稀な異常事態におけるこの程度の遅延は許容範囲内である。

親: REQ003

子: —

### ADR049 {h(g)} ✓ レビュー済 局所ビュー →

# 実行中タスクの強参照セットによる追跡

## Context and Problem Statement / コンテキスト

`Spot` インスタンスは、バックグラウンドで実行中の保存タスク（`Future`）を内部のセットで管理しています。これはシャットダウン時や `flush()` 呼び出し時に、すべてのタスクが完了したかを確認するために必要です。この追跡において、強参照を使用すべきか、それとも GC を妨げない弱参照を使用すべきかを決定する必要があります。

## Decision Drivers / 要求

- Durability: ユーザーコードが `Future` オブジェクトへの参照を即座に破棄した場合でも、タスクが途中で GC されることなく確実に完了まで実行されること。
- Waitability: `flush()` 呼び出し時に、全ての未完了タスクを漏れなく検知し、待機できること。
- Determinism: タスクの追跡ライフサイクルが、GC という非決定的なイベントではなく、タスクの完了という明確なイベントに同期していること。

## Considered Options / 検討

- Option 1: `weakref.WeakSet` を使用する。参照がなくなると自動的に削除されるが、待機すべきタスクが GC のタイミングで勝手に消えてしまうリスクがある。
- Option 2: 通常の `set`（強参照）を使用し、タスク完了のコールバックによって明示的にセットから削除する。

## Decision Outcome / 決定

Chosen option: Option 2.

タスクの追跡には通常の `set` を使用し、強参照によってタスクの生存を保証します。

1. 生存保証: `Spot` が強参照を保持することで、タスクが確実に完了まで実行されることを保証します。
2. 待機可能性: `WeakSet` で発生しうる「待機すべきタスクが勝手に消える」という不透明な挙動を排除します。
3. 決定論的な挙動: タスク削除のタイミングが完了コールバックに紐付くため、挙動が予測可能になり、デバッグやテストが容易になります。

## Consequences / 決定

- Positive:
  - タスクの完了が保証され、データの整合性が高まる。
  - `flush()` の挙動が正確になり、確実なドレインが可能になる。
- Negative:
  - コールバックが呼ばれないような致命的なバグが発生した場合に、メモリを占有し続けるリスクがある。ただし、`atexit` 等のシャットダウン機構によってプロセスレベルでの救済策が講じられているため、許容可能である。

# 実行中タスクの強参照セットによる追跡 ## Context and Problem Statement / コンテキスト `Spot` インスタンスは、バックグラウンドで実行中の保存タスク（`Future`）を内部のセットで管理しています。これはシャットダウン時や `flush()` 呼び出し時に、すべてのタスクが完了したかを確認するために必要です。この追跡において、強参照を使用すべきか、それとも GC を妨げない弱参照を使用すべきかを決定する必要があります。 ## Decision Drivers / 要求 * **Durability**: ユーザーコードが `Future` オブジェクトへの参照を即座に破棄した場合でも、タスクが途中で GC されることなく確実に完了まで実行されること。 * **Waitability**: `flush()` 呼び出し時に、全ての未完了タスクを漏れなく検知し、待機できること。 * **Determinism**: タスクの追跡ライフサイクルが、GC という非決定的なイベントではなく、タスクの完了という明確なイベントに同期していること。 ## Considered Options / 検討 * **Option 1**: `weakref.WeakSet` を使用する。参照がなくなると自動的に削除されるが、待機すべきタスクが GC のタイミングで勝手に消えてしまうリスクがある。 * **Option 2**: 通常の `set`（強参照）を使用し、タスク完了のコールバックによって明示的にセットから削除する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. タスクの追跡には通常の `set` を使用し、強参照によってタスクの生存を保証します。 1. **生存保証**: `Spot` が強参照を保持することで、タスクが確実に完了まで実行されることを保証します。 2. **待機可能性**: `WeakSet` で発生しうる「待機すべきタスクが勝手に消える」という不透明な挙動を排除します。 3. **決定論的な挙動**: タスク削除のタイミングが完了コールバックに紐付くため、挙動が予測可能になり、デバッグやテストが容易になります。 ## Consequences / 決定 * **Positive**: * タスクの完了が保証され、データの整合性が高まる。 * `flush()` の挙動が正確になり、確実なドレインが可能になる。 * **Negative**: * コールバックが呼ばれないような致命的なバグが発生した場合に、メモリを占有し続けるリスクがある。ただし、`atexit` 等のシャットダウン機構によってプロセスレベルでの救済策が講じられているため、許容可能である。

親: REQ006

子: —

### ADR050 {h(g)} ✓ レビュー済 局所ビュー →

# Delegate Database Lifecycle Management to Caller

## Context and Problem Statement / コンテキスト

`Spot` クラスは `TaskDBBase` インスタンスを DI で受け取っているにもかかわらず、シャットダウン処理内で強制的に `db.shutdown()` を呼び出していました。これにより、複数の `Spot` インスタンスで 1 つの DB を共有する場合、一方の `Spot` が破棄されると他の `Spot` も DB にアクセスできなくなるという重大なバグが生じていました。これは DI の導入と、GC 時の強制破棄戦略が複雑に絡み合った結果の技術的負債でした。

## Decision Drivers / 要求

- Shared Resource Support: 複数の `Spot` インスタンス間で単一の DB インスタンスを安全に共有できるようにすること。
- Principle of Ownership: リソースを注入した側がそのライフサイクル（クローズ処理）に責任を持つという、DI の原則を徹底すること。
- Stability: バックグラウンドタスクが実行されている最中に、DB コネクションが予期せず閉じられることを防ぐこと。

## Considered Options / 検討

- Option 1: `Spot` が引き続き DB のシャットダウンを担当する。インスタンス共有時にクラッシュするリスクがある。
- Option 2: `Spot` クラスから `db.shutdown()` の呼び出しを削除し、DB を生成・注入した側の責任とする。

## Decision Outcome / 決定

Chosen option: Option 2.

`Spot` クラス内部から `db.shutdown()` の呼び出しを完全に削除します。リソース（DB インスタンス）を生成して注入した側が、そのライフサイクルの全責任を負うという原則を厳格に適用します。

## Consequences / 決定

- Positive:
  - 複数の `Spot` インスタンスでの DB 共有が安全に行えるようになり、関心の分離が明確になった。
  - バックグラウンドタスク実行中に DB が不意に閉じられることによるエラーやデータロストのリスクが解消された。
- Negative:
  - アプリケーション側で DB を適切にクローズし忘れると、コネクションリークが発生する可能性がある。
- Mitigation:
  - ドキュメントにて、カスタム DB を使用する場合のライフサイクル管理の責任について明確に警告し、適切な使用例を示す。

# Delegate Database Lifecycle Management to Caller ## Context and Problem Statement / コンテキスト `Spot` クラスは `TaskDBBase` インスタンスを DI で受け取っているにもかかわらず、シャットダウン処理内で強制的に `db.shutdown()` を呼び出していました。これにより、複数の `Spot` インスタンスで 1 つの DB を共有する場合、一方の `Spot` が破棄されると他の `Spot` も DB にアクセスできなくなるという重大なバグが生じていました。これは DI の導入と、GC 時の強制破棄戦略が複雑に絡み合った結果の技術的負債でした。 ## Decision Drivers / 要求 * **Shared Resource Support**: 複数の `Spot` インスタンス間で単一の DB インスタンスを安全に共有できるようにすること。 * **Principle of Ownership**: リソースを注入した側がそのライフサイクル（クローズ処理）に責任を持つという、DI の原則を徹底すること。 * **Stability**: バックグラウンドタスクが実行されている最中に、DB コネクションが予期せず閉じられることを防ぐこと。 ## Considered Options / 検討 * **Option 1**: `Spot` が引き続き DB のシャットダウンを担当する。インスタンス共有時にクラッシュするリスクがある。 * **Option 2**: `Spot` クラスから `db.shutdown()` の呼び出しを削除し、DB を生成・注入した側の責任とする。 ## Decision Outcome / 決定 Chosen option: **Option 2**. `Spot` クラス内部から `db.shutdown()` の呼び出しを完全に削除します。リソース（DB インスタンス）を生成して注入した側が、そのライフサイクルの全責任を負うという原則を厳格に適用します。 ## Consequences / 決定 * **Positive**: * 複数の `Spot` インスタンスでの DB 共有が安全に行えるようになり、関心の分離が明確になった。 * バックグラウンドタスク実行中に DB が不意に閉じられることによるエラーやデータロストのリスクが解消された。 * **Negative**: * アプリケーション側で DB を適切にクローズし忘れると、コネクションリークが発生する可能性がある。 * **Mitigation**: * ドキュメントにて、カスタム DB を使用する場合のライフサイクル管理の責任について明確に警告し、適切な使用例を示す。

親: REQ003

子: —

### ADR051 {h(g)} ✓ レビュー済 局所ビュー →

# Thread-Safe SQLite Connection Closing and Lock Management

## Context and Problem Statement / コンテキスト

`beautyspot` の `SQLiteTaskDB` は、並行読み取り性能を高めるため、各スレッドごとに専用のコネクションを保持する設計となっています。シャットダウン時には WAL のチェックポイントを妨げないよう、これらの接続を一括で閉じようとしていましたが、以下の深刻な問題がありました。 1. `check_same_thread` の制約: 別スレッドから `close()` を呼ぶとエラーが発生する。 2. クラッシュの危険性: クエリ実行中に強制クローズされるとセグメンテーションフォールトを引き起こす。 3. リカバリ時のデッドロック: エラー発生時の再接続処理で、再帰的なロック取得によりハングする。 4. GC 時のブロック: ロック解放を待機する設計では、GC 時にメインスレッドがフリーズし、ADR-0045 の「GC 時は絶対にメインスレッドをブロックさせない」という原則に違反する。

## Decision Drivers / 要求

- Thread Safety: クエリ実行と接続クローズの競合を防ぎ、プロセスクラッシュを回避すること。
- Liveness: シャットダウンや GC の際、他のスレッドの状態に関わらずメインスレッドをフリーズさせないこと。
- Deadlock Freedom: エラーリカバリなどの再入的な処理においても、ハングアップしないこと。

## Considered Options / 検討

- Option 1: 単純なロックによる同期的なクローズ。デッドロックや GC 時のフリーズのリスクが高い。
- Option 2: 再入可能ロック (`RLock`) とノンブロッキングクローズ (`blocking=False`) を組み合わせたフェイルファストなクリーンアップ。

## Decision Outcome / 決定

Chosen option: Option 2.

相反する要件を解決するため、以下の機構を導入します。

1. リエントラントロック (`threading.RLock`) の導入: 同一スレッド内でのリカバリ処理によるデッドロックを防ぎます。
2. 安全なクローズの許可: `check_same_thread=False` を指定し、別スレッドからのクローズを許可します。
3. ノンブロッキング・クローズ機構 (Fail-fast Close):
4. シャットダウンや GC 時の `close()` 呼び出しは、ロック取得を ノンブロッキング (`blocking=False`) で試行します。
5. 他のスレッドがクエリを実行中でロックを取得できなかった場合、安全を最優先してクローズを諦め、Python の自然な GC 管理に委ねます。

## Consequences / 決定

- Positive:
  - シャットダウン時に可能な限り接続が閉じられ、WAL のクリーンアップが機能する。
  - プロセスクラッシュやリカバリ時のデッドロックのリスクが排除された。
  - GC 時にメインスレッドがブロックされず、システムの安定性が向上した。
- Trade-off:
  - 他のスレッドがクエリ実行中の場合、コネクションの切断が Python の GC タイミングまで遅延する可能性がある。

# Thread-Safe SQLite Connection Closing and Lock Management ## Context and Problem Statement / コンテキスト `beautyspot` の `SQLiteTaskDB` は、並行読み取り性能を高めるため、各スレッドごとに専用のコネクションを保持する設計となっています。シャットダウン時には WAL のチェックポイントを妨げないよう、これらの接続を一括で閉じようとしていましたが、以下の深刻な問題がありました。 1. **`check_same_thread` の制約**: 別スレッドから `close()` を呼ぶとエラーが発生する。 2. **クラッシュの危険性**: クエリ実行中に強制クローズされるとセグメンテーションフォールトを引き起こす。 3. **リカバリ時のデッドロック**: エラー発生時の再接続処理で、再帰的なロック取得によりハングする。 4. **GC 時のブロック**: ロック解放を待機する設計では、GC 時にメインスレッドがフリーズし、ADR-0045 の「GC 時は絶対にメインスレッドをブロックさせない」という原則に違反する。 ## Decision Drivers / 要求 * **Thread Safety**: クエリ実行と接続クローズの競合を防ぎ、プロセスクラッシュを回避すること。 * **Liveness**: シャットダウンや GC の際、他のスレッドの状態に関わらずメインスレッドをフリーズさせないこと。 * **Deadlock Freedom**: エラーリカバリなどの再入的な処理においても、ハングアップしないこと。 ## Considered Options / 検討 * **Option 1**: 単純なロックによる同期的なクローズ。デッドロックや GC 時のフリーズのリスクが高い。 * **Option 2**: 再入可能ロック (`RLock`) とノンブロッキングクローズ (`blocking=False`) を組み合わせたフェイルファストなクリーンアップ。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 相反する要件を解決するため、以下の機構を導入します。 1. **リエントラントロック (`threading.RLock`) の導入**: 同一スレッド内でのリカバリ処理によるデッドロックを防ぎます。 2. **安全なクローズの許可**: `check_same_thread=False` を指定し、別スレッドからのクローズを許可します。 3. **ノンブロッキング・クローズ機構 (Fail-fast Close)**: - シャットダウンや GC 時の `close()` 呼び出しは、ロック取得を **ノンブロッキング (`blocking=False`)** で試行します。 - 他のスレッドがクエリを実行中でロックを取得できなかった場合、**安全を最優先してクローズを諦め**、Python の自然な GC 管理に委ねます。 ## Consequences / 決定 * **Positive**: * シャットダウン時に可能な限り接続が閉じられ、WAL のクリーンアップが機能する。 * プロセスクラッシュやリカバリ時のデッドロックのリスクが排除された。 * GC 時にメインスレッドがブロックされず、システムの安定性が向上した。 * **Trade-off**: * 他のスレッドがクエリ実行中の場合、コネクションの切断が Python の GC タイミングまで遅延する可能性がある。

親: REQ019

子: —

### ADR052 {h(g)} ✓ レビュー済 局所ビュー →

# SQLite Write Task Cancellation Strategy

## Context and Problem Statement / コンテキスト

`db.py` (`SQLiteTaskDB`) では、バックグラウンドでの書き込みを直列化するために専用の Writer スレッドとキューを使用しています。シャットダウン時 (`shutdown(wait=True)`) には、キューに積まれた全てのタスクの完了を待機しますが、タイムアウトが発生した場合の振る舞いが問題となります。特に、現在実行中 (RUNNING) の書き込みタスクを強制的にキャンセルすべきかどうかの設計判断が求められました。

## Decision Drivers / 要求

- Data Integrity: SQLiteデータベースファイルの破損や不整合を絶対に防ぐこと。
- Predictability: 実行中のトランザクションが予測不能な状態で中断されないこと。
- Responsiveness: シャットダウン処理が完全にフリーズしないこと（タイムアウトの尊重）。

## Considered Options / 検討

- Option 1: タイムアウト時に、実行中のタスクであってもスレッドを強制終了、または接続を強制的に閉じてキャンセルする。
- Option 2: PENDING (待機中) のタスクは破棄するが、RUNNING (実行中) のタスクは絶対にキャンセルせず、完遂を待つ（警告ログのみ出力）。

## Decision Outcome / 決定

Chosen option: Option 2.

実行中の書き込みタスク (RUNNING) は、タイムアウト時間を超過した場合でも強制的にキャンセルしない設計としました。

### Rationale / 理由

SQLite のトランザクション実行中にスレッドを強制終了させたり、非同期的に接続を閉じたりすると、データベースファイルが破損する（Corruption）、あるいはWALファイルが不適切な状態になるリスクがあります。

キューの処理ロジックにおいて、すでにキューから取り出され実行状態に入ったタスクは「不可分なアトミック操作」と見なします。シャットダウンのタイムアウトは「新たなタスクの取り出し」を停止するためには機能しますが、実行中のI/O処理を途中で引き裂くことはしません。これにより、システムの安全性とデータ一貫性を最優先しています。

## Consequences / 決定

- Positive:
- SQLite データベースの破損リスクを完全に排除できる。
- トランザクションの整合性が保証される。
- Negative:
- 実行中のタスクがI/Oレベルで極端にスタック（OS起因など）した場合、タイムアウト後もプロセスの終了が遅延する可能性がある。

# SQLite Write Task Cancellation Strategy ## Context and Problem Statement / コンテキスト `db.py` (`SQLiteTaskDB`) では、バックグラウンドでの書き込みを直列化するために専用の Writer スレッドとキューを使用しています。シャットダウン時 (`shutdown(wait=True)`) には、キューに積まれた全てのタスクの完了を待機しますが、タイムアウトが発生した場合の振る舞いが問題となります。特に、現在実行中 (RUNNING) の書き込みタスクを強制的にキャンセルすべきかどうかの設計判断が求められました。 ## Decision Drivers / 要求 * **Data Integrity**: SQLiteデータベースファイルの破損や不整合を絶対に防ぐこと。 * **Predictability**: 実行中のトランザクションが予測不能な状態で中断されないこと。 * **Responsiveness**: シャットダウン処理が完全にフリーズしないこと（タイムアウトの尊重）。 ## Considered Options / 検討 * **Option 1**: タイムアウト時に、実行中のタスクであってもスレッドを強制終了、または接続を強制的に閉じてキャンセルする。 * **Option 2**: PENDING (待機中) のタスクは破棄するが、RUNNING (実行中) のタスクは絶対にキャンセルせず、完遂を待つ（警告ログのみ出力）。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 実行中の書き込みタスク (RUNNING) は、タイムアウト時間を超過した場合でも強制的にキャンセルしない設計としました。 ### Rationale / 理由 SQLite のトランザクション実行中にスレッドを強制終了させたり、非同期的に接続を閉じたりすると、データベースファイルが破損する（Corruption）、あるいはWALファイルが不適切な状態になるリスクがあります。 キューの処理ロジックにおいて、すでにキューから取り出され実行状態に入ったタスクは「不可分なアトミック操作」と見なします。シャットダウンのタイムアウトは「新たなタスクの取り出し」を停止するためには機能しますが、実行中のI/O処理を途中で引き裂くことはしません。これにより、システムの安全性とデータ一貫性を最優先しています。 ## Consequences / 決定 * **Positive**: * SQLite データベースの破損リスクを完全に排除できる。 * トランザクションの整合性が保証される。 * **Negative**: * 実行中のタスクがI/Oレベルで極端にスタック（OS起因など）した場合、タイムアウト後もプロセスの終了が遅延する可能性がある。

親: REQ019

子: —

### ADR053 {h(g)} ✓ レビュー済 局所ビュー →

# Background Loop and Executor Shutdown Sequence

## Context and Problem Statement / コンテキスト

`beautyspot` では、バックグラウンドでのI/Oタスク処理に専用のスレッド（`_BackgroundLoop`）と、ブロッキング操作を委譲する `ThreadPoolExecutor` の2つを組み合わせて使用しています。 `Spot.shutdown(save_sync=False)` またはプロセスのシャットダウン時に、これら2つのリソースを破棄する順序やタイミングの不整合が問題となりました。

具体的には、バックグラウンドループ内の非同期タスクが `run_in_executor` に依存している状態（I/O待ち）で、先に `Executor` が `cancel_futures=True` で強制終了されると、タスク側で予期せぬ `CancelledError` や `RuntimeError` が発生し、クラッシュやログのノイズ、正常な後処理（一時ファイルの削除やDB切断など）の阻害を引き起こすリスクがありました。

## Decision Drivers / 要求

- Graceful Degradation: 強制終了時（`save_sync=False`）であっても、システムがクラッシュせず、残存タスクが安全に破棄されること。
- Correct Sequencing: リソースの依存関係（Loop -> Executor）に基づいた適切なシャットダウン手順の確立。
- Observability: 意図的なキャンセルによるタスクのドロップが、ユーザーに適切に通知されること（エラーの握りつぶしを防ぐ）。

## Considered Options / 検討

- Option 1: Executorのシャットダウンを常にLoopの完全な終了（タイムアウトなし）まで遅延させる。 -> シャットダウンがブロックし続けるリスクがあり、フェイルファストの要件を満たせない。
- Option 2: LoopとExecutorのシャットダウン順序は維持しつつ、`run_in_executor` を呼び出すバックグラウンドタスク内で `CancelledError` および `RuntimeError` を明示的に捕捉・ハンドリングする。

## Decision Outcome / 決定

Chosen option: Option 2.

シャットダウン手順の順序自体（Loopの停止 -> Executorの停止）は論理的に正しいものの、強制停止（`save_sync=False` または Loop のドレインタイムアウト）時には、Executor内のタスクがキャンセルされることを前提とした防御的プログラミングを採用します。

具体的には、`_save_result_async` 内で `run_in_executor` を呼び出す際に `try...except (asyncio.CancelledError, RuntimeError)` ブロックを設け、シャットダウンによる強制キャンセルを安全に捕捉します。捕捉した場合は、`on_background_error` コールバックを呼び出すか、適切に警告を記録することで、クラッシュを防ぎつつ通知を行います。

## Consequences / 決定

- Positive:
- 強制シャットダウン時にエラーによるクラッシュや未処理例外のログ洪水が発生しなくなる。
- バックグラウンドタスクが安全に中断され、リソースリークを防ぐことができる。
- Negative:
- 強制キャンセルされた保存タスクは破棄されるため、データロストの事実がログ（Warning）にのみ残る。これは `save_sync=False` による明示的な要求動作として許容される。

# Background Loop and Executor Shutdown Sequence ## Context and Problem Statement / コンテキスト `beautyspot` では、バックグラウンドでのI/Oタスク処理に専用のスレッド（`_BackgroundLoop`）と、ブロッキング操作を委譲する `ThreadPoolExecutor` の2つを組み合わせて使用しています。 `Spot.shutdown(save_sync=False)` またはプロセスのシャットダウン時に、これら2つのリソースを破棄する順序やタイミングの不整合が問題となりました。 具体的には、バックグラウンドループ内の非同期タスクが `run_in_executor` に依存している状態（I/O待ち）で、先に `Executor` が `cancel_futures=True` で強制終了されると、タスク側で予期せぬ `CancelledError` や `RuntimeError` が発生し、クラッシュやログのノイズ、正常な後処理（一時ファイルの削除やDB切断など）の阻害を引き起こすリスクがありました。 ## Decision Drivers / 要求 * **Graceful Degradation**: 強制終了時（`save_sync=False`）であっても、システムがクラッシュせず、残存タスクが安全に破棄されること。 * **Correct Sequencing**: リソースの依存関係（Loop -> Executor）に基づいた適切なシャットダウン手順の確立。 * **Observability**: 意図的なキャンセルによるタスクのドロップが、ユーザーに適切に通知されること（エラーの握りつぶしを防ぐ）。 ## Considered Options / 検討 * **Option 1**: Executorのシャットダウンを常にLoopの完全な終了（タイムアウトなし）まで遅延させる。 -> シャットダウンがブロックし続けるリスクがあり、フェイルファストの要件を満たせない。 * **Option 2**: LoopとExecutorのシャットダウン順序は維持しつつ、`run_in_executor` を呼び出すバックグラウンドタスク内で `CancelledError` および `RuntimeError` を明示的に捕捉・ハンドリングする。 ## Decision Outcome / 決定 Chosen option: **Option 2**. シャットダウン手順の順序自体（Loopの停止 -> Executorの停止）は論理的に正しいものの、強制停止（`save_sync=False` または Loop のドレインタイムアウト）時には、Executor内のタスクがキャンセルされることを前提とした防御的プログラミングを採用します。 具体的には、`_save_result_async` 内で `run_in_executor` を呼び出す際に `try...except (asyncio.CancelledError, RuntimeError)` ブロックを設け、シャットダウンによる強制キャンセルを安全に捕捉します。捕捉した場合は、`on_background_error` コールバックを呼び出すか、適切に警告を記録することで、クラッシュを防ぎつつ通知を行います。 ## Consequences / 決定 * **Positive**: * 強制シャットダウン時にエラーによるクラッシュや未処理例外のログ洪水が発生しなくなる。 * バックグラウンドタスクが安全に中断され、リソースリークを防ぐことができる。 * **Negative**: * 強制キャンセルされた保存タスクは破棄されるため、データロストの事実がログ（Warning）にのみ残る。これは `save_sync=False` による明示的な要求動作として許容される。

親: REQ006

子: —

### ADR001 {h(g)} ✓ レビュー済 局所ビュー →

# Manage Executor Lifecycle via Instance Ownership and Weak References

## Context and Problem Statement / コンテキスト

`src/beautyspot/core.py` において、非同期タスクのIOオフロード用に `ThreadPoolExecutor` がグローバル変数 `_io_executor` として定義されている。

この実装には以下の課題がある：

1. リソース制御の欠如: スレッド数（`max_workers=4`）がハードコードされており、ユーザーが実行環境（AWS Lambda、強力なサーバー等）に合わせて調整できない。
2. ゾンビプロセス: プロセス終了時に明示的なシャットダウンが行われないため、環境によってはゾンビプロセス化するリスクがある。
3. テスト困難性: グローバル変数はモック化が難しく、ユニットテストの分離を妨げる。

代替案として `with` 文（Context Manager）による管理も検討されたが、`beautyspot` の「デコレータを付与するだけで動作する」という簡易な利用体験（DX）を損なうため、採用には至らなかった。ユーザーコードを変更させずに、安全にリソースをクリーンアップする仕組みが必要である。

## Decision Drivers / 要求

- Developer Experience (DX): ユーザーに `shutdown()` の呼び出しや `with` 文を強制したくない。
- Configurability: スレッド数や Executor の実装をユーザーが制御できるようにしたい。
- Safety: プロセス終了時やインスタンス破棄時に、確実にスレッドプールを閉じたい。
- Memory Safety: クリーンアップ処理の登録によって、メモリリーク（循環参照）を引き起こしてはならない。

## Considered Options / 検討

- Option 1: 現状維持（グローバル変数のまま）。
- Option 2: `Project` を Context Manager 化し、`__exit__` で `shutdown()` を呼ぶ。
- Option 3: `atexit` モジュールを使い、終了時に `shutdown()` を呼ぶ。
- Option 4: インスタンス管理に変更し、`weakref.finalize` で自動クリーンアップを行う。

## Decision Outcome / 決定

Chosen option: Option 4.

`Project` クラスの設計を以下のように変更する：

1. Executorのインスタンス化: グローバル変数を廃止し、`Project` インスタンスごとに `ThreadPoolExecutor` を保持する。
2. Dependency Injection (DI): コンストラクタで外部 `Executor` の注入を許可する。
3. 所有権と責任の分離:
   - 外部注入 (`executor` 引数あり): `Project` はそれを借用するのみ。シャットダウンの責任はユーザー（呼び出し元）にあるため、自動クリーンアップは行わない。
   - 内部生成 (`executor` 引数なし): `Project` がライフサイクルを管理する責任を持つ。
4. 自動クリーンアップ: 内部生成した場合に限り、`weakref.finalize` を使用してシャットダウンを自動化する。

### Technical Details / 技術詳細: Why `weakref.finalize` instead of `atexit`?

単純な `atexit.register(self.shutdown)` を採用しなかった理由は、メモリリーク（循環参照）のリスク である。

- 問題点: `atexit` レジストリにインスタンスメソッド（`self.shutdown`）を登録すると、`atexit` が `self` への強参照（Strong Reference）を保持し続ける。これにより、アプリケーション実行中に `Project` インスタンスが不要になってもガベージコレクション（GC）されず、メモリリークを引き起こす。これを防ぐには `atexit.unregister` が必要になるが、管理が複雑になる。
- 解決策: `weakref.finalize(self, func, *args)` を採用する。
  - `self` がGCされた時点で、即座に `func`（クリーンアップ処理）が実行される。
  - プログラム終了時まで `self` が生存していた場合も、`atexit` 相当のタイミングで `func` が実行される。
  - 重要: クリーンアップ関数 `_shutdown_executor` は `staticmethod` とし、引数には `self` ではなく `self.executor` のみを渡す。これにより、ファイナライザが `self` を参照し続けて寿命を延ばしてしまう事故を防ぐ。

    # Implementation Sketch
    class Project:
        def __init__(self, ...):
            # ...
            self.executor = ThreadPoolExecutor(...)
            # self を参照しないよう、executor オブジェクトだけを渡す
            self._finalizer = weakref.finalize(self, self._shutdown_executor, self.executor)

        @staticmethod
        def _shutdown_executor(executor):
            executor.shutdown(wait=True)

## Consequences / 決定

- Positive:
  - ユーザーはリソース管理を意識する必要がなくなり、APIもシンプルに保たれる。
  - `Project` インスタンスがGCされたタイミングでスレッドプールも回収されるため、リソース効率が良い。
  - DIにより、テスト時にモックExecutorを差し込むことが容易になる。
- Negative:
  - `core.py` の実装において、GCの挙動と `weakref` の仕様を理解したコーディングが必要になり、内部的な複雑性が若干増す。
  - `Project` インスタンスを大量に生成・破棄するような特殊な使い方をした場合、スレッドプールの生成コストがオーバーヘッドになる可能性がある（ドキュメントでシングルトン的な利用を推奨することで緩和する）。

# Manage Executor Lifecycle via Instance Ownership and Weak References ## Context and Problem Statement / コンテキスト `src/beautyspot/core.py` において、非同期タスクのIOオフロード用に `ThreadPoolExecutor` がグローバル変数 `_io_executor` として定義されている。 この実装には以下の課題がある： 1. **リソース制御の欠如**: スレッド数（`max_workers=4`）がハードコードされており、ユーザーが実行環境（AWS Lambda、強力なサーバー等）に合わせて調整できない。 2. **ゾンビプロセス**: プロセス終了時に明示的なシャットダウンが行われないため、環境によってはゾンビプロセス化するリスクがある。 3. **テスト困難性**: グローバル変数はモック化が難しく、ユニットテストの分離を妨げる。 代替案として `with` 文（Context Manager）による管理も検討されたが、`beautyspot` の「デコレータを付与するだけで動作する」という簡易な利用体験（DX）を損なうため、採用には至らなかった。ユーザーコードを変更させずに、安全にリソースをクリーンアップする仕組みが必要である。 ## Decision Drivers / 要求 * **Developer Experience (DX)**: ユーザーに `shutdown()` の呼び出しや `with` 文を強制したくない。 * **Configurability**: スレッド数や Executor の実装をユーザーが制御できるようにしたい。 * **Safety**: プロセス終了時やインスタンス破棄時に、確実にスレッドプールを閉じたい。 * **Memory Safety**: クリーンアップ処理の登録によって、メモリリーク（循環参照）を引き起こしてはならない。 ## Considered Options / 検討 * **Option 1**: 現状維持（グローバル変数のまま）。 * **Option 2**: `Project` を Context Manager 化し、`__exit__` で `shutdown()` を呼ぶ。 * **Option 3**: `atexit` モジュールを使い、終了時に `shutdown()` を呼ぶ。 * **Option 4**: インスタンス管理に変更し、`weakref.finalize` で自動クリーンアップを行う。 ## Decision Outcome / 決定 Chosen option: **Option 4**. `Project` クラスの設計を以下のように変更する： 1. **Executorのインスタンス化**: グローバル変数を廃止し、`Project` インスタンスごとに `ThreadPoolExecutor` を保持する。 2. **Dependency Injection (DI)**: コンストラクタで外部 `Executor` の注入を許可する。 3. **所有権と責任の分離**: * **外部注入 (`executor` 引数あり)**: `Project` はそれを借用するのみ。シャットダウンの責任はユーザー（呼び出し元）にあるため、自動クリーンアップは行わない。 * **内部生成 (`executor` 引数なし)**: `Project` がライフサイクルを管理する責任を持つ。 4. **自動クリーンアップ**: 内部生成した場合に限り、`weakref.finalize` を使用してシャットダウンを自動化する。 ### Technical Details / 技術詳細: Why `weakref.finalize` instead of `atexit`? 単純な `atexit.register(self.shutdown)` を採用しなかった理由は、**メモリリーク（循環参照）のリスク** である。 * **問題点**: `atexit` レジストリにインスタンスメソッド（`self.shutdown`）を登録すると、`atexit` が `self` への強参照（Strong Reference）を保持し続ける。これにより、アプリケーション実行中に `Project` インスタンスが不要になってもガベージコレクション（GC）されず、メモリリークを引き起こす。これを防ぐには `atexit.unregister` が必要になるが、管理が複雑になる。 * **解決策**: `weakref.finalize(self, func, *args)` を採用する。 * `self` がGCされた時点で、即座に `func`（クリーンアップ処理）が実行される。 * プログラム終了時まで `self` が生存していた場合も、`atexit` 相当のタイミングで `func` が実行される。 * **重要**: クリーンアップ関数 `_shutdown_executor` は `staticmethod` とし、引数には `self` ではなく `self.executor` のみを渡す。これにより、ファイナライザが `self` を参照し続けて寿命を延ばしてしまう事故を防ぐ。 ```python # Implementation Sketch class Project: def __init__(self, ...): # ... self.executor = ThreadPoolExecutor(...) # self を参照しないよう、executor オブジェクトだけを渡す self._finalizer = weakref.finalize(self, self._shutdown_executor, self.executor) @staticmethod def _shutdown_executor(executor): executor.shutdown(wait=True) ``` ## Consequences / 決定 * **Positive**: * ユーザーはリソース管理を意識する必要がなくなり、APIもシンプルに保たれる。 * `Project` インスタンスがGCされたタイミングでスレッドプールも回収されるため、リソース効率が良い。 * DIにより、テスト時にモックExecutorを差し込むことが容易になる。 * **Negative**: * `core.py` の実装において、GCの挙動と `weakref` の仕様を理解したコーディングが必要になり、内部的な複雑性が若干増す。 * `Project` インスタンスを大量に生成・破棄するような特殊な使い方をした場合、スレッドプールの生成コストがオーバーヘッドになる可能性がある（ドキュメントでシングルトン的な利用を推奨することで緩和する）。

親: REQ006

子: —