Table of Contents

🇬🇧 English version

Voir aussi : Système de mémoire · Référence YAML · Inventaire des outils · Retour à l'index

RaggableTree

Graphe sémantique multi-langage du code source : parsing Tree-sitter, enrichissement embeddings, stockage vectoriel, et exposition aux agents via 15 tools. Remplace la lecture de fichiers brute par un index structuré à cinq niveaux stratifiés (L0 monorepo → L1 package → L2 module → L3 symbole → L4 statement) plus la couche d'arêtes entre eux.

Vue d'ensemble

Un agent qui n'a que file_read et directory_read doit lire des fichiers entiers, deviner leurs relations et surcharger son contexte LLM. Le RaggableTree précalcule ces éléments :

  • Structure : chaque fichier est décomposé en classes, méthodes, fonctions, avec des Fully-Qualified Names stables (pkg::Module::Class::method).
  • Relations : imports, appels, héritage et implémentations sont résolus à partir de l'AST et stockés comme arêtes du graphe.
  • Sémantique : signatures, docstrings, extraits de code et, optionnellement, résumés LLM sont composés en EmbeddingText puis vectorisés.
  • Évolution : un moteur de réindexation incrémentale met à jour l'index après chaque commit ou événement watcher.

Les tools exposés (codebase_map, symbol_detail, flow_trace, impact_analysis, etc.) donnent à l'agent des requêtes structurelles au lieu de grep sur du plain-text.

Quick start

using Orkeon.Analysis.Abstractions.DTOs.Tools;
using Orkeon.Analysis.Adapters;
using Orkeon.Analysis.Core;
using Orkeon.Domain.FileSystem;

// fileSystem : IFileSystemService (VFS) avec le projet monté sur /src
var builder = new RaggableTreeBuilder(new TypeScriptAdapter(), fileSystem);
var result = await builder.BuildAsync(
    "/src",
    new IndexCodebaseRequest { RootPath = "/src" },
    CancellationToken.None);

Console.WriteLine($"{result.Tree.Nodes.Count} nodes, {result.Tree.Edges.Count} edges, indexId={result.IndexId}");

Pour un usage en DI :

services.AddRaggableTree(new RaggableTreeOptions
{
    Languages = ["typescript", "python"],
    Embedding = new EmbeddingOptions { Provider = EmbeddingProviderKind.OpenAI, ApiKey = apiKey },
});

Configuration

La seule surface de configuration que les hôtes livrés lient est la section appsettings RaggableTree (lue clé par clé par le runner host — il n'existe pas de clé raggableTree: dans le YAML de crew) :

{
  "RaggableTree": {
    "Enabled": true,                    // false désactive entièrement la feature (opt-out du runner)
    "Exclude": ["node_modules", "dist", ".git", "bin", "obj"],
    "RootAlias": "",                    // raccourcit chaque FQN quand renseigné
    "EnrichWithLlm": false,             // true génère SemanticSummary via le summarizer
    "IncludeStatements": false,         // true indexe L4 (par statement)
    "IndexMode": "frozen",              // assigné mais consommé par rien — voir la note historique plus bas
    "Embedding": {
      "Provider": "LocalSmartComponents", // le défaut — ou None | OpenAI | Ollama (Onnx est un bras no-op réservé)
      "Model": "",                      // vide → le défaut du provider
      "ApiKey": null,
      "BaseUrl": null,
      "Dimensions": null,
      "MaxTextChars": null
    }
  }
}

Les langages ne se configurent jamais : ils sont auto-détectés depuis la codebase et scellés par appel index_codebase. Tout le reste de RaggableTreeOptions (Summarizer, ValidateCitations — défaut true, qui gate les validateurs de citations) est accessible en C# via AddRaggableTree(options) ; les groupes d'options VectorStore/Cache existent sur le record mais ne sont consommés par rien — le store est InMemoryRaggableStore, adapté sur un IVectorStoreProvider ambiant quand il y en a un d'enregistré.

Tous les champs sont des record immuables dans Orkeon.Analysis.DependencyInjection.RaggableTreeOptions.

Pipeline en six phases

  1. Discovery (IFileSystemDiscoverer) — parcours récursif, détection de packages via markers (package.json, *.csproj, pyproject.toml, go.mod, Cargo.toml), exclusion via patterns et .gitignore.
  2. Parse + Extract (UniversalSemanticMapper + ILanguageAdapter) — Tree-sitter parse → application des queries de l'adaptateur → nœuds L2 (module) et L3 (symbole) avec Fqn, Signature, SourceSnippet, Sha256.
  3. Dependency resolution (DependencyGraphBuilder + IReferenceResolver) — résolution des imports, appels, héritage, implémentations en arêtes (EdgeKind.Imports, Calls, Extends, Implements). Les FQN non résolus deviennent des UnresolvedRef.
  4. Fingerprinting (IFrameworkFingerprinter) — application des règles par décorateur/annotation (NestJS, Angular, ASP.NET, Flask, FastAPI) : pose de tags comme http-endpoint, guard, service sur les nœuds concernés.
  5. Enrichissement — composition du EmbeddingText (IEmbeddingTextComposer), optionnellement résumé LLM (INodeSummarizer), puis embedding batch (IEmbeddingProvider).
  6. Persistance — IVectorStoreProvider.IndexAsync pour les embeddings ; l'arbre lui-même reste dans le store en mémoire (le sérialiseur JSON RaggableTreeCache existe mais n'est câblé par aucun hôte livré).

