TL;DR
L’heuristique de résolution d’arêtes plafonne à 25-37 % de précision : homonymes, re-exports et imports dynamiques restent ambigus. En branchant rust-analyzer, pyright, gopls et typescript-language-server via LSP, Cartog passe à 44-81 % selon le langage.
Pour tester directement : documentation · code source.
L’article Tree-sitter et code graph pour mieux naviguer dans le code explique comment Cartog construit un graph dirigé : les nœuds sont des symboles (fonctions, classes, méthodes), les arêtes sont leurs relations (appels, imports, héritage). Pour créer une arête, il faut résoudre chaque référence vers le bon symbole cible.
Une cascade à 6 niveaux (même fichier > chemin d’import > même répertoire > scope parent > unique global > discrimination par kind) fait ce travail rapidement sur un projet entier.
flowchart TD
A["validate(data)"] --> H["Heuristique 6 niveaux<br>fichier · import · répertoire<br>scope · unique · kind"]
H -->|"3 homonymes,<br>aucun niveau ne tranche"| U["Non résolue ❌"]
Trois cas fréquents d’échec :
Homonymes : plusieurs fonctions validate() dans des modules différents. L’heuristique « même répertoire » trouve 3 candidats : impossible de trancher.
Re-exports : Python et TypeScript utilisent massivement les re-exports (__init__.py, index.ts). Le chemin d’import ne pointe pas vers le fichier source réel mais vers un barrel file.
Imports dynamiques : getattr(module, func_name)() ou require(variable), aucune trace statique à suivre.
Pour des requêtes comme cartog impact validate --depth 3, une arête non résolue signifie un appelant manquant dans l’analyse d’impact.
Le résultat est incomplet, et l’agent prend des décisions sur une vue partielle du code.
Le problème : on plafonne à 25-37 % de précision selon les langages. Autrement dit, sur 100 appels de fonction extraits du code, seuls 25 à 37 sont correctement reliés à leur définition.
Le reste pointe dans le vide ou n’est pas résolu du tout.
25 % à 37 %, ça paraît faible… Pourquoi ?
Pourtant quand mon IDE renomme une fonction à travers tout le projet, il ne casse rien… Comment fait-il ?
Les IDE interrogent un language server : un compilateur partiel qui maintient un modèle sémantique complet du projet (types, scopes, re-exports, signatures).
Quand vous faites « Go to Definition » dans VS Code, c’est le serveur LSP correspondant (rust-analyzer, pyright, gopls, typescript-language-server…) qui répond.
Le Language Server Protocol est un standard ouvert défini par Microsoft : un même serveur peut alimenter VS Code, Neovim, Helix, Zed…
Plutôt que de réécrire un compilateur par langage, Cartog s’appuie sur ces serveurs existants comme source de vérité pour résoudre ses arêtes.
sequenceDiagram
participant C as Cartog
participant LSP as Language Server
C->>LSP: initialize(rootUri)
LSP-->>C: capabilities
Note over C: Pour chaque arête non résolue...
C->>LSP: textDocument/definition(file, line, col)
LSP-->>C: targetFile:targetLine
Note over C: Lookup symbole à cette position
C->>C: Résoudre edge → target_id
C->>LSP: shutdown
Le LSP est une feature optionnelle activée par défaut lors de la compilation du binaire. Cartog reste pleinement fonctionnel sans : l’heuristique couvre déjà la majorité des cas.
Le LSP intervient uniquement sur les arêtes que l’heuristique n’a pas su résoudre. Elle vient en complément, pas en remplacement : il n’y a donc pas de dégradation si le serveur est lent ou ne trouve pas la définition.
Au lancement, Cartog détecte les language servers disponibles sur le PATH :
| Langage | Server | Commande détectée |
|---|---|---|
| Rust | rust-analyzer | rust-analyzer |
| Python | pyright | pyright-langserver |
| TypeScript/JS | typescript-language-server | typescript-language-server |
| Go | gopls | gopls |
| Ruby | ruby-lsp / solargraph | ruby-lsp, solargraph |
| Java | jdtls | jdtls |
| PHP | intelephense / phpactor | intelephense, phpactor |
Si aucun server n’est trouvé pour un langage, Cartog continue silencieusement avec l’heuristique seule. Pas d’erreur, pas de dépendance forcée.
flowchart TD
IDX["Indexation<br>tree-sitter"] --> HEUR["Résolution heuristique<br>6 niveaux"]
HEUR --> CHECK{"Arêtes non<br>résolues ?"}
CHECK -->|non| DONE["Graph complet"]
CHECK -->|oui| LSP_CHECK{"LSP servers<br>disponibles ?"}
LSP_CHECK -->|non| DONE
LSP_CHECK -->|oui| LSP_RESOLVE["Résolution LSP<br>textDocument/definition"]
LSP_RESOLVE --> DONE
style LSP_RESOLVE fill:#e8f5e9,stroke:#4caf50
style HEUR fill:#fff3e0,stroke:#ff9800
Le LspManager maintient un client par langage qui est réutilisé pour toutes les arêtes non résolues de ce langage.
Pour chaque arête non résolue, Cartog :
- Construit l’URI du fichier source + position (ligne, colonne) de l’appel
- Envoie une requête
textDocument/definition - Reçoit le fichier et la position de la définition cible
- Cherche dans le graph quel symbole occupe cette position
- Résout l’arête :
target_id = symbole trouvé
sequenceDiagram
participant C as Cartog
participant LSP as Language Server
participant DB as Graph
loop Pour chaque fichier (séquentiel)
C->>LSP: textDocument/didOpen(file)
loop Pour chaque arête non résolue du fichier
C->>LSP: textDocument/definition(file, ligne, col)
alt Définition trouvée
LSP-->>C: targetFile : targetLine
C->>DB: symbole à (targetFile, targetLine) ?
DB-->>C: symbol_id
C->>DB: target_id = symbol_id ✓
else Pas de réponse / hors projet
LSP-->>C: ∅
Note over C: Arête laissée non résolue<br>(pas de dégradation)
end
end
end
Si le server ne répond pas ou ne trouve pas la définition, l’arête reste non résolue, pas de dégradation. Les requêtes sont envoyées séquentiellement par fichier pour respecter le protocole LSP.
| Langage | Précision heuristique | Précision avec LSP |
|---|---|---|
| Python | ~25% | ~65% |
| TypeScript | ~30% | ~72% |
| Rust | ~37% | ~81% |
| Go | ~28% | ~44% |
Ces mesures sont effectuées sur un projet Python de 69 fichiers / 4k lignes, en comparant les arêtes résolues à la vérité terrain obtenue par résolution manuelle.
Les gains varient selon le langage.
Rust obtient les meilleurs résultats : rust-analyzer maintient un modèle sémantique complet du projet (types, traits, impls).
Go est plus modeste car gopls résout moins bien les appels à travers des interfaces satisfaites implicitement (duck typing structurel de Go).
Concrètement, sur une analyse cartog impact validate --depth 3, l’heuristique seule remonte 12 symboles avec 3 arêtes manquantes.
Avec LSP, le même impact retourne 18 symboles : le graph est complet, l’agent a une vue fiable des conséquences d’un changement.
Si le LSP donne une bien meilleure précision, pourquoi Cartog ne l’active pas par défaut partout ?
Parce que le LSP n’est pas gratuit :
| Aspect | Heuristique seule | Avec LSP |
|---|---|---|
| Temps d’indexation | ~1s | +10-60s (startup server) |
| Dépendances | Aucune | Language server sur PATH |
| Précision | 25-37% | 44-81% |
| Fiabilité | Déterministe | Dépend du server |
Le startup du language server est le coût principal (initialisation du workspace, chargement des types). Cartog impose un timeout de 20 secondes sur la phase d’initialisation (variable d’environnement CARTOG_LSP_READY_TIMEOUT_SECS pour ajuster) et surveille les tokens window/workDoneProgress du protocole LSP : si le server ne signale aucun progrès, il est abandonné silencieusement et l’arête reste sur l’heuristique seule.
Pour un usage quotidien (développement actif, re-indexation fréquente), l’heuristique seule est suffisante. Le LSP prend tout son sens pour une analyse d’impact approfondie ou une première indexation complète.
Le graph est maintenant précis mais Cartog re-parse l’intégralité du projet à chaque appel à cartog index : sur 10 000 fichiers, ça devient un frein, surtout en mode watch où chaque sauvegarde déclenche un re-index.
Le développeur modifie un fichier. Pourquoi ré-parser les 9 999 autres ?
Ce sera le sujet du prochain article de la série : indexation incrémentale via IDs stables et Merkle tree.