The CLI
enola --explain, check, coverage, dashboard and the history commands all read it. See Reference.
01 · Contents
The graph is made of facts. A fact is one thing Enola found in the source: a module, a function, a route, a table. Each fact records where it was found (repository, file, line), some properties, and its relations, the directed edges to other facts. The checks, the dashboard, the MCP tools and enola check all read this one graph. So can your own tools.
| Kind | One fact per |
|---|---|
| module | package or directory of code |
| symbol | function, method, type, class, variable, constant or enum |
| route | API endpoint, on the side that serves it or the side that calls it (HTTP, gRPC, GraphQL) |
| storage | data store the code names: a table, a model, a bucket, a message topic |
| dependency | import, or declared package; in a multi-repo graph, also one repository depending on another |
| service | whole repository, in a multi-repo graph |
| intent | entry in the architecture you declared (a layer, a service, an allowed seam), rather than something measured |
| association | framework model relationship, such as a Rails has_many |
| extraction | extractor's own account of what it saw and could not resolve in one repository |
| test_ref | test file, recording which production code it exercises |
| file_ref | source file, recording calls made at the top level, outside any function |
| lint | finding an external linter reported through a provider |
Most kinds are measured from source. intent is declared: it is what you said the architecture should be, which is how the intent and layers checks can compare the two. extraction, test_ref, file_ref and lint are reference-only: they inform coverage and dead-code questions, and nothing counts them as coupling.
| Relation | Meaning |
|---|---|
| declares | a symbol, route or storage fact belongs to its module |
| imports | a module imports another module or package |
| calls | a symbol calls another |
| implements | a symbol implements an interface or extends a base class |
| instantiates | a symbol constructs an instance of a type |
| injects | a symbol receives a type through dependency injection |
| names | a symbol refers to a method by name, as data, without calling it |
| depends_on | one repository depends on another |
| handled_by | a route is served by a handler symbol (added after extraction) |
| implemented_by | a declared contract operation is implemented by a code symbol (added after extraction) |
| has_method | a type owns a method (added when the graph is built) |
From examples/cross-repo, where a web service calls an api service. First as a summary, then one line as it is stored:
route /api/v2/orders/{id} api/server.go:27 GET, served handled_by -> getOrder
route /api/v2/orders/{id} web/client.go:10 GET, client call
service web depends_on -> api
dependency web -> api via http-client endpoints: GET /api/v2/orders, GET /api/v2/orders/{id}{"kind":"service","name":"web","repo":"web","props":{"edge_coverage":[{"declared":0,"detected":3,"edge_type":"http_client","external":0,"resolved":2,"unresolved":1}],"synthetic":"crossrepo"},"relations":[{"kind":"depends_on","target":"api","target_id":"13e352a8450956d6c20ab80501574e2f"}],"id":"ee73c9905d2bd500f619cb224dafe5cb"}The service fact carries the link and its own coverage account: three outbound calls detected, two resolved, one not. Every field, every property per kind and the full vocabularies are in schema/facts.md.
02 · Built
A snapshot is produced by one fixed pipeline, the same on every run:
No step calls a language model. Re-runs re-parse only files whose content changed, so refreshing a snapshot is fast. How it works follows one change through every stage.
03 · Across repositories
Give Enola several repositories (a cluster.yaml, or a folder holding them) and it builds one graph over all of them. Every fact carries the label of the repository it came from, and each repository becomes a service fact. The linker then connects them from evidence the extractors already found:
| Signal | What is matched |
|---|---|
| HTTP | a route one repository calls, to the route another serves, by normalised path and method |
| Messaging | a Kafka topic one repository consumes, to the repository that owns it |
| gRPC and GraphQL | calls, to the services and operations that serve them |
| Imports | a shared library one repository imports from another |
| Shared code | two repositories declaring the same distinctive types, recorded as coupled without a direction and without an edge |
Each link becomes a dependency fact whose via property says how it was established. A call Enola detected but could not match is counted as unresolved, never guessed at, and enola coverage reports those counts per service. Because links are ordinary edges, every traversal and impact question reaches across repositories with no extra step. Multi-repo shows how to load them.
04 · Identity
The same source, configuration and Enola build always produce the same graph, byte for byte. Three identifiers make that checkable:
Before comparing two snapshots, Enola checks their receipts. If they were built by a different Enola version, with different extractors or different ignore rules, their difference would describe how they were made rather than what changed, so Enola declines to compare them.
05 · On disk
enola --generate writes the snapshot to .enola/ in the repository. In a multi-repo run, every repository's .enola/ receives the same complete linked graph.
| File | What it holds | Stable contract |
|---|---|---|
| facts.jsonl | The graph: every fact and its relations, one JSON object per line. There is no separate graph file. | yes |
| insights.json | The findings the checks produced, with confidence and evidence | yes |
| receipt.json | How the snapshot was built: format version, snapshot ID, Enola version, git commit and dirty state, extractors, config hashes, extraction quality | yes |
| snapshot.meta.json | The receipt fields plus per-file content hashes, for incremental runs | no |
| llm_context.md | A compact written summary for an agent to read directly | no |
| extractor_cache.json | Parsed results per file, so unchanged files are not re-parsed | no; keep it between runs |
| previous/ | The snapshot before this one, rotated on every write | same files as above |
| baseline/ | The snapshot pinned with enola baseline pin; survives re-snapshots | same files as above |
The contract files carry format_version. Additive changes, such as a new kind or property, keep the version; renaming, removing or changing the meaning of a documented field bumps it. Consumers must accept unknown kinds and fields. The formats are documented in schema/.
Outside the repository, ~/.enola/graphs/ holds the architecture history: one revision per snapshot, stored as a patch against the previous one. Nothing that grades a change reads it, so deleting it changes no verdict and no snapshot ID.
06 · Consuming
The same snapshot serves every consumer, and none of them needs an AI model.
enola --explain, check, coverage, dashboard and the history commands all read it. See Reference.
The tools give an agent queries over the graph: look up facts, traverse edges, find the path between two symbols, measure the impact of a change. See Reference.
Run Enola as a subprocess and load facts.jsonl, insights.json and receipt.json into your own store. Cognee does this for its code-graph search. See INTEGRATING.md.
To add facts from your own tooling, see PROVIDERS.md. To teach the linker a connection it does not know, see EXTENDING.md.
07 · Limits