Skip to main content
Image de couverture pour Tree-sitter et code graph: naviguer dans le code mieux qu'avec grep

Tree-sitter et code graph: naviguer dans le code mieux qu'avec grep

TL;DR
grep renvoie du texte, pas de la structure. Un agent IA dépense des centaines de tokens à départager définitions, imports et faux positifs. Cartog parse le code avec tree-sitter, construit un graph de symboles en SQLite et répond aux requêtes (« où est défini X ? », « qui appelle Y ? ») en microsecondes, sans recharger le code source.
Pour tester Cartog : documentation · code source.

Voyons comment un agent recherche du texte et quel est le lien avec le contexte.

Un agent IA qui explore une codebase utilise grep et cat.

Aussi, lorsqu’il cherche UserService, il récupère ce genre de sortie :

src/auth.py:3:   from services import UserService
src/auth.py:45:  # UserService handles authentication
src/tests/test_auth.py:12: mock_user_service = UserService()
src/logs/config.py:8: logger.info("UserService initialized")

Parmi les 4 résultats , un seul est la définition de UserService.
L’agent ne le sait pas — il doit lire chaque fichier pour comprendre ce qui est pertinent.

Pourquoi parle-t-on de tokens ?
Un agent IA ne « lit » pas un fichier comme un humain. Tout ce qui passe entre lui et le code comme la sortie de grep ou le contenu d’un fichier avec cat est découpé en tokens et facturé au modèle. Ces tokens entrent dans une fenêtre de contexte limitée. Plus l’agent en consomme pour explorer, moins il en reste pour raisonner.
Au-delà d’un certain volume, sa précision se dégrade (phénomène de context rot).

Comptons sur l’exemple ci-dessus.

L’agent fait grep UserService et renvoie 4 lignes de résultat (~80 tokens).
Il ne sait pas laquelle est la bonne, donc il fait cat src/auth.py (~600 tokens)
puis cat src/tests/test_auth.py (~400 tokens)
puis cat src/logs/config.py (~300 tokens) pour disqualifier les faux positifs.

Bilan :

~1 400 tokens pour répondre à « où est définie UserService ? », alors qu’un humain aurait suffi à regarder le préfixe class ou def dans la première sortie.

Pire :

Ce coût se répète à chaque question. « Qui appelle authenticate ? », « De quoi hérite UserService ? », « Que fait verify_token ? »
Chaque question relance un cycle grep → cat → cat → cat. La fenêtre de contexte se remplit de fragments redondants avant même que l’agent commence à coder.

Le problème fondamental : grep cherche des chaînes de caractères, pas des concepts. Il ne distingue pas une définition de classe d’un import, d’un commentaire ou d’un log. L’agent doit reconstruire mentalement la structure du code à partir de fragments textuels — et chaque fragment coûte des tokens.

Pourquoi est-ce un problème maintenant ? Les agents utilisent grep depuis des années.

Parce que le coût a changé. Quand un humain lit 4 fichiers, il filtre mentalement en quelques secondes. Quand un agent lit 4 fichiers, il consomme du contexte — et ce contexte est limité, lent, et coûteux. Ce qui était une friction devient un goulot d’étranglement.

Mais alors, comment une machine peut comprendre comment est structuré du code, sans le lire comme un humain ?

Tree-sitter est un parser incrémental qui produit un arbre syntaxique concret (CST) pour n’importe quel langage.

Là où une regex voit class UserService:, tree-sitter voit :

class_definition
  name: identifier "UserService"
  superclasses: argument_list
    identifier "BaseService"
  body: block
    function_definition
      name: identifier "authenticate"
      parameters: ...
    function_definition
      name: identifier "refresh_token"
      parameters: ...

L’arbre encode la structure :

UserService est une classe, elle hérite de BaseService, elle contient deux méthodes.
Ce n’est plus du texte — c’est de la connaissance structurelle.

Cartog utilise tree-sitter avec des grammaires pour actuellement 8 langages : Python, TypeScript, JavaScript, Rust, Go, Ruby, Java, et Markdown (les documents Markdown sont indexés comme symboles sans arêtes, utile pour la recherche sémantique).
Chaque grammaire produit un CST spécifique au langage, et un extracteur dédié transforme ce CST en symboles et relations normalisés.

Tree-sitter donne un arbre par fichier.