La réindexation incrémentale (IncrementalReindexEngine) repart de la phase 2 pour les fichiers changés uniquement, réutilise les nœuds des fichiers inchangés, et ne lance les phases 4-6 que sur les newNodes.

Catalogue de tools

Tool Usage Requête type
index_codebase Construit l'index initial { "root_path": "/src" }
incremental_reindex Met à jour l'index après modif de fichiers { "changed_files": ["src/a.ts"] }
index_status Liste les racines virtuelles indexées (date, comptes de nœuds/arêtes) {}
is_path_indexed Vérifie si un chemin est couvert par une racine indexée { "virtual_path": "/src/app/main.ts" }
codebase_map Vue d'ensemble : packages, compte de fichiers/symboles { "level": "L2_Module" }
package_summary Résumé d'un package (modules, dépendances, symboles clés) { "fqn": "app::core" }
symbol_detail Détail d'un symbole (signature, doc, callers, callees) { "fqn": "app::UserService::create" }
symbol_source Extrait du code source (signature, body, span complet) { "fqn": "...", "mode": "SignatureAndBody" }
codebase_search Recherche sémantique par embedding { "query": "session expiration", "top_k": 10 }
dependency_graph Graphe de dépendances à une portée (L1/L2/L3), rendu Mermaid/DOT { "scope": "L2_Module", "edge_kinds": ["Imports", "Calls"] }
sub_graph Expansion BFS autour de seeds { "seeds": ["pkg::X"], "depth": 3 }
flow_trace Chemins d'appel entre deux FQN { "from": "A", "to": "B", "max_paths": 5 }
impact_analysis Impacts transitifs d'un changement { "target": "...", "direction": "Backward" }
complexity_report Top-N par métrique (Cyclomatic, LoC, Callers...) { "metric": "Cyclomatic", "top_n": 20 }
statement_query Requêtes structurelles L4 (if, try, return...) { "parent_fqns": ["..."], "kinds": ["TryCatch"] }

Les 15 tools sont enregistrés via AddRaggableTreeTools (Orkeon.Tools.Analysis.DependencyInjection.RaggableToolsExtensions).

Stratégies d'agents

Le framework expose ICodebaseContextProvider qui produit un résumé compact du codebase (packages, top complexité, top couplage, patterns détectés) adapté au system prompt d'un agent. Trois formats : markdown (défaut, ~300 tokens), compact (~150 tokens), json (~400 tokens). Aucun composant du framework ne l'appelle automatiquement — il est enregistré par AddRaggableTree et l'hôte le résout et injecte le résumé là où il le souhaite.

Recherche hybride

