---
title: Comment ça marche
description: >-
  L’algorithme de signature, pourquoi la correspondance est exacte et ce que le
  registre enregistre réellement.
sidebar:
  order: 2
---
## Signatures

Chaque décision est normalisée en une signature stable avant d’être utilisée comme clé de cache :

```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");
}
```

La normalisation est volontairement superficielle — les clés des objets sont triées récursivement afin que `{ a: 1, b: 2 }` et `{ b: 2, a: 1 }` produisent un hash identique, mais rien concernant `intent` ou les *valeurs* des paramètres n’est modifié. Il n’y a ni racinisation, ni normalisation de la casse, ni canonicalisation sémantique.

```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) ──┘
```

## Pourquoi une correspondance exacte, et non sémantique

Un cache sémantique/flou détecterait davantage de répétitions — « résume cette PR » et « donne-moi un résumé de cette pull request » seraient tous deux trouvés. Mais cela signifierait aussi que Runic doit déterminer si deux requêtes formulées différemment sont suffisamment proches pour partager une réponse mise en cache, ce qui est précisément le genre de jugement non déterministe que Runic est conçu pour éviter.

Une correspondance exacte signifie qu’un succès de cache est prouvablement la même décision, et non probablement la même décision. En contrepartie, les appelants doivent choisir délibérément ce qui entre dans `params` — voir [Démarrage rapide](/quickstart#choosing-intent-and-params).

## Ce que le registre enregistre

Le registre est un journal en ajout seul, et non un total cumulé susceptible de dériver :

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

- **Lors d’un échec**, Runic enregistre les `tokensSpent` que votre code a signalés pour produire l’artefact.
- **Lors d’un succès**, Runic recherche l’échec d’origine pour cette signature et enregistre la *même* valeur `tokensSpent` que celle qui a été sauvegardée — jamais une estimation, jamais une supposition. Si, pour une raison quelconque, aucun échec antérieur n’existe pour une signature qui est trouvée, il enregistre `0` plutôt que d’inventer un nombre.

```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 ne mesure jamais lui-même le coût en tokens. Il ne fait que répéter ce que le code appelant lui a indiqué via `storeResult(decision, artifact, tokensSpent)` — voir [Benchmarks](/benchmarks) pour savoir comment `openrouter-savings` obtient ce nombre à partir d’une réponse d’API réelle plutôt que d’une estimation.

## Backends de stockage

`@runic-labs/cache` comme `@runic-labs/ledger` sont construits autour d’une petite interface de stockage, avec deux implémentations livrées dans la v1 :

| Backend | Durée de vie | Cas d’utilisation |
| --- | --- | --- |
| `MemoryCacheStore` / `MemoryLedgerStore` | Durée de vie du processus uniquement | Tests, scripts éphémères, benchmarks |
| `FileCacheStore` / `FileLedgerStore` | Persisté sur le disque (`.runic/` par défaut) | Un processus CLI et un processus d’agent lisant le même état sans serveur en cours d’exécution |

Les deux ont une portée par session dans la v1 — voir [Portée](/scope) pour comprendre pourquoi le partage entre sessions est délibérément une phase ultérieure, plutôt qu’une fonctionnalité ajoutée à la hâte maintenant.

## L’obsolescence est uniquement indicative

Les entrées du cache portent un indicateur `stale`, calculé à partir d’une fenêtre configurable (24 h par défaut). Runic n’évince ni ne refuse jamais automatiquement une entrée obsolète — c’est une information sur laquelle l’appelant peut agir s’il le souhaite, et non un mécanisme d’application. Si votre cas d’utilisation nécessite une expiration stricte, vérifiez vous-même `entry.stale` et décidez quoi en faire.
