From Source Files to a Shared Structural Graph
In Code-Graph-RAG, the graph is not a decorative layer added after an AI assistant has read some files. It is the system’s central representation of a repository. This chapter follows the boundary between source code, syntax analysis, graph storage, and user-facing retrieval so that readers can see where structural facts come from, what a common schema makes possible, and why a graph can still be incomplete even when indexing succeeds.
Four transformations, not one black box
The first transformation is from source text to a syntax tree. Code-Graph-RAG uses Tree-sitter-based parsers to analyze supported languages. A syntax tree is a structured description of code: a function declaration contains its parameters and body; a class contains methods; an import statement has a module and, where the language permits it, an alias. This is different from reading lines as independent strings. The parser can recognize that two words play different roles because they occur in different parts of the language’s grammar.
The parser’s job is not to decide what a developer should do with the code. Its job is to extract facts that can be represented consistently: files and modules, functions and methods, classes and interfaces, imports, definitions, calls, inheritance, references, and other relationships the relevant language support provides. The project also parses Markdown headings as document sections, which lets documentation structure enter the same model without pretending that a document has functions or call sites.
The second transformation turns those extracted facts into graph elements. The repository hierarchy becomes nodes for a project, packages or folders, files, and modules. Code declarations become nodes such as Function, Method, Class, Interface, Type, or Enum. Relationships connect the nodes: a module defines a function, a module imports another module, a function calls a method, or a class implements an interface. External packages and modules can be represented separately from code that belongs to the indexed project. The graph therefore retains both location and meaning.
Memgraph owns this persistent structural state in the default setup. It is not the parser’s temporary output. Once ingested, the graph can be queried repeatedly by the CLI, the MCP server, an SDK program, or other tools without reparsing the entire repository for every question. That persistence also creates an operational responsibility: the graph describes the source state and analyzer configuration under which it was built. If the source or parsing mode changes, freshness becomes part of the answer’s quality.
A small feature makes the transformation easier to see. Imagine a service module that defines a UserService class and a method that calls a repository operation. The graph can place the file and module inside the project hierarchy, connect the module to the class through a definition edge, connect the class to its method, and connect the method to the repository operation through a call edge. If another module imports the service, that import is a separate relationship with its own source location. If the service implements an interface, the implementation relationship can connect the types without pretending that the interface and the concrete class are the same node.
This arrangement preserves several questions at once. “Where is the service defined?” follows containment and definition edges. “Who depends on it?” follows import or call edges in the reverse direction. “Which implementation satisfies this contract?” follows inheritance or implementation edges. “What should I inspect after changing the method?” can combine callers, importers, overrides, and tests. A text index can help locate the names involved, but it does not naturally preserve the direction, scope, and kind of each connection. The graph does not answer every question automatically; it gives each question an appropriate place to begin.
The example also explains why names and locations matter. Two methods can have the same short name in different classes or modules, while a qualified name and source span keep them distinct. A function defined inside another function belongs to its enclosing scope rather than being flattened into the module. A document heading can be modeled as a section under a document module, but it is not silently promoted into a code definition. These choices prevent the common schema from becoming a vague bucket in which unlike things happen to share labels.
The third transformation is from graph state to a query. Code-Graph-RAG’s open-ended agent can translate a natural-language question into Cypher, the graph query language used to ask for patterns of nodes and relationships. The user might ask which functions call a service, which classes implement an interface, or where authentication-related code lives. The model helps express the intent as a query, but it does not replace the parser or graph. In parallel, the project provides fixed tools for exact names and locations, callers, callees, implementors, overrides, importers, and tests that reach a symbol. Those deterministic paths make a known lookup reproducible without asking a model to rediscover a fact.
The fourth transformation is from graph results to a human decision. A result can identify a definition, a relationship, or a candidate area of code. It can support a question about architecture, guide a change, or point a reader to source that needs inspection. It is not automatically a complete explanation of the repository. A generated query may be narrower than the wording of the original question, and a graph relationship may have a resolution boundary that matters. The final answer therefore belongs to a loop that includes source inspection, tests, or another evidence layer when the claim demands it.
The ownership boundaries are useful because they prevent category errors. Tree-sitter does not choose an LLM provider. Memgraph does not infer the meaning of an ambiguous natural-language request. The model does not make a missing import edge appear merely by speaking confidently. The user-facing tools coordinate these parts, but each part owns a different kind of state and failure.
This small view is intentionally not the entire schema. It shows the important idea: hierarchy tells us where an entity belongs, definition edges tell us what a module contains, and relationship edges tell us how code interacts. External modules and resources remain visibly different from first-party code. That distinction becomes important when a query asks whether a call stays inside the project or whether a value crosses a system boundary.
One schema, many languages, uneven guarantees
A common schema is valuable because a monorepo is usually organized around products and services rather than around parser technologies. Suppose a repository contains a Python API, a TypeScript client, and a Rust worker. A maintainer may want to find implementations of a contract, trace a dependency, or locate tests that reach a shared operation. If each language produced unrelated records, the maintainer would need a separate mental model and query style for each part. Code-Graph-RAG instead gives the parts a shared vocabulary: modules import, modules define, callables call, types inherit or implement, and files sit inside a project hierarchy.
The schema does not erase language differences. It normalizes the relationship that a query needs while allowing language-specific details to remain attached to the node or edge. A method can retain its qualified name, location, documentation, modifiers, return type, and parameter information when the extractor can read them. A call edge can later carry the site where it occurred and the way it was resolved. A Rust macro, a C++ template, a C# property, or a TypeScript interface may require different extraction rules even though each contributes to a common graph.
This is why “supported language” needs a more precise meaning. The project’s matrix distinguishes several tiers. The fully supported Tree-sitter languages provide the structural capabilities listed for each language, but those capabilities are not identical: one language may have package detection or inheritance handling that another lacks. Scala and SQL are described as in development in the inspected documentation, with narrower feature coverage. The ast-grep tier can add basic modules, functions, classes, and imports from declarative patterns for languages without a hand-written Tree-sitter integration, but it does not provide call-graph resolution. Markdown belongs to a document tier: headings become nested Section nodes, while functions, classes, and calls are not expected.
That distinction changes what a query can mean. A call-chain or dead-code analysis needs CALLS edges. If a structural-tier file has definitions but no call graph, its functions cannot be judged by the same reachability procedure as a fully analyzed language. A document can be searchable as a structured set of sections, but it cannot participate in a code call chain. A graph query may still return rows from all these tiers, yet the rows do not carry equal evidentiary power.
Compiler-backed frontends add another layer of precision. Tree-sitter remains the backbone, while a compiler or language tool can supply facts that syntax alone cannot determine reliably: which declaration a call binds to, whether a base is an inheritance or implementation relationship, or which semantic aliases are in play. The project documents C++, C#, Go, Java, and Python frontends with different responsibilities. The key design rule is fallback: when an optional toolchain is missing, unavailable, or does not apply, the graph should fall back to the Tree-sitter representation rather than lose the baseline coverage. A precision layer is an addition to the structural model, not a replacement that makes the graph disappear when a compiler is absent.
The fallback rule is valuable, but it does not mean the fallback is equivalent. A Tree-sitter call heuristic may name several plausible targets, while a compiler can bind one. An ambiguous site may be left for the heuristic, and the frontend protocol distinguishes a site that was analyzed and proven external from one that was not analyzed at all. That distinction matters because “nothing found” and “the analyzer did not look” are different facts.
Adding a grammar illustrates the same boundary. The project can derive common node types and calls from a grammar’s metadata, but grammar structure alone cannot tell the system the meaning of every extends clause, import alias, mixin, or qualified name. Language-specific code must teach those semantics. The project’s own documentation warns that partial support can fail silently: indexing completes and produces a graph, but a relationship kind may be missing and queries may simply return fewer results. A successful parse is therefore evidence that some structure was recognized, not evidence that every question about that language is covered.
The practical consequence is a two-part inspection habit. First ask what the common graph says: which nodes, relationships, and locations are present? Then ask what produced that graph: which language tier, frontend, capture configuration, and parser state were active? The first question tells us how to navigate the model. The second tells us how far a conclusion can travel.
What the graph makes possible—and what it does not
The shared structural model changes the scale of questions Code-Graph-RAG can address. A text search can return occurrences of a name. A graph can connect a definition to callers, importers, implementors, overrides, and tests that reach it. A graph export can be loaded by the Python SDK for custom analysis or documentation generation. A real-time updater can reparse a changed file and affected dependents so the model follows active development. These are consequences of representing relationships as durable data rather than reconstructing them from scratch in every conversation.
But graph representation is not the same as semantic omniscience. The parser may not resolve a dynamic dispatch. An optional capture group may not have been enabled. A language tier may not provide the edge a question requires. An index may reflect an earlier commit or an earlier frontend mode. Even a correctly represented edge answers only the question its semantics support. “Function A calls function B at this recorded site” is not the same as “B is the only behavior that can occur at runtime,” and “no modeled flow path was found” is not the same as “no value can ever reach the sink.”
The graph’s value is therefore strongest when its structure and its boundary are read together. It gives the reader somewhere concrete to inspect: a node with a definition span, an edge with a producing site, a module with a known language, or a resource relationship with an explicit capture condition. It also gives later tools a common place to combine evidence without pretending that all evidence came from the same analysis.
Code-Graph-RAG’s foundational design choice is thus a disciplined compromise. It spends effort turning heterogeneous source into a shared model because relationships are what make repository-scale questions useful. It preserves language-specific differences and exposes fallback paths because normalization alone cannot guarantee meaning. The result is not a perfect mirror of a running program. It is a durable structural map whose usefulness depends on reading the map’s symbols, provenance, and coverage together. The next question is how an individual edge carries enough detail to become inspectable evidence rather than just a line between two names.