I was looking at one of the least glamorous files in Enola recently: the extractor cache.
It is not the kind of file people usually write blog posts about. There is no clever algorithm that needs a conference talk. There is no dramatic benchmark chart. The cache does what a cache should do. It stores extracted facts, reuses them when the inputs have not changed, and invalidates them when the extractor output or fact schema changes.
Still, this file might be one of the better explanations of what Enola actually is.
The interesting part is not the cache implementation itself. The interesting part is the version history above it.
Every time the fact schema changes or an extractor starts producing different facts, the cache version has to be bumped. Otherwise, Enola could reuse old facts that no longer mean the same thing. That is boring engineering hygiene, but after enough versions, it starts telling a story.
In Enola, that story is now a history of all the places where “just parse the code” was not enough.
Code search is not architecture extraction
A lot of current AI developer tooling treats code understanding as a search problem. Index the repository, embed the files, retrieve the most relevant chunks, and let the agent reason from there.
That is useful. I use those tools myself. But search and architecture extraction are not the same thing.
Search can find URLSession. Architecture extraction needs to know whether that is an actual outbound network call, a file URL, a test fixture, or a wrapper that later becomes transitive I/O.
Search can find a Rails route file. Architecture extraction needs to connect the route to the controller action, including nested resources, singular resources, mounted route files, and API prefixes.
Search can find a Kotlin interface. Architecture extraction needs to understand whether Dagger, Hilt, Retrofit, Room, lifecycle overrides, callable references, or generated bindings change what should count as reachable code.
Search can find a React component name. Architecture extraction needs to understand that JSX usage, route config values, default exports, HOCs and class member references are real usage patterns, even if there is no simple function call.
Search gives you matching text. Enola is trying to extract meaning.
That difference sounds abstract until you look at the cache version history.
The cache version became an engineering diary
The cache version started as a simple safety mechanism. If the extractor output changes, old facts should not be trusted. The version is mixed into the cache key, so changing it forces a cold cache.
Over time, the comments above that version became a compact changelog of static analysis edge cases.
Some entries are about obvious extractor improvements: detecting Java HTTP clients, PHP routes, Swift endpoints, TypeScript network I/O, Ruby routes, Kotlin package resolution.
Some are about false negatives: functions used only through Ruby send, Swift custom operators, Kotlin callable references, TypeScript JSX, or file-scope route configs. These are cases where the code is live, but a naive call graph would claim it is dead.
Some are about false positives: constant-bounded loops reported as scaling complexity, overloads reported as recursion, subscript access reported as function calls, reactive chains reported as nested loops, test/tooling modules polluting package metrics, DI infrastructure being counted as production abstractions.
Some are about framework reality: Rails route nesting, XcodeGen targets, Swift package targets, Android source sets, Dagger/Hilt wiring, Retrofit endpoints, Room DAO methods, React component usage, CommonJS imports, default exports.
This is where static analysis becomes less clean than people want it to be.
Architecture is not always in the place where the parser makes it convenient. Sometimes it is in a config file. Sometimes it is in a macro. Sometimes it is in a framework convention. Sometimes it is in a generated binding. Sometimes it is in an annotation. Sometimes it is in a string prefix close to a dynamic dispatcher. Sometimes the right answer is to avoid recording an edge, because recording the edge would be worse than missing it.
Why this matters for AI agents
The original motivation for Enola was not “let’s build another static analyser”. The motivation was more practical.
AI coding agents spend a lot of time rediscovering the same codebase. They grep, search, inspect files, build a temporary understanding, make a plan, and then lose most of that understanding in the next session. On small projects, this is fine. On larger or multi-repo systems, it becomes expensive, slow and unreliable.
The common answer is to give the agent better search or better memory.
That helps, but it still leaves the agent responsible for discovering architecture from raw code every time. It still has to decide which references are meaningful, which files matter, which routes connect to which handlers, which modules are production modules, which edges are real coupling, and which “dead” functions are actually framework entry points.
Enola tries to move that work out of the agent loop.
Instead of asking the agent to rediscover architecture by reading files, Enola extracts architectural facts deterministically and exposes them as a graph. The agent can still reason, plan and explain, but it starts from a more structured view of the system.
That is the difference I care about.
Enola does not remember what the agent happened to find. It extracts what the architecture says.
Why does caching have to be conservative
Caching extracted facts sounds simple until you ask what makes reuse safe.
If the TypeScript extractor changes how it detects JSX usage, cached facts from the old extractor are no longer equivalent. If the Swift extractor starts resolving XcodeGen targets differently, old module facts are no longer safe. If the Ruby extractor changes how it treats dynamic dispatch, old dead-code results may be wrong. If the Kotlin extractor changes package resolution, old coupling metrics can point to the wrong module.
So the cache cannot just be “file hash did not change, reuse the old result”.
The cache has to know which files affect which extractor. Source files matter, but so do manifests, configs and build files. A Swift source file should not invalidate the Ruby extractor, but a shared config or manifest might need to invalidate more broadly. The implementation intentionally over-invalidates in some cases because over-invalidation is slow but safe. Under-invalidation is fast and wrong.
That is a tradeoff I am very comfortable with.
For Enola, correctness matters more than pretending everything is maximally incremental. If the architecture graph is going to be used by a human or by an AI agent planning a change, stale facts are much worse than a cold cache.
The real product is not the cache
The cache is just infrastructure.
The useful part is what the cache history reveals: Enola is not built around generic text retrieval. It is built around deterministic architecture extraction, and that requires language- and framework-specific semantics.
This is also why I am sceptical when tools present broad language support as the main proof point. Recognising file extensions across many languages is easy. Producing useful architectural facts without flooding the graph with lies is much harder.
A useful architecture graph needs to know what should count as a call, dependency, route, module, abstraction, entry point, test reference, framework convention or I/O boundary. It needs to avoid treating every textual match as meaning. It needs to know when a reference proves reachability and when it is just noise. It needs to know when a package looks “abstract” only because a framework generated a lot of wiring around it.
That is not a generic problem. It is hundreds of small, annoying, language-specific and framework-specific problems.
The cache version history is a nice accidental record of those problems.
Where does this leave Enola?
I keep describing Enola as a deterministic architecture analyser for humans and AI agents. That can sound like positioning until you look at the implementation details.
The difference is not branding.
Code search finds text. Agent memory remembers sessions. Enola extracts architecture.
The cache made Enola faster, but the more interesting thing is that it forced every extractor change to admit something important: the meaning of the graph changed.
That is the part worth building around.
Because if an AI agent is going to change real software, especially in larger codebases, I do not want it to start from another pile of retrieved chunks and a hopeful summary. I want it to start from architectural facts that were extracted deterministically, invalidated safely, and improved every time the analyser learned where code hides meaning.
That is the work.
And, weirdly enough, one boring cache file explains most of it.
FAQ
Why does Enola's extractor cache need a version number?
The cache version is mixed into the cache key, so changing the extractor's output or the fact schema forces a cold cache. Without it, Enola could reuse facts computed by an old, semantically different extractor and silently serve outdated architectural conclusions.
Why isn't code search enough to build an architecture map?
Search finds text that matches a query — a class name, an import, a symbol. Architecture extraction has to decide what that match means: whether a Kotlin interface is DI-generated, whether a React component reference is a real usage pattern, whether a route belongs to a mounted resource. That interpretation step is what turns a text match into an architectural fact.
Why does Enola's cache over-invalidate instead of trying to be maximally incremental?
Because under-invalidation is fast and wrong. If a shared config or manifest changes in a way that could affect multiple extractors, Enola invalidates broadly rather than risk reusing facts that no longer reflect the code. A cold cache costs time; stale facts cost correctness.
Is broad language support a good proxy for extraction quality?
No. Recognising file extensions across many languages is easy. The hard part is producing architectural facts — calls, routes, dependency edges — without flooding the graph with false positives from framework wiring, dependency injection, or generated code. Language count says little about whether the underlying facts are trustworthy.
Does Enola use AI to extract architecture, or is it deterministic?
Deterministic. Enola's extractors are AST parsers and tree-sitter grammars, not language models, so the same commit produces the same graph every time. AI agents query the resulting facts rather than being asked to re-derive architecture from raw source on every session.
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