---
title: 仕組み
description: 署名アルゴリズム、なぜ一致判定が厳密なのか、そして台帳が実際に記録する内容。
sidebar:
  order: 2
---
## 署名

すべての決定は、キャッシュキーとして使用される前に安定した署名へ正規化されます。

```ts
export function signature(decision: Decision): string {
  const normalized = {
    intent: decision.intent,
    params: sortKeysDeep(decision.params),
  };
  return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
}
```

正規化は意図的に浅いものです。オブジェクトキーは再帰的にソートされるため、`{ a: 1, b: 2 }` と `{ b: 2, a: 1 }` は同一のハッシュになりますが、`intent` やパラメータの*値*には一切手を加えません。語幹抽出、大小文字の正規化、意味的な正規化は行いません。

```txt
{ intent: "summarize_pr", params: { repo: "acme/widgets", pr: 482 } }
{ intent: "summarize_pr", params: { pr: 482, repo: "acme/widgets" } }
        │                                       │
        └──────────────── same signature ──────┘

{ intent: "summarize_pr", params: { repo: "acme/widgets", pr: 483 } }
        │
        └── different signature (different pr) ──┘
```

## なぜ意味的一致ではなく完全一致なのか

意味的／あいまいなキャッシュなら、より多くの繰り返しを検出できます。たとえば「この PR を要約して」と「このプルリクエストの要約を教えて」は、どちらもヒットします。しかしその場合、Runic は異なる言い回しの 2 つのリクエストがキャッシュされた回答を共有できるほど近いかどうかを判断しなければなりません。これはまさに、Runic が行わないよう設計されている種類の非決定的な判断です。

完全一致であれば、キャッシュヒットはおそらく同じ決定なのではなく、証明可能なほど同じ決定です。その代わり、呼び出し元は `params` に何を含めるかを意図的に決める必要があります。詳しくは[クイックスタート](/quickstart#choosing-intent-and-params)を参照してください。

## 台帳が記録する内容

台帳は、ずれが生じうる累積合計ではなく、追記専用のログです。

```ts
interface LedgerEvent {
  signature: string;
  kind: "hit" | "miss";
  tokensSpent: number;
  timestamp: number;
}
```

- **ミス時**、Runic はアーティファクトの生成にコードが報告した `tokensSpent` を記録します。
- **ヒット時**、Runic はその署名に対応する元のミスを検索し、保存されていたものと*同じ* `tokensSpent` 値を記録します。推定値や推測値を使うことはありません。ヒットした署名に対応する以前のミスが何らかの理由で存在しない場合、数値を捏造する代わりに `0` を記録します。

```txt
summary.totalSpent = sum of all miss events
summary.totalSaved = sum of all hit events
tokensWithoutRunic = totalSpent + totalSaved   (what it would've cost with no cache at all)
savingsPercent     = totalSaved / tokensWithoutRunic
```

Runic 自身がトークンコストを測定することはありません。`storeResult(decision, artifact, tokensSpent)` を通じて呼び出し元コードから伝えられた内容をそのまま繰り返すだけです。`openrouter-savings` が推定値ではなく実際の API レスポンスからその数値を取得する方法については、[ベンチマーク](/benchmarks)を参照してください。

## ストレージバックエンド

`@runic-labs/cache` と `@runic-labs/ledger` はどちらも小さなストレージインターフェースに基づいて構築されており、v1 では 2 つの実装が提供されています。

| バックエンド | 有効期間 | ユースケース |
| --- | --- | --- |
| `MemoryCacheStore` / `MemoryLedgerStore` | プロセスの存続期間のみ | テスト、一時的なスクリプト、ベンチマーク |
| `FileCacheStore` / `FileLedgerStore` | ディスクに永続化（デフォルトでは `.runic/`） | サーバーを実行せずに同じ状態を読み取る CLI プロセスとエージェントプロセス |

v1 ではどちらもセッション単位のスコープです。セッション間共有が今すぐ追加するものではなく、意図的に後のフェーズとされている理由については、[スコープ](/scope)を参照してください。

## 古さは助言にすぎません

キャッシュエントリには、設定可能な期間（デフォルトでは 24 時間）から計算される `stale` フラグがあります。Runic が古いエントリを自動的に削除したり拒否したりすることはありません。これは呼び出し元が必要に応じて対応するための情報であり、強制メカニズムではありません。ユースケースで厳格な有効期限が必要な場合は、自分で `entry.stale` を確認し、どう扱うかを決めてください。
