Quand grep gagne contre SQL
Sur un petit corpus de mémoire atomique gouvernée — 558 claims du canary Verdara Cycle, schéma SvL Memory V2 — trois retrievers s'affrontent sans reranker, sans embedding, sur 28 questions auto-vérifiables. Le résultat est net : grep file-system > grep MCP > SQL exact. Et l'écart se creuse partout — précision, latence, coût, robustesse.
Le décor
SvL Memory V2 est un schéma de mémoire gouvernée pour agents IA. Un schéma Bootstrap minimal, doctrinaire, qui sépare strictement les croyances (claims atomiques en L2) des événements d'évidence (L1 append-only). Le canary Verdara Cycle — une SAS d'économie circulaire que je gère — sert de banc d'essai depuis trois semaines, et porte aujourd'hui 558 claims actifs en granularité G1 fine : une propriété, un claim.
La question pratique : quand un agent veut retrouver un fait dans cette mémoire, quelle surface lui sert-on ? Trois options s'imposaient naturellement.
R1 — SQL exact
La surface historique : svl memory query --entity-id X --predicate Y. Filtre exact sur les colonnes indexées. Retourne le claim entier avec son audit (policy_bundle, temporal_envelope, hashes RFC 8785). C'est ce qui existe en production.
R2 — grep MCP
Un serveur MCP minimal (memory_grep) qui expose ILIKE/regex sur entity_id ∪ predicate ∪ value. Retour JSON inline. Read-only, hors path gouverné Phase 3. Pensé comme miroir du inline grep du papier arXiv 2605.15184.
R3 — file-grep
Une projection plate du canary sous ~/svl-memory/canary/<entity_id>/<predicate>.md — un fichier markdown par fait, valeur littérale dans le corps. L'agent dispose de grep, ls, find, cat. C'est l'option file-programmatic du papier, en version atomique. La projection est en lecture seule pour le bench (snapshot pris au début, jamais modifié pendant les 84 calls).
La méthode
Un modèle unique : claude-sonnet-4-6. Un harness unique : Claude Code en mode --print --no-bare, qui charge la configuration utilisateur réelle (CLAUDE.md, skills gstack, hooks). 28 questions générées déterministiquement depuis le corpus avec un seed fixe, en quatre catégories miroir LongMemEval :
- S1 single_claim — lookup d'un attribut : « Quelle est la raison sociale de Café Aurora ? » → « Tierra Maestra »
- S2 multi_claim_entity — agrégation sur un root : « Liste les valeurs documentées pour Pueblo Mex »
- S3 cross_entity — pont label → entité : « En lien avec Verdara Cycle, quelle est la date de signature du contrat entre Café Aurora et PedalGreen ? »
- S4 temporal_supersede — fait historique daté : « Quelle est la date d'effet du contrat de Vinia Negra ? »
Chaque question est posée à chaque retriever — 84 appels au total, environ 50 minutes wall-clock, $13.76 d'équivalent pay-per-token (gratuit sur abonnement Pro/Max). Chaque retriever ne voit que ses propres outils, isolé par --allowed-tools et --strict-mcp-config.
Les résultats
| Catégorie | n | R1 SQL | R2 grep MCP | R3 file-grep |
|---|---|---|---|---|
| S1 single_claim | 10 | 9 | 10 | 10 |
| S2 multi_claim_entity † | 8 | 0 | 0 | 0 |
| S3 cross_entity | 4 | 3 | 3 | 4 |
| S4 temporal_supersede | 6 | 4 | 6 | 6 |
| Global exact-match | 28 | 16 (57 %) | 19 (68 %) | 20 (71 %) |
| Global substring | 28 | 16 (57 %) | 20 (71 %) | 22 (79 %) |
| Erreurs runtime | — | 5 | 0 | 0 |
† Le 0/8 sur S2 ne reflète pas un échec : les trois retrievers ont retrouvé les claims du root demandé (recall 79–89 %), mais dans un ordre différent et en incluant des items hors filtre du générateur (statuts commerciaux, SIREN, marques). L'exact-match strict sur une liste comma-separated est trop fragile pour cette catégorie.
Quatre catégories, quatre histoires
S1 — la facilité du lookup littéral
G1 fine produit des valeurs courtes et propres : « Tierra Maestra », « 28005 », « Huile à barbe ». Les grep retrievers tapent 10/10 sans effort. R1 SQL en rate une seule, par budget dépassé sur un output volumineux.
S2 — le piège de la métrique de listing
Quand on demande « toutes les valeurs documentées pour Pueblo Mex », les retrievers retournent davantage que ce que mon générateur avait listé comme gold set. R3 récupère les statuts commerciaux et les marques. R2 fait pareil. R1 SQL filtre plus prudemment mais accumule trois erreurs runtime. La conclusion est nuancée : les grep retrievers sur-recall (89 % vs 79 %), avec une précision plus basse — un compromis acceptable pour un agent qui veut tout voir.
S3 — le bridging label → entité
« En lien avec Verdara Cycle, quelle est la date de signature du contrat entre Café Aurora et PedalGreen ? » Cette question oblige le modèle à parcourir deux entités, et R3 file-grep est le seul à passer 4/4. Pourquoi ? Parce que ls ~/svl-memory/canary/ | grep aurora rend visible la structure entity_id, là où R2 doit deviner que field='entity_id' est la bonne option à passer.
S4 — le temporel
Dates de signature, dates d'effet, immatriculations. R2 et R3 cartonnent 6/6. R1 SQL en rate deux — une fois par mauvaise convention d'entity_id devinée, une fois par timeout. Les claims temporels ont des valeurs littérales (2024-03-22, 2025-12-16) tellement spécifiques qu'un simple grep ramène la réponse en un coup.
Pourquoi grep gagne
Une surface qui exige un identifiant exact perd au profit d'une surface qui accepte une approximation textuelle. C'est l'enseignement transversal du papier arXiv 2605.15184, et il se transpose en petit corpus G1 fine.
R1 SQL n'est pas un retriever équitable, à la réflexion. svl memory query --entity-id cafe_aurora_raison_sociale exige que le modèle devine la convention de nommage canonique depuis le label naturel « Café Aurora ». C'est un concours de devinette — pas un retrieval. Quand il rate (S4-02, S4-06), il ne rate pas le claim, il rate l'entity_id.
R2 et R3 lèvent cette contrainte. R2 grep peut chercher autour du label (le nouveau paramètre field='any' ajouté en cours de bench cherche dans entity_id ∪ predicate ∪ value). R3 expose la structure même : ls liste les dossiers entité, grep trouve la ligne.
Le mode de livraison — JSON inline pour R2, lecture filesystem pour R3 — n'est pas le facteur dominant sur Sonnet 4.6 (68 % vs 71 % exact). L'inversion observée par les auteurs du papier sur Haiku 4.5 / Chronos n'a pas d'analogue ici. Un seul modèle, un seul harness — c'est un sujet ouvert.
Robustesse runtime
R1 SQL accumule 5 erreurs sur 28 : quatre exit=1 et un timeout. Origine probable : le payload JSON d'un query_claims contient policy_bundle, authority_policy_pinning, temporal_envelope, plusieurs KB par claim. Sur des questions impliquant plusieurs claims (S2 listing), le modèle tente plusieurs requêtes pour explorer, et le budget par appel saute.
R2 et R3 : zéro erreur. Les retours grep sont compacts — une ligne par claim, pas d'enveloppe. C'est un avantage opérationnel qui pèse autant que la précision pour un agent en production.
La projection markdown comme substrate dual
R3 gagne le bench non pas parce que grep est magique, mais parce que la projection markdown filesystem encode la structure d'une façon que les outils Unix standard exploitent directement. Le path <entity_id>/<predicate>.md est l'index : ls <root>/ énumère les attributs, find -name '*aurora*' localise par label, cat lit la valeur, grep -r scanne le contenu. Aucun de ces outils n'a été conçu pour la mémoire d'agent — ils marchent parce que le modèle connaît leur sémantique parfaitement, depuis des décennies de présence dans tous les corpus d'entraînement.
L'intérêt second — qui dépasse le bench — est que la même projection est lisible par un humain. Une projection, deux consommateurs : l'agent retrieves, l'humain navigate.
Sur l'instance de test, un viewer markdown self-hosted (mdxg-web.service sur tailnet) sert les .md en HTML rendu avec navigation par dossiers entité. Un opérateur qui veut comprendre l'état du canary ouvre l'arborescence dans son navigateur ; un agent qui veut récupérer un fait grep le même fichier. Cela résout en pratique le compromis classique entre « structure machine-friendly » (SQL/JSON) et « structure human-friendly » (markdown/wiki). La SvL Memory V2 gouvernée (L1+L2) reste la source de vérité auditée, la projection .md en est la dérivation cosmétique mais fonctionnellement complète.
Related work — MDXG. Le projet vercel-labs/mdxg (Markdown Experience Guidelines) formalise précisément ce besoin du côté humain : il définit une spec de présentation pour markdown (pages virtuelles via headings, navigation, search, thèmes, modes preview/source). Le constat de motivation est strictement le nôtre : « markdown is the primary interface between humans and AI agents. […] Markdown is already ideal for agents: low token cost, simple to generate, trivial to parse. The missing piece is the human side. » MDXG fournit le standard humain, R3 fournit la validation empirique côté agent — les deux se composent. Tout fichier .md que produit export_projection_tree.py est éligible à un rendu MDXG-conformant : un viewer comme @mdxg/vscode ouvert sur ~/svl-memory/canary/ donnerait à l'opérateur la navigation polished que MDXG spécifie, sans rien changer aux fichiers que l'agent grep en parallèle.
Conséquence doctrinale : la projection ne stocke rien d'autoritaire, elle reflète la mémoire L2. C'est une projection L4 au sens de la doctrine — non gouvernée en écriture, mais utilisable en lecture par deux audiences. Sa fraîcheur dépend du script d'export ; un export_projection_tree.py lancé en hook post-write maintiendrait la synchro temps réel (non testé dans cette étude — la projection est snapshot à T0).
Ce que ça change
Trois enseignements transverses, qui dépassent le cas SvL :
- L'audit n'est pas une surface de lecture. Le path gouverné (R1) renvoie l'audit complet à chaque appel. Pour un retriever, c'est du bruit qui coûte cher. Séparer la lecture compacte de l'audit complet est un design pattern à graver — l'agent qui veut savoir lit court, l'agent qui doit prouver lit long.
- L'entity_id est canonique mais opaque. Une convention de nommage interne (
<root>_<suffix>) est bonne pour l'indexation, mauvaise pour le retrieval. Si la surface exige cette convention en entrée, elle force le modèle à la deviner. Solution : exposer la convention par recherche (grep sur entity_id) plutôt que par lookup. - Le filesystem est sous-estimé comme couche de retrieval. 89 LOC pour générer une projection
.md, et soudain Sonnet 4.6 dispose degrep / ls / find / cat— outils que tous les modèles savent manier parfaitement. C'est un retriever quasi-gratuit qui domine 6/8 questions difficiles.
Limites
- N=1 modèle, N=1 harness. Pas de comparaison Opus / GPT / Gemini, pas de variation Chronos vs Claude Code. C'est un mini-bench intra-config.
- N=28 questions. Pouvoir statistique limité, surtout sur cat S3 (n=4).
- Harness non-stock. Le mode
--no-barecharge mon CLAUDE.md global et mes skills. La mesure est « Claude Code tel que Guillaume l'a configuré », pas « Claude Code par défaut ». Une réexécution--bareavec ANTHROPIC_API_KEY isolerait ce facteur. - R2 a évolué pendant le bench. Le paramètre
field='any'a été ajouté après un premier smoke test où R2 ne cherchait que dansvalue::text. Sans ce fix, R2 aurait échoué structurellement sur toutes les lookups d'attribut. La version finale de R2 et R3 sont maintenant équitables. - R1 n'est pas un retriever neutre. Il exige l'entity_id exact en entrée. Il mesure conjointement la capacité du modèle à deviner la nomenclature ET la capacité de SQL à retourner. Le séparer demanderait une couche de discovery, qui sortirait du périmètre « retriever sans reranker ».
Reproduire
Le bench est entièrement reproductible — code, données, commande. La DB op_demo_canary est gelée au snapshot compliance_hash=sha256:e62623af…. Le script reproduce.sh régénère la projection filesystem, le jeu de questions (seed=42) et lance les 84 appels.
| Artefact | Chemin |
|---|---|
| Paper complet | research/2026-05-retriever-bench/paper.md |
| 28 questions | research/2026-05-retriever-bench/questions.json |
| 84 résultats bruts | research/2026-05-retriever-bench/results.csv |
| Synthèse | research/2026-05-retriever-bench/summary.json |
| Script reproduce | research/2026-05-retriever-bench/reproduce.sh |
| Serveur MCP | src/svl/memory/mcp_server.py |
| Export projection | tools/research/export_projection_tree.py |
| Générateur de questions | tools/research/gen_questions.py |
| Runner bench | tools/research/run_bench.py |
Aller plus loin
Quatre directions ouvertes :
- Multi-modèle. Refaire le bench sur Opus 4.7, Haiku 4.5, GPT-5.4, Gemini 3 pour mesurer l'effet modèle sur ce corpus G1 fine.
- Multi-harness. Ajouter Codex CLI, Cursor, Cline, pour mesurer si l'écart Claude Code vs Chronos du papier original tient sur petit corpus.
- Audit séparé. Implémenter une surface SvL compact-read qui ne ramène pas l'audit dans le payload retriever. Mesurer si R1 reprend la tête.
- Embedding baseline. Ajouter R4
vector searchsur les claims pour mettre les résultats à l'épreuve d'un retriever classique. Probablement R4 perd aussi sur G1 fine — à vérifier.
Le canary Verdara Cycle continue de grossir au fil des sessions d'atomisation (+355 claims en 24 h le 17 mai). Le bench restera reproductible tant que les commits a9868fa → 7e8a812 tiennent — et le bench est, par construction, prêt à être rejoué sur n'importe quelle évolution du corpus.