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

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

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

### 対象アイテム

17

### REQ

1

### ARCH

1

### SPEC

4

### TST

4

### IMPL

4

### ADR

3

### レビュー済

17/17

### Suspect

0

### グループ

STORE

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

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

グループ | REQ | ARCH | SPEC | TST | IMPL | ADR
--- | --- | --- | --- | --- | --- | ---
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC009 ✓ ## インターフェース ```python class LocalStorage(BlobStorageMaintenable): def __ini... | TST009 ✓ ## 目的 `LocalStorage` はBlobデータのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性... | IMPL009 ✓ ## 実装概要 - `LocalStorage` クラスが `BlobStorageBase` を実装。 ファイルは `{base_dir}/{key}.... | ADR030 ✓ # Declarative Storage Policy ## 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保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが... | IMPL010 ✓ ## 実装概要 `S3Storage` クラスが `BlobStorageBase` を実装。 `s3://bucket/prefix/` 形式の URI を... | 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の肥大化（性能... | IMPL011 ✓ ## 実装概要 `StoragePolicyProtocol` が `should_save_as_blob(data: bytes) -> bool` を定... | 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, サードパーティ）が 準拠すべき抽... | IMPL024 ✓ ## 実装概要 `storage.py` 内の `BlobStorageBase` ABC が SPEC024 の中核実装。 5つの抽象メソッド（`save`... | ADR030 ✓ # Declarative Storage Policy ## 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データのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性... | IMPL009 ✓ ## 実装概要 - `LocalStorage` クラスが `BlobStorageBase` を実装。 ファイルは `{base_dir}/{key}.... | 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データのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性... | IMPL009 ✓ ## 実装概要 - `LocalStorage` クラスが `BlobStorageBase` を実装。 ファイルは `{base_dir}/{key}.... | ADR046 ✓ # Delegate Workspace Initialization to Storage Backends ## Context and Problem ...
STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ（ローカルファイルシステムやS3等）に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC010 ✓ ## インターフェース ```python class S3Storage(BlobStorageMaintenable): def __init__... | TST010 ✓ ## 目的 `S3Storage` はクラウド環境での大規模Blob保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが... | IMPL010 ✓ ## 実装概要 `S3Storage` クラスが `BlobStorageBase` を実装。 `s3://bucket/prefix/` 形式の URI を... | 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保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが... | IMPL010 ✓ ## 実装概要 `S3Storage` クラスが `BlobStorageBase` を実装。 `s3://bucket/prefix/` 形式の URI を... | 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の肥大化（性能... | IMPL011 ✓ ## 実装概要 `StoragePolicyProtocol` が `should_save_as_blob(data: bytes) -> bool` を定... | 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の肥大化（性能... | IMPL011 ✓ ## 実装概要 `StoragePolicyProtocol` が `should_save_as_blob(data: bytes) -> bool` を定... | 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, サードパーティ）が 準拠すべき抽... | IMPL024 ✓ ## 実装概要 `storage.py` 内の `BlobStorageBase` ABC が SPEC024 の中核実装。 5つの抽象メソッド（`save`... | 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, サードパーティ）が 準拠すべき抽... | IMPL024 ✓ ## 実装概要 `storage.py` 内の `BlobStorageBase` ABC が SPEC024 の中核実装。 5つの抽象メソッド（`save`... | ADR046 ✓ # Delegate Workspace Initialization to Storage Backends ## Context and Problem ...

## カバレッジ（局所）

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

## アイテム詳細

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

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

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

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

親: —

子: ADR030, ADR044, ADR046, ARCH004

### ARCH004 ARCH {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

### SPEC009 SPEC {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 SPEC {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 SPEC {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

### SPEC024 SPEC {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

### TST009 TST {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 TST {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 TST {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

子: —

### TST024 TST {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

子: —

### IMPL009 IMPL {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 IMPL {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 IMPL {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

子: —

### IMPL024 IMPL {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

子: —

### ADR030 ADR {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

子: —

### ADR044 ADR {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

子: —

### ADR046 ADR {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

子: —