Pour relier ces arbres entre eux (ex: un appel qui traverse trois modules ou une classe qui hérite d’une autre dans un autre paquet) il faut nommer les choses et nommer les liens.

À partir du CST, cartog extrait deux types d’entités :

  • Un symbole, c’est un élément nommé : une fonction, une classe, un import, un type, un enum.
  • Une arête, c’est une relation entre symboles : calls quand une fonction en appelle une autre, inherits quand une classe en étend une autre, raises quand une fonction lève une exception, imports quand un module en référence un autre.

Chaque symbole reçoit un identifiant stable de la forme file_path:kind:qualified_name :

src/auth.py:class:UserService
src/auth.py:method:UserService.authenticate
src/auth.py:function:verify_token

Ce format survit aux déplacements de lignes dans un fichier — seuls un renommage ou un changement de structure invalident l’ID.

C’est ce qui permet de comparer le graph entre deux indexations sans perdre les références existantes.

Au total, cartog reconnaît une dizaine de types de symboles et autant d’arêtes. La liste exhaustive (kinds de symboles, kinds d’arêtes, couverture par langage) vit dans le repo GitHub de cartog — elle évolue à mesure que de nouvelles grammaires sont ajoutées.

Avec un graph de symboles et d’arêtes résolues, des requêtes structurelles deviennent possibles.

Voici le type de navigation que le graph permet :

graph LR
    AuthController -->|appelle| authenticate["UserService.authenticate"]
    authenticate -->|appelle| verify_token
    authenticate -->|appelle| refresh_token
    Admin -->|hérite| UserService
    verify_token -->|lève| TokenExpiredError

Quelques exemples concrets :

cartog refs UserService — Qui utilise UserService ?
Retourne tous les symboles qui ont une arête pointant vers UserService, classés par type (calls, imports, inherits).

cartog callees UserService.authenticate — Qu’est-ce que cette méthode appelle ?
Suit les arêtes calls sortantes pour lister les dépendances directes.

cartog impact verify_token --depth 3 — Qu’est-ce qui casse si je change cette fonction ?
Remonte le graph des appelants sur 3 niveaux de profondeur. L’équivalent avec grep nécessiteraitt de lire manuellement chaque fichier appelant, puis chaque fichier appelant les appelants, etc.

cartog hierarchy BaseService — Quel est l’arbre d’héritage ?
Affiche la hiérarchie complète : classes parentes et enfants.

cartog outline src/auth.py — Structure d’un fichier
Liste tous les symboles d’un fichier avec leur hiérarchie, sans avoir à lire le fichier entier.

Tout ça semble super sur le papier. Mais est-ce que ça change vraiment quelque chose en pratique pour un agent ?

Pour quantifier ces gains, un benchmark compare cartog à grep/cat sur 13 scénarios typiques d’agent IA (recherche de définition, traçage d’appelants, analyse d’impact) exécutés sur 5 langages, dont une fixture Python de 69 fichiers / 4k lignes indexée en 95ms :

Métriquegrep/catcartog
Tokens par requête~1 700~280
Recall78%97%
Latence — lecture (outline, refs)variable8-14 µs
Latence — analyse transitive (impact depth-3)N/A2.7-17 ms

Les gains les plus importants : le traçage de chaînes d’appels (88% de réduction de tokens) et la recherche d’appelants (95% de réduction).

L’agent consomme moins de contexte, obtient des résultats plus pertinents, et les obtient en microsecondes. Il peut poser des questions structurelles sur le code au lieu de le lire ligne par ligne.

Cartog s’appuie sur une stack volontairement minimale, choisie pour tenir en un seul binaire qui tourne sur le poste du développeur :

  • Rust — un binaire unique, ~5 Mo, cross-compilé pour Linux, macOS (x86 + ARM) et Windows. Pas de runtime à installer, pas de JVM, pas de Node.
  • tree-sitter — parsing incrémental, multi-langage, structurel. Actuellement 8 grammaires embarquées (Python, TypeScript, JavaScript, Rust, Go, Ruby, Java, Markdown).
  • SQLite (avec rusqlite bundled) — le graph entier dans un fichier .cartog.db (~1 Mo pour un projet moyen). Mode WAL pour permettre lectures concurrentes — utile quand le watcher et le serveur MCP tournent en parallèle.
  • sqlite-vec + FTS5 — recherche vectorielle (KNN) et plein texte (BM25) intégrées à SQLite, sans serveur externe. Détaillées dans l’article 2.
  • ONNX Runtime (via fastembed) — inférence des embeddings en local, sur CPU. Pas d’API externe, le code ne quitte jamais la machine.
  • rmcp — serveur MCP en stdio pour exposer le graph aux agents.