codebase_search (et IRaggableStore.SemanticSearchAsync) est hybride depuis le chantier fraîcheur : un classement cosinus par embeddings et un classement lexical BM25 sont fusionnés par Reciprocal Rank Fusion (SemanticQuery.Mode : Hybrid par défaut / Vector / Lexical). Le versant lexical utilise un tokenizer conscient du code (CodeTokenizer) : les identifiants sont découpés aux frontières camelCase / snake_case / chiffres et indexés à la fois en sous-tokens et en entier (getUserById → get user by id getuserbyid), de sorte que les requêtes par identifiant exact gardent leur signal fort tandis que les requêtes conceptuelles gagnent en rappel. Chaque SearchHit porte MatchOrigin (hybrid/vector/bm25) ; les scores hybrides sont des agrégats de rangs RRF (pas des similarités — ne jamais les comparer entre origines). Sans embedder câblé, Hybrid dégrade vers Lexical au lieu de renvoyer vide ; un Vector explicite conserve le contrat historique.

Fraîcheur (coordination édition ↔ recherche)

L'index reste fidèle à un workspace en cours d'édition grâce à une conception mark-dirty + réindexation paresseuse :

  • Hook d'écriture — FileWriteTool prend un IIndexInvalidation optionnel (enregistré par AddRaggableTree, implémenté par le store) : chaque écriture réussie marque son chemin comme sale. O(1), synchrone, no-op hors des racines indexées.
  • Passe paresseuse — les tools de lecture (codebase_search, symbol_source, flow_trace, codebase_map) appellent IndexFreshnessService.EnsureFreshAsync avant de répondre : l'ensemble sale ∪ les changements du working tree git (attrape les éditions via shell_command) est réindexé incrémentalement — groupé, single-flight, avec debounce (une sonde git propre fait foi pendant 2 s). Les réponses de codebase_search rapportent refreshed_files ; index_status rapporte dirty_count/dirty_paths, de sorte que le « périmé » est observable.
  • Sûreté du store — InMemoryRaggableStore détient un ReaderWriterLockSlim : les recherches énumèrent en sécurité PENDANT une réindexation incrémentale (auparavant une recherche concurrente pouvait lever sur les dictionnaires mutés). Le rafraîchissement publie RaggableTreeUpdated sur l'IRaggableTreeEventBus.
  • Barrière d'échec — un rafraîchissement en échec sert l'index courant (périmé) et conserve la dette de saleté pour la tentative suivante : un résultat périmé vaut mieux qu'une recherche morte.

Note historique : une version antérieure de ce document décrivait trois modes de synchronisation pilotés par le watcher (frozen/live/breakOnChange via RaggableTreeIndexMode). Ces modes n'ont jamais été consommés par aucun code — l'enum existait, rien ne la lisait. La conception de fraîcheur ci-dessus remplace cette fiction ; ICodebaseWatcher reste disponible pour les hôtes qui veulent une invalidation en push par-dessus la passe paresseuse.

Extensibilité

Ajouter un langage

Implémenter ILanguageAdapter (Orkeon.Analysis.Abstractions.Interfaces) : fournir LanguageName, FileExtensions, les sept queries Tree-sitter (DeclarationQuery, ImportQuery, CallQuery, InheritanceQuery, DocCommentQuery, DecoratorQuery, StatementQuery), le mapping MapNodeKind et les extracteurs ExtractSignature / ResolveImportPath. Enregistrer l'adaptateur via DI (services.AddSingleton<ILanguageAdapter, MyAdapter>()) ; AddRaggableTree le découvre automatiquement.

Ajouter un fingerprinter

Créer un IReadOnlyList<FingerprintRule> listant les décorateurs/annotations à matcher avec leurs tags puis construire un FrameworkFingerprinter(framework, rules). Voir Orkeon.Analysis.Fingerprinters.NestJsRules comme référence (15 règles).

Ajouter un embedding provider

Implémenter IEmbeddingProvider.EmbedBatchAsync(texts, ct) → IReadOnlyList<ReadOnlyMemory<float>>. Trois implémentations de référence : OpenAIEmbeddingProvider, OllamaEmbeddingProvider, et LocalEmbeddingProvider (cf. section ci-dessous). La méthode d'interface par défaut EmbedAsync(nodes, model, ct) (sur IEmbeddingProvider, Orkeon.Analysis.Abstractions.Interfaces) remplit node.Embedding à partir du EmbeddingText composé.

Embedding providers

Cinq membres existent sur EmbeddingProviderKind :

Provider Réseau Clé API Dims Notes
None n/a n/a n/a Pipeline sans embedding (phases 5-6 désactivées)
Onnx non non — Réservé — actuellement un bras d'enregistrement no-op
OpenAI requis requis 1536 (text-embedding-3-small) Qualité maximale, MTEB ~62.3
Ollama local (HTTP) non dépend du modèle Daemon Ollama / llama.cpp local requis
LocalSmartComponents non non 384 (BGE-micro-v2) In-process, ONNX CPU, démarrage à froid ~200 ms

