There is a pattern I keep seeing around AI coding tools. Someone takes Tree-sitter, or another parser, extracts symbols from a repository, puts those symbols into some kind of graph, exposes that to an agent, and then describes the result as codebase memory, architecture awareness, or sometimes even an architecture graph.
I understand why this exists. It is useful. It is clearly better than chunking source files into random text blocks and hoping embeddings will find the right thing later. A structured symbol index is a good tool. For many questions, it is enough. If the agent needs to find a class, a function, or the file where something is defined, this kind of system already helps.
But after spending a lot of time building Enola, I think this wording creates confusion. A syntax tree is not an architecture graph. A symbol index is not an architecture graph either. They can be inputs into one, but they are not the thing itself.
The difference sounds small from the outside, but once you start building it, it becomes the whole problem.
Parsing is the beginning, not the hard part
A parser can tell you a lot about the source code. It can tell you that a function exists, that a class has methods, that one file imports another, that a call expression appears inside a method body, or that some declaration exists at a specific location.
That is all valuable. I am not dismissing it. Enola also needs extraction. There is no architecture graph without facts coming from the code first.
But those facts are still mostly local. They describe what is present in the source code. They do not automatically explain what those things mean architecturally.
This is where the real work starts. You need to decide whether a relationship is a call, a dependency, an implementation, a framework registration, an injection, a route binding, an ownership relation, or just implementation noise. You need to decide which direction the edge should point in. You need to decide whether that direction is useful for the question being asked, or whether the reverse direction is actually what matters.
For example, A imports B and A calls B are not the same relationship. A implements B is different again. Constructor injection has a different meaning from a local variable instantiation. A package dependency is not necessarily runtime flow. A route pointing to a handler is not the same as a handler depending on a repository.
All of that can be represented as a graph, but not every graph is useful. A graph with weak semantics can look very convincing while giving the wrong mental model of the system.
The hard part is meaning
The tempting version is to say: "We parse the code and put it into a graph." That sounds like a big step, and technically it is. But it skips the important question: what does the graph mean?
If every relationship becomes a generic connects_to edge, the graph may be visually nice, but it will not answer deeper architecture questions reliably. If edge direction is chosen only because it was easy to extract, impact analysis may be wrong. If framework conventions are ignored, the graph will miss important connections. If every syntactic reference is treated as architectural, the graph becomes noisy and eventually useless.
This matters because people trust graphs. Once something is represented as a node and an edge, it feels factual. That means the graph has to be careful about what it claims.
In Enola, I care a lot about this distinction. Some relationships are deterministic because they come directly from source structure. Some are inferred because they come from conventions, naming, generated clients, or framework patterns. Some are useful for one type of question but misleading for another. The model has to keep those differences visible instead of flattening everything into one generic graph.
That is much less glamorous than saying "we support 150 languages," but it is where the actual engineering work is.
Same words, different architecture
One thing that becomes obvious very quickly is that the same architectural word does not mean the same thing across languages and frameworks.
Take the word "route." It sounds simple enough. In a Go backend, a route might be registered explicitly through a router and point to a handler function. That handler may call an application service, which may depend on an interface, which may be implemented by some storage adapter.
In a TypeScript project, a route might be an Express route, a Next.js file-system route, a server action, an RPC endpoint, or a generated API client boundary. Some of those are declared directly. Some are discovered through file structure. Some only make sense when connected to code generated from another repository.
In Swift, the interesting "route" may not even be an HTTP route. It may be an app navigation route, an API client method, or a boundary between a screen and the backend. If you force all of those things into the same generic concept, you get a clean model that is also wrong.
This is why language support is not just parser support. Yes, a parser may support many languages. That does not mean the architecture model supports those languages in a meaningful way.
Supporting a language properly means understanding how real projects in that ecosystem are structured. How are modules expressed? How are dependencies wired? How are interfaces or protocols used? How are routes declared? How are framework conventions represented? Which references matter architecturally and which ones are just implementation details?
There is no single answer that works perfectly across Go, TypeScript, Swift, Java, Ruby, Python and everything else. There are shared patterns, but the details matter a lot.
Cross-repository architecture makes it worse
Inside one repository, you can get pretty far with a basic symbol graph. It will answer some useful questions, and it will demo well. The agent asks where something is defined, the tool finds it. The agent asks what calls something, the tool follows some edges. Great.
The harder problems start when the system is split across repositories.
A frontend may call a generated client. That client may map to a backend route in another repository. The backend handler may call a service, which depends on an interface, which is implemented somewhere else. The backend may publish an event that another service consumes. Some of those connections are explicit imports. Some are generated. Some are shared route strings. Some are conventions. Some only become visible when you understand how the repositories relate to each other.
This is where "parse it and graph it" stops being enough. The graph has to connect architectural concepts, not just syntax nodes.
For AI agents, this is exactly where deterministic context matters. Without a graph, the agent has to rediscover the system every time. It greps, opens files, follows imports, summarizes what it saw, loses some details, searches again, and slowly builds a partial model in the context window. Sometimes that works. Sometimes it misses one edge and confidently proposes the wrong change.
A structural architecture graph does not make the agent magically intelligent. It gives the agent a better base to reason from.
What Enola is trying to do
This is the layer I am building with Enola. Not "memory" in the sense of remembering what an agent saw before, and not a generic syntax graph where everything is connected to everything else. The goal is to build a deterministic structural model of a codebase, and especially of multiple related repositories, so both humans and agents can ask architecture questions without reconstructing the map from scratch every time.
That means extracting facts from source code, normalizing them across languages, creating typed nodes and typed relationships, linking repositories where possible, and keeping enough evidence around to explain why something exists in the graph.
It also means being honest about limits. A static graph does not know everything. It does not automatically know production traffic, ownership boundaries, runtime frequency, operational failure modes, or architectural intent that exists only in someone's head. Some dynamic behavior will always be hard. Some framework magic will always require heuristics. Some relationships should have lower confidence than others.
But that is exactly why the model matters. The goal is not to pretend the graph is omniscient. The goal is to know what was extracted, how it was connected, which parts are deterministic, and which parts should be treated as inferred.
Why I care about this distinction
The reason I care is practical. Coding agents spend a lot of tokens on exploration. They read files, inspect neighboring files, follow imports, repeat searches, and slowly rebuild context that should already exist somewhere. In small projects, this is annoying but manageable. In larger systems, especially multi-repository systems, it becomes expensive and unreliable.
If the agent can query a compact architectural subgraph instead of reading thousands of lines of source code, the token usage changes. More importantly, the quality of the context changes. The model spends less effort discovering the map and more effort reasoning about the change.
That is the practical value.
Not a prettier graph. Not a diagram for a slide. Not another "AI memory" label.
A computable architecture model that can answer questions like: what depends on this, what does this call, what is the transitive impact, which path connects these components, which cycles exist, and which repository boundary is involved?
Tree-sitter and similar tools can absolutely be part of that pipeline. They are useful. They are often the right place to start.
But they are not the architecture: a syntax tree tells you what the code looks like while an architecture graph has to tell you how the system is connected, what those connections mean, and why you should trust them.
That difference is where most of the actual work lives.
FAQ
Is a symbol graph from Tree-sitter the same as an architecture graph?
No. A Tree-sitter symbol graph tells you what exists in the code — functions, classes, imports — and where. An architecture graph has to decide what each relationship means: whether an edge is a call, a dependency, an implementation, a framework registration, an injection, or a route binding, which direction it should point, and whether that direction answers the question being asked. A symbol graph is a useful input into an architecture graph, but it is not the same thing.
Why can't every code relationship be represented as one generic edge type?
Because different relationships carry different architectural meaning even when they look similar syntactically. An import is not a call. An interface implementation is not a dependency. Constructor injection behaves differently from a local variable instantiation, and a route pointing to a handler is not the same as a handler depending on a repository. Collapsing all of these into a single connects_to edge produces a graph that looks complete but cannot answer deeper questions like impact analysis reliably.
Why does the same architectural word mean different things across languages?
Take "route." In a Go backend it is usually registered explicitly through a router and points to a handler function. In a TypeScript project it might be an Express route, a Next.js file-system route, a server action, or a generated API client boundary — some declared directly, some discovered through file structure. In Swift, the relevant "route" may not be an HTTP route at all, but an app navigation route or a boundary between a screen and the backend. Forcing all of these into one generic concept produces a model that is clean but wrong.
Why is cross-repository architecture harder than single-repository symbol graphs?
Inside one repository, a basic symbol graph gets you fairly far — finding definitions and following local edges works well enough to demo. Across repositories, the connections are often not explicit imports: a frontend calling a generated client that maps to a backend route in another repo, a published event another service consumes, a shared route string, a naming convention. A graph built only from local syntax nodes cannot see these; it has to connect architectural concepts across repository boundaries, not just symbols within one file tree.
enola is open source under Apache 2.0 — free, local, no data leaves your machine.
curl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | sh