enolaDocsThe graph

Documentation

The graph

Everything Enola does starts from one graph of your software system, built by parsing source code. What is in it, how it is built, what it writes to disk, and how to use it from your own tools.

01 · Contents

What is in the graph

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.

KindOne fact per
modulepackage or directory of code
symbolfunction, method, type, class, variable, constant or enum
routeAPI endpoint, on the side that serves it or the side that calls it (HTTP, gRPC, GraphQL)
storagedata store the code names: a table, a model, a bucket, a message topic
dependencyimport, or declared package; in a multi-repo graph, also one repository depending on another
servicewhole repository, in a multi-repo graph
intententry in the architecture you declared (a layer, a service, an allowed seam), rather than something measured
associationframework model relationship, such as a Rails has_many
extractionextractor's own account of what it saw and could not resolve in one repository
test_reftest file, recording which production code it exercises
file_refsource file, recording calls made at the top level, outside any function
lintfinding 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.

RelationMeaning
declaresa symbol, route or storage fact belongs to its module
importsa module imports another module or package
callsa symbol calls another
implementsa symbol implements an interface or extends a base class
instantiatesa symbol constructs an instance of a type
injectsa symbol receives a type through dependency injection
namesa symbol refers to a method by name, as data, without calling it
depends_onone repository depends on another
handled_bya route is served by a handler symbol (added after extraction)
implemented_bya declared contract operation is implemented by a code symbol (added after extraction)
has_methoda type owns a method (added when the graph is built)

An example

From examples/cross-repo, where a web service calls an api service. First as a summary, then one line as it is stored:

Four facts from two repositoriessummary
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}
One line of facts.jsonljson
{"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

How it is built

A snapshot is produced by one fixed pipeline, the same on every run:

  1. Walk the files. Everything under the repository, minus the ignore globs: build output, vendored code, tests and generated files.
  2. Extract. Each language extractor checks whether it applies (Go runs when there is a go.mod), parses the files it owns, and emits facts. Frameworks are handled here: a Rails route file or a Spring controller becomes route facts.
  3. Link. Resolve the edges no single file could show: a route's handler, a router prefix composed across functions, and, when two or more repositories are loaded, the connections between them.
  4. Index. Build the graph both ways, so "what does X depend on" and "what depends on X" are equally cheap.
  5. Analyse. Run the checks (explainers) over the graph and record their findings.
  6. Write. Save the snapshot to .enola/ with a receipt of how it was built.

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

One graph over several 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:

SignalWhat is matched
HTTPa route one repository calls, to the route another serves, by normalised path and method
Messaginga Kafka topic one repository consumes, to the repository that owns it
gRPC and GraphQLcalls, to the services and operations that serve them
Importsa shared library one repository imports from another
Shared codetwo 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

Identity and determinism

The same source, configuration and Enola build always produce the same graph, byte for byte. Three identifiers make that checkable:

  • Fact ID. A hash of the fact's repository, kind, name and file. It excludes line numbers, so it stays stable when code moves within a file.
  • Relation targets. An edge names its target (target) and carries the target's ID (target_id) whenever that name resolves to exactly one fact. An unresolved or ambiguous target keeps only the name, and a consumer should keep it that way rather than pick one.
  • Snapshot ID. A SHA-256 fingerprint over all the facts, the Enola version and the effective configuration. Two runs over the same inputs produce the same ID.

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

What a snapshot writes

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.

FileWhat it holdsStable contract
facts.jsonlThe graph: every fact and its relations, one JSON object per line. There is no separate graph file.yes
insights.jsonThe findings the checks produced, with confidence and evidenceyes
receipt.jsonHow the snapshot was built: format version, snapshot ID, Enola version, git commit and dirty state, extractors, config hashes, extraction qualityyes
snapshot.meta.jsonThe receipt fields plus per-file content hashes, for incremental runsno
llm_context.mdA compact written summary for an agent to read directlyno
extractor_cache.jsonParsed results per file, so unchanged files are not re-parsedno; keep it between runs
previous/The snapshot before this one, rotated on every writesame files as above
baseline/The snapshot pinned with enola baseline pin; survives re-snapshotssame 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

Using the graph

The same snapshot serves every consumer, and none of them needs an AI model.

The CLI

enola --explain, check, coverage, dashboard and the history commands all read it. See Reference.

MCP

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.

The files

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

What is not in the graph

  • Runtime behaviour. Timeouts, retries, delivery guarantees and performance under load are not structure, and the graph has no representation for them.
  • What could not be resolved. A URL built at runtime or a client library Enola does not know is counted, not linked. The extraction and service facts carry those counts, and receipt.json rolls them up.
  • What was excluded. Files the ignore rules skipped and files that failed to parse are counted in the receipt, with a sample of each naming the glob or the error, rather than silently missing.
The full page, with links to the schema and the engine internals, is docs/GRAPH.md. Per-language limits are in docs/extraction/.