01 · Pipeline
From source file to snapshot
Enola does not execute the application. Tree-sitter grammars and language-specific extractors convert source into data. The same commit and configuration produce the same snapshot ID.
walk files -> extract facts -> store -> link -> index the graph -> explain -> write .enola/
- Walk. Files are enumerated under the repository and filtered through the ignore globs, so build output, vendored code and generated files never reach a parser. The TypeScript extractor additionally detects minified or bundled files by content (any line longer than roughly 2,000 characters), because a checked-in vendor bundle outside a build directory would otherwise fill the graph with obfuscated symbols.
- Extract. Each extractor detects whether it applies, then parses its files and emits facts. For example, the Go extractor activates when it finds go.mod. This stage uses no model inference.
- Store. Facts land in an in-memory store indexed by kind, file, name and repository. In multi-repo mode each fact carries a repository label and paths are prefixed with it.
- Link. Edges no single extractor could see are resolved here, in three ordered steps: pre-link binders, then cross-repo signals (only when two or more repositories are loaded), then post-link binders, which bind routes to their handlers and record which routes went unmatched. The whole stage is recomputed from scratch whenever a repository is appended.
- Index. The bidirectional graph is built. This is what traverse, find_path and impact_analysis walk.
- Explain. All twenty-two explainers run on every snapshot. Policy determines which findings may set an exit code.
- Write. Artifacts go to .enola/, including llm_context.md, a token-budgeted summary an agent reads directly. Content hashes make the next run incremental.
Five plugin roles drive the pipeline: extractors convert source to facts, binders resolve references, cross-repo signals identify dependencies between repositories, explainers produce findings, and renderers write artifacts. Each role has a registered Go interface.
02 · Facts
What a file becomes
A fact is a typed node with a name, file, line, and outgoing edges. Explainers and agent tools operate on these nodes and edges.
A Go file internal/auth/handler.go with a LoginHandler struct, a Verify method on it, and a call to tokens.Decode becomes roughly:
module internal/auth symbol LoginHandler (symbol_kind: struct) declares--> LoginHandler.Verify symbol LoginHandler.Verify (symbol_kind: method) calls-----> tokens.Decode dependency internal/tokens (imported by internal/auth)
These facts come from parsing the file. The graph lists every kind and relation. Use show_symbol or query_facts to inspect a symbol or file.
03 · Baseline
Why there are two snapshots
A linter checks the current tree. Enola compares two trees to identify structural changes.
baseline pin stores the first snapshot. check builds the second and reports findings introduced since the baseline.
Each snapshot includes a receipt with the Enola version, Git ref, working-tree state, extractors, and a content-derived snapshot ID. A version or ignore-configuration mismatch makes the snapshots non-comparable, and the check exits 3.
04 · Traced
One regression, end to end
This is examples/layers-gate: five Go packages and one ./run.sh. The module declares a layer order, outermost first:
layers:
- {name: delivery, paths: ["web/**", "notify/**"]}
- {name: api, paths: ["api/**"]}
- {name: storage, paths: ["storage/**"]}A baseline is pinned, and then one function is added to the innermost package. It emails the buyer a receipt, from inside storage:
import "layersgate/notify"
func LoadPrice(item, buyer string) int {
price := ReadPrice(item)
notify.SendReceipt(buyer, item)
return price
}Here is what each stage does with it.
- Extraction records a dependency fact, storage -> layersgate/notify at storage/storage.go:3, a symbol fact for storage.LoadPrice at line 11, and the call edges out of it.
- The layers explainer reads the declared order, sees the innermost layer importing the outermost one, and emits a finding. It carries confidence 1.00 because the order was declared. An order Enola merely recognised from repository shape caps at 0.80, which is below the default gate floor.
- The delta compares against the pinned baseline. This finding was not there before, so it is reported as introduced rather than as one of the repository's standing findings.
- The policy decides the exit code, and only then. With no --fail-on, the run reports and passes, and says so:
PASS — 1 new finding reported, nothing enforced: no policy set.
New findings (reported — no failure policy set):
- [layers] 1.00 — Layer violation: storage -> delivery
import of notify
No --fail-on policy is set, so nothing in this run could fail the build.Name the rule and the same change stops the build:
FAIL — 1 structural regression introduced.
Regressions (fail):
- [layers] 1.00 — Layer violation: storage -> delivery
import of notify
Policy: fail on new findings from [layers] at confidence >= 1.00.
What changed
symbols +1
dependencies +1
edges +4 (imports +1, calls +2, declares +1)
Added (2):
symbol storage.LoadPrice storage/storage.go:11
dependency storage -> layersgate/notify storage/storage.go:3
New coupling (4):
storage --imports--> notify
storage.LoadPrice --calls--> notify.SendReceipt
storage.LoadPrice --calls--> storage.ReadPrice
storage.LoadPrice --declares--> storage05 · Limits
Extraction limits
Extraction resolves what it can parse. A URL assembled at runtime, a client library Enola does not know, a framework that registers handlers reflectively: each can leave an edge unresolved. A tool that quietly drops those looks identical to one that found everything.
enola coverage reports detected outbound calls, resolved routes, and unresolved calls per service. A missing graph edge does not prove that no runtime relationship exists.
Findings computed with certainty reach confidence 1.00, including dependency cycles, declared layer violations, declared cross-repository seams, and declared constraint breaches. Repository-relative estimates remain below 1.00 and require a lower gate floor.
The long version of where this fails, written against real repositories and including a bug the exercise found in Enola itself, is docs/BLIND-SPOTS.md. The full engine internals (the fact model, the value model and the tool reference) are in ARCHITECTURE.md.