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

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

生成日時: 2026-03-09 23:19:54

### 対象アイテム

11

### REQ

1

### ARCH

1

### SPEC

3

### TST

3

### IMPL

3

### レビュー済

11/11

### Suspect

0

### グループ

KEY

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

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

グループ | REQ | ARCH | SPEC | TST | IMPL
--- | --- | --- | --- | --- | ---
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC004 ✓ ## インターフェース ```python class KeyGen: @staticmethod def _default(args: tu... | TST004 ✓ ## 目的 キャッシュキーはキャッシュの同一性判定の基盤であり、ハッシュが不安定だとキャッシュミスの頻発（性能劣化）や誤ヒット（データ不整合）を引き起こす。P... | IMPL004 ✓ ## 実装概要 `KeyGen` クラスの静的メソッド群がキャッシュキー生成を担う。 `_default(args, kwargs)` が主要なエントリポイン...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC005 ✓ ## インターフェース ```python @singledispatch def canonicalize(obj: Any) -> Any: ... ``... | TST005 ✓ ## 目的 `canonicalize()` は引数の型ごとに正規化ルールを適用し、論理的に同値な入力が同じキャッシュキーを生成することを保証する。正規化の不... | IMPL005 ✓ ## 実装概要 `canonicalize()` は `functools.singledispatch` で実装された再帰的正規化関数。 型ごとにディスパッ...
KEY | REQ002 ✓ 関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。 | ARCH002 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy... | SPEC006 ✓ ## インターフェース ```python class Strategy(Enum): DEFAULT = auto() # 再帰的正規化 ... | TST006 ✓ ## 目的 `KeyGenPolicy` は引数ごとのキー生成戦略をカスタマイズする機構であり、ログレベルやデバッグフラグのような非決定的引数をキーから除外し... | IMPL006 ✓ ## 実装概要 `Strategy` enum（DEFAULT, IGNORE, FILE_CONTENT, PATH_STAT の4種）と `KeyGenP...

## カバレッジ（局所）

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

## アイテム詳細

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

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

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

親: —

子: ARCH002

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

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

### TST004 TST {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 変更でキーが変わること

親: SPEC004

子: —

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

親: SPEC005

子: —

### TST006 TST {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"))` のようにデコレータ経由で戦略が正しく適用されること

親: SPEC006

子: —

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

子: —