Provider local LocalSmartComponents

Implémenté par Orkeon.Tools.Embeddings.Local (package opt-in, package upstream SmartComponents.LocalEmbeddings v0.1.0-preview10148, modèle BGE-micro-v2). Adapté aux contextes CLI dev, indexation CI offline, démos sans clé d'API.

Trade-off qualité / coût (cf. spec §10) :

  • 384 dimensions vs 1536 (OpenAI) — recherche sémantique moins fine sur du Q&A factuel hors-domaine.
  • Score MTEB ~58.5 vs ~62.3 — acceptable pour l'indexation de code (les signatures, FQN et docstrings sont des signaux structurés moins ambigus que du texte long).
  • Coût d'API → 0 (modèle embarqué dans le NuGet, ~22 MB).
  • Aucune dépendance réseau, aucune clé à gérer.
  • Démarrage à froid ~150-300 ms (chargement ONNX en mémoire) ; ~5-15 ms / texte sur CPU x86_64 moderne.
  • Empreinte RAM ~200 MB une fois le modèle chargé.

Configuration minimale (appsettings.json, cf. spec §7.2)

{
  "RaggableTree": {
    "Embedding": {
      "Provider": "LocalSmartComponents"
      // Dimensions, BaseUrl, ApiKey, Model : tous optionnels et ignorés.
      // Defaults : 384 dims (BGE-micro-v2), CPU, in-process.
    }
  }
}

Côté code, l'enregistrement DI est :

services.AddOrkeonLocalEmbeddings();        // package Orkeon.Tools.Embeddings.Local
services.AddRaggableTree(new RaggableTreeOptions
{
    Embedding = new EmbeddingOptions
    {
        Provider = EmbeddingProviderKind.LocalSmartComponents,
    },
});

Configuration étendue avec ModelPath VFS (cf. spec §7.3)

Pour pointer vers un autre modèle ONNX que le BGE-micro-v2 embarqué, déclarer un mount VFS lecture seule et passer un chemin virtuel dans Local.ModelPath. Les chemins physiques (C:/..., /var/...) sont rejetés par IFileSystemService au démarrage — voir vfs-compliance.md.

{
  "Orkeon": {
    "FileSystem": {
      "Mounts": [
        "C:\\data\\embedding-models:/models:ro"     // <physique>:<virtuel>:<droits>
      ]
    }
  },
  "RaggableTree": {
    "Embedding": {
      "Provider": "LocalSmartComponents",
      "Dimensions": 384,
      "MaxTextChars": 2000,
      "Local": {
        "ModelPath": "/models/my-custom.onnx",     // chemin virtuel — résolu par IFileSystemService
        "MaxConcurrency": 8
      }
    }
  }
}

Les licences du modèle et du package upstream sont tracées dans THIRD-PARTY-NOTICES.md. Un exemple console minimal vit dans examples/local-embeddings/.

Ajouter un vector store

Implémenter IVectorStoreProvider (IndexAsync, SearchAsync, DeleteAsync). Voir les six providers mémoire existants (Redis, SQLite, ChromaDB, Pinecone, LanceDB, InMemory) pour les patterns de mapping VectorDocument/VectorMetadata.

Limitations V1

  • Pas de support des macros C++ / templates pleinement résolus, ni des méta-programmations Ruby/Elixir.
  • Le résolveur d'imports est heuristique pour les langages dynamiques (Python, TypeScript) : les re-exports et les monkey patches peuvent produire des UnresolvedRef.
  • L'embedding et le summarizer nécessitent un provider externe (clés API ou endpoint Ollama local) pour être activés ; le pipeline fonctionne sans (phases 5-6 désactivées).
  • Le watcher se base sur System.IO.FileSystemWatcher — sous Linux/WSL, les événements de rename peuvent arriver décomposés (observés comme Deleted puis Created).
  • Le cache JSON n'est pas versionné : une mise à jour d'adapter peut invalider les caches existants, qui sont alors reconstruits à la demande.

Voir aussi : ADR RaggableTree · Inventaire des outils · Retour à l'index