Les forces qui ressortent de ces choix :

  1. Zéro infrastructure — un binaire, un fichier de base. Pas de Postgres à provisionner, pas de Neo4j à administrer.
  2. 100 % local — aucune donnée envoyée à un service externe. Compatible code propriétaire, NDA, environnements air-gapped.
  3. Latence en microsecondes — le graph est pré-calculé, les requêtes sont des SELECT indexés.
  4. Cross-platform — un seul cargo install cartog couvre les principaux OS.

Le modèle de stockage est extensible : un spec interne décrit déjà un mode S3 (graph partagé entre CI, équipe, machines), avec SQLite local comme cache. Le format reste le même — c’est la couche de distribution qui change.

Curieux du schéma exact (tables, colonnes, index) et des décisions d’architecture ? Tout est documenté sur le repo GitHub de cartog et la page docs.rs.

Les commandes CLI ci-dessus sont la face visible. Pour qu’un agent puisse les utiliser sans bricolage, cartog est distribué comme plugin Claude Code — c’est la voie recommandée : installation en une commande, MCP server préconfiguré, skill agent embarquée. Le plugin expose 12 outils MCP : cartog_search, cartog_refs, cartog_callees, cartog_impact, cartog_hierarchy, cartog_outline, cartog_deps, cartog_changes, cartog_rag_index, cartog_rag_search, cartog_index, cartog_stats.

Pour les autres clients MCP (Cursor, Windsurf, Zed, etc.), cartog serve expose les mêmes outils en stdio. Un skill agent installable séparément via npx skills add jrollin/cartog apprend à l’agent quand utiliser quel outil : quelle recherche router vers cartog_search plutôt que cartog_rag_search, comment enchaîner les outils pour un refactoring, vers quel fallback heuristique se tourner quand un symbole n’est pas trouvé.

On a un graph de symboles. Mais une arête calls → verify_token ne dit pas quel symbole verify_token utiliser.
Il peut en exister plusieurs dans le projet. Le graph est incomplet si on ne résout pas ça.

Quand tree-sitter voit verify_token() dans le corps d’une méthode, il crée une arête avec target_name = "verify_token".

Mais vers quel symbole exactement ?

On peut résoudre cette ambiguïté avec une cascade d’heuristiques, du plus spécifique au plus général :

flowchart TD
    A["verify_token — cible non résolue"] --> B["Même fichier ?"]
    B -->|oui| R["✓ Résolu"]
    B -->|non| C["Chemin d'import ?"]
    C -->|oui| R
    C -->|non| D["Même répertoire ?"]
    D -->|oui| R
    D -->|non| E["Unique dans le projet ?"]
    E -->|oui| R
    E -->|non| F["✗ Non résolue"]

Une seconde passe rejoue ensuite les imports une fois résolus, pour récupérer les cas où l’import lui-même était ambigu au premier tour.

Sur nos benchmarks, cette heuristique résout 25 à 37% des arêtes selon le langage. C’est suffisant pour des projets simples, mais clairement insuffisant pour des codebases complexes avec homonymes et re-exports.

Pourtant dans les IDE j’arrive bien à renommer des classes ou des fonctions sans rien casser ? Comment ça marche, et pourquoi cartog ne le fait pas tout de suite ?

Les IDE résolvent ça en interrogeant un language server — un compilateur partiel qui connaît la sémantique exacte du projet (types, scopes, re-exports), là où l’heuristique de cartog ne voit que des noms et des chemins. Brancher cartog sur LSP fait passer la résolution à 44-81%. C’est le sujet d’un article dédié plus loin dans la série.

Et si l’agent cherche “la logique de validation des commandes” sans connaître le nom de la fonction ? Il faut bien une autre approche…

Le graph structurel résout le problème de la compréhension du code, mais il exige de connaître le nom exact du symbole recherché.

Ce sera le sujet du prochain article de la série : la recherche sémantique sur du code.