There is a moment that happens in almost every mature codebase. You follow one dependency and discover that a supposedly isolated package is connected to half the system. Two modules form a cycle. A temporary adapter has become permanent infrastructure. Nobody remembers deciding to design it that way.
The architecture accumulated one reasonable change at a time.
Until now, enola. could show the architecture that exists today and compare it with the state before a change. It could tell a developer or coding agent what depends on a component, estimate the blast radius, and detect whether the current work introduced a new structural regression.
enola. can now also preserve that architecture over time:
enola log enola show <revision> enola diff <a>..<b> enola blame <pattern>
These four commands are experimental: the flags and the on-disk format may still change, but they run today, and everything below is real output from enola on its own repository rather than an illustration.
The timeline was built with enola log --backfill --sample=daily, which walks the repository's own commit history and snapshots one commit per day: 66 revisions covering February to August 2026, produced in eleven seconds. Nothing was recorded live; a repository enola has never seen still has a past to read.
$ enola log a202333 2026-02-10 19:28 (3b0f52fcf06a, backfill) initial: 268 facts, 8 findings 83bae96 2026-02-11 17:15 (b506ea7dc746, backfill) 3 new findings · 1 resolved · +50/-2 facts · ~6 changed · +337/-14 edges 1467567 2026-02-13 16:40 (273222544b32, backfill) 2 new findings · +57/-10 facts · ~3 changed · +268/-62 edges … 3b0ec33 2026-06-03 22:12 (e7ea737650a1, backfill) +59 facts · ~2 changed · +209/-1 edges 3b0ec33 2026-06-06 11:45 (e7c6bdcc80e9, backfill) no architectural change … 4d5e4ea 2026-08-02 21:24 (4ec536bdda33, backfill) 4 new findings · 1 resolved · +182/-107 facts · ~6 changed · +644/-423 edges 79ab04b 2026-08-03 23:19 (482e429640b7, backfill) 10 new findings · 1 resolved · +613/-96 facts · ~10 changed · +2095/-347 edges
Six months of work read as structure rather than as commits: 268 facts and 8 findings at the beginning, 4,106 facts and 120 findings today. Note the second June row. A commit landed that day and the architecture did not move: the same snapshot id as the revision before it. That is a result too, and it is the one that makes the other rows worth reading.
That makes architectural archaeology much easier. But archaeology is what we need after the architecture has already drifted. The better outcome is to notice where the system is going while the change is still being made.
Architecture should be checked while it is changing
Most architectural problems are not created in one obviously disastrous commit. They emerge through changes that are locally reasonable but structurally expensive.
A developer reuses an internal helper from another package. An agent puts new behaviour next to similar code, even though that package should not own it. A migration adds a temporary dependency that survives long after the migration is finished. Each change may compile, pass its tests, and look harmless in the textual diff.
The structural consequence becomes visible only when the new architecture is compared with the old one.
This is the development loop enola. is intended to create. Before changing the code, a developer or agent can inspect the current structure, understand the dependencies involved, and estimate the blast radius. After the change, enola. compares the result with a pinned baseline and reports what the work introduced.
That last part matters. A mature codebase may already contain cycles, oversized packages, and questionable dependencies. Reporting every existing problem on every change does not protect the architecture. It produces a wall of output that developers quickly learn to ignore.
A useful architecture check needs to work as a ratchet. Existing debt remains visible, but the current change is judged on whether it makes the structure worse.
If enola. detects a newly introduced cycle, dependency, or boundary violation, the developer can correct it while the implementation is still fresh. A coding agent can receive the same feedback before it declares the task complete. CI then runs the full check again against the submitted change.
The process is simple: understand the architecture, make the change, inspect the structural result, fix what went wrong, and enforce the same verdict in CI.
History extends that loop by preserving the accepted result.
The code diff is not the architecture diff
Git already preserves every textual change, but architectural questions are rarely line questions.
Git can show when an import statement appeared. It does not directly show that the import closed a cycle across three packages. It can show that a new client was added, but not that this created a new dependency between repositories. It can show who edited a file, but not when one package gradually became a central dependency for several unrelated parts of the system.
The evidence exists in the source history, but the structural meaning has to be reconstructed.
That usually means checking out old commits, running analysis repeatedly, following imports and calls, comparing different states, and trying to determine which textual change mattered architecturally.
enola.'s history records architectural revisions alongside the development history. enola log shows how the graph evolved. enola show explains one revision. enola diff compares any two architectural states.
enola blame applies the familiar idea of blame to structural facts rather than individual lines. The target can be a module, symbol, path, finding, or dependency edge. It can help answer when a relationship first appeared, which revision introduced a cycle, whether it later disappeared, and when it returned.
Asking when one dependency edge entered the architecture is a single command, and the answer is a single line:
$ enola blame --first "internal/server -> github.com/enola-labs/enola/pkg/history" 79ab04b 2026-08-03 23:19 (482e429640b7, backfill) + dependency internal/server -> github.com/enola-labs/enola/pkg/history (internal/server/history.go:11) 1 event across 66 searched revisions.
enola show then explains everything else that revision did: the change that gave enola. a history also gave its own architecture a dependency chain five modules deep.
$ enola show @66 # revision 79ab04b · 2026-08-03 23:19 · (482e429640b7, backfill) # Architecture diff Baseline 2026-08-02T21:24:09+02:00 — current 2026-08-03T23:19:56+02:00. ## Regressions introduced (10) ### 1. Deep dependency chain: internal/engine (depth 5) - explainer: dependency-depth · confidence: 0.70 - Module "internal/engine" has a longest dependency chain of 5 modules: internal/engine -> internal/history -> pkg/history -> pkg/facts -> internal/facts. Deep chains slow comprehension and widen rebuild/retest impact when a module near the bottom changes.
Nothing in the textual diff of that change says "depth 5". The imports it added are individually unremarkable; the chain is a property of the graph they completed.
This is not mainly about finding somebody responsible. Historical context often shows that the original decision was reasonable. A narrow adapter gained more consumers. A temporary migration path became permanent. A shared helper attracted unrelated responsibilities because it was already convenient.
Looking only at the current graph can make the architecture appear irrational. Looking through its history shows how a sequence of understandable local decisions produced the current structure.
Fixing drift before it becomes history
The value of architectural history is obvious when a problem already exists. It helps explain where it came from.
The more important use of architectural comparison happens earlier.
Suppose a developer is changing a component used across several packages or repositories. Before editing anything, enola. can expose its consumers, dependencies, and cross-repository relationships. A change that initially looked local may turn out to have a much larger impact.
pkg/history.Read reads one revision out of the store. It is a small function, and it looks like a safe thing to change. This is what an agent receives when it asks what depends on it, before the first edit:
# Impact summary: pkg/history.Read 33 total dependents — depth 1: 10 symbols, 6 test_refs; depth 2: 11 symbols; depth 3: 6 symbols ## Hotspot modules - pkg/command (13 dependents) - internal/history (9 dependents) - internal/server (5 dependents) - internal/engine (4 dependents) - pkg/bootstrap (1 dependents) ## Architectural insights touching this target (2) - High fan-in symbol: pkg/history.Read (10 dependents) — "pkg/history.Read" is depended upon by 10 other symbols — well above the repo average. High fan-in concentrates change risk: edits here ripple across many call sites, and it often indicates a missing or overly broad abstraction. - Call-graph hotspot: pkg/history.Read (fan-in 10, fan-out 6) — "pkg/history.Read" is a pinch point: 10 symbols call into it and it calls out to 6 others, so a large share of call chains pass through it.
Changing that signature is a five-module change. enola. says so before the edit, rather than after the build breaks, and it says it in the same terms it will use afterwards to grade the result.
The same applies to coding agents. Without a structural model, an agent may find the closest matching implementation and extend it. The result can be locally consistent while still placing responsibility in the wrong package or creating a dependency in the wrong direction.
After the implementation, enola. compares the resulting architecture with the pinned starting point.
The question is not whether the code changed. Git already answers that. The question is what the change did to the system.
Did it add a new dependency between domains? Did it make a lower-level package depend on a higher-level one? Did it close a cycle? Did it create a cross-repository edge? Did it increase the blast radius of a component?
When that feedback arrives during development, fixing the problem may require moving a function or removing one dependency. When it arrives months later, other code may already depend on the mistake.
That is why architecture belongs in the development loop, not only in diagrams, design reviews, and post-mortems.
Closing the loop for coding agents
Coding agents make architectural feedback more urgent, but they did not create the underlying problem. Developers have always introduced accidental coupling and structural drift. Agents simply produce changes faster and often have less implicit knowledge of why a boundary exists.
Telling an agent to "respect the architecture" is not enough. That assumes the architecture is documented, current, and available in a form the agent can use.
A more useful approach is to give the agent architectural facts before it starts and an architectural verdict after it finishes.
Before the change, enola. can help the agent find where responsibilities live, which components depend on them, and what may be affected. Afterwards, the architectural diff shows what the agent actually built.
If the result introduces a regression, the agent can receive that feedback inside the same task and correct the implementation before completion.
This is different from simply giving the agent more context. enola. also grades the structural result.
CI remains the final enforcement point. An agent should not be the authority on whether its own work is acceptable, and the complete submitted change may differ from what one agent inspected during development. The same architectural comparison therefore runs again against the final change.
The local feedback loop and the CI gate are two stages of the same process.
Why only new regressions should fail
A tool that analyses a repository and reports everything it considers wrong is easy to build. It is also easy to ignore.
A large codebase may already contain hundreds of findings. Some are known debt. Some are intentional trade-offs. Others may be valid but not worth fixing now.
If every pull request repeats the full list, the output has little connection to the work being reviewed. One newly introduced cycle is buried among problems that have existed for years.
enola. compares the current architecture with a pinned snapshot and separates existing findings from newly introduced ones. The old problems remain visible, but they do not cause every new change to fail.
The codebase does not need to become perfect before architectural checks become useful. It only needs to stop getting worse.
A benign change should pass. Introducing one new cycle should produce one new regression. Reverting it should return the result to clean. Existing findings should not be presented as though the current developer created them.
That is testable on this repository. enola.'s own snapshot carries 120 findings. Pin a baseline, then add a single file to pkg/facts: a helper that reaches back into internal/history, a package that already depends on pkg/facts. This is exactly the ordinary accident described earlier: a lower-level package borrowing something convenient from a higher-level one.
Go's own compiler would reject this particular cycle, and that is worth saying plainly rather than hiding: in Go, a direct import cycle is a build error. The demonstration is about what the report does with a regression, not about whether this one is detectable. Write the same shape in TypeScript, Python, Ruby or Java and it builds, ships, and waits.
$ enola check FAIL — 1 structural regression introduced. Regressions (fail): - [cycles] 1.00 — Cyclic dependency detected (3 modules) module "internal/history" is part of the cycle Policy: fail on new findings from [cycles] at confidence >= 1.00. Altered findings (1) — same finding, moved content: - [exported-surface] Large public surface: pkg/facts exports 26 of 26 symbols (100%) was: Large public surface: pkg/facts exports 25 of 25 symbols (100%) 19 findings changed only in supporting evidence (same title and confidence) — typically a fan-in or top-N list gaining a member. Use --detail to see them. What changed symbols +1 dependencies +1 edges +4 (imports +1, calls +1, instantiates +1, declares +1) Added (2): symbol pkg/facts.PruneHistory pkg/facts/prune.go:7 dependency …ithub.com/enola-labs/enola/internal/history pkg/facts/prune.go:3 New coupling (4): pkg/facts --imports--> internal/history pkg/facts.PruneHistory --calls--> internal/history.GC pkg/facts.PruneHistory --instantiates--> internal/history.GCOptions pkg/facts.PruneHistory --declares--> pkg/facts
One regression, not 121. The twenty findings whose contents shifted are reported as shifted rather than as new, and the rest of the 120 are not mentioned at all: they are still there and still true, but none of them is this change's doing. The command exits 1.
Delete the file, and the verdict returns:
$ enola check PASS — no architectural change.
That makes the report actionable.
One architecture model from development to CI
Architecture tooling often becomes fragmented across the development process. A developer inspects one diagram, an agent uses a retrieval system, reviewers reason from the textual pull-request diff, and CI runs a separate collection of static checks.
Each stage sees a different approximation of the system.
enola. uses the same derived architecture graph throughout the workflow. A developer can inspect it from the command line. An agent can query it through MCP. A snapshot establishes the baseline. The change can be checked during development and then checked again in CI.
The meaning of the verdict remains consistent across stages.
History adds the timeline to that same model. Once the change is accepted, its architectural revision becomes part of the record. Later, developers can see what the architecture looked like before the work, what changed, and how that change contributed to the system that exists today.
A history that can be trusted
Recording architectural history only works if the underlying snapshots are stable.
If the same source tree produces a different graph across repeated runs, apparent changes may come from the analyser rather than the code. A developer cannot act on a noisy diff, an agent cannot self-correct from unstable feedback, and CI cannot enforce a result that changes without a source change.
That is why enola.'s benchmarks focus on more than parsing speed.
The current benchmark corpus covers 38 public repositories, 239,349 parsed source files, and more than 4.2 million extracted facts across 13 language and schema tags. Scale matters because architectural feedback must work on real codebases, but reproducibility matters more.
The benchmark suite checks whether the same tree produces the same result, whether no source change produces zero architectural delta, whether a benign change remains benign, and whether one injected cycle produces exactly one new regression. Reverting that cycle must return the verdict to clean without repeating existing findings.
Historical analysis introduces one additional problem: enola. itself changes.
An extractor may improve, support for another language may be added, configuration may change, or a plugin may contribute new facts. The next snapshot could then look structurally different even though the code's architecture did not change.
enola. records an epoch with every revision: a fingerprint of the analyser version, extraction behaviour, relevant configuration, and active plugins. When the epoch changes, the history marks the boundary rather than presenting the resulting differences as an ordinary architectural change.
The same guard covers the delta itself, and it fires in the timeline above. Six of the sixty-six revisions carry a mark:
eafa813 2026-02-17 13:16 (102f3652c523, backfill, incomparable) 2 new findings · +72 facts · ~5 changed · +265/-1 edges f392b4b 2026-02-22 10:48 (7eff8ecbf823, backfill, incomparable) 3 new findings · 1 resolved · +44 facts · ~12 changed · +238 edges 9f3fa1c 2026-03-06 14:13 (986d9dc7caff, backfill, incomparable) 1 new finding · +34 facts · ~8 changed · +118/-14 edges
The numbers are still there. What the row adds is an instruction not to read them as somebody's change: the two snapshots either side of that delta were not produced over equivalent inputs, so the difference between them describes something other than the work done that day. A delta is only a description of a change when both sides were made the same way, and the history would rather label a delta than quietly present one.
The check that matters most in a history is the one that comes back empty:
$ enola blame --findings "Cyclic" Nothing matching "Cyclic" in 66 searched revisions.
In six months of recorded revisions, this repository has never carried a dependency cycle. That claim is only worth making because the command distinguishes absent from not looked at: a revision whose stored contents have aged out is reported as unsearched rather than as clean, and the count of searched revisions is printed alongside the answer. "Nothing found" and "nothing found in what I could read" are different statements, and only one of them is evidence.
The history store also remains separate from the source of truth. Every revision can be reproduced from the corresponding source commit. Present-day checks do not depend on accumulated historical state, and deleting the history does not change what enola. reports about the current working tree.
The source remains authoritative. History makes its previous architectural states easier to inspect.
From archaeology to prevention
The question "How did the codebase get like this?" will never disappear completely. Systems live for years, teams change, requirements shift, and temporary decisions outlast the context that created them.
Architectural history gives developers a practical way to investigate those changes without manually rebuilding the graph from old commits.
But the larger goal is to ask that question less often.
Architecture should be visible before a change, measured afterwards, corrected during development, and enforced before the change is merged. History then preserves the result and explains how the system evolved.
Architectural archaeology is useful when we need it. Preventing the next excavation is better.
FAQ
What is enola log?
enola log lists every recorded architectural revision for a repository, one line per snapshot: findings added and resolved, facts and edges changed, and whether the tree moved at all that day. --backfill walks a repository's existing commit history to build that timeline retroactively, so a repository enola. has never analyzed before still gets a past.
What does enola blame do?
It applies the git-blame idea to structural facts instead of source lines. Point it at a module, symbol, dependency edge, or finding name and it returns the revision where that fact first appeared or reappeared, along with how many revisions were actually searched, so a clean result and an unsearched one never look identical.
Why does enola mark some revisions incomparable?
Every revision is stamped with an epoch: a fingerprint of the analyzer version, extraction behavior, configuration, and active plugins. When an upgrade or config change shifts that fingerprint, the following delta is marked incomparable instead of being reported as an ordinary architectural change, so an analyzer update never masquerades as something a developer did.
Do I need enola running continuously to build architectural history?
No. enola log --backfill reconstructs a timeline from a repository's existing commit history in one pass, sampling at whatever interval you choose. A repository enola. is seeing for the first time can still get a multi-month history without anything having run live during that period.
How is architectural history different from the enola check gate?
enola check grades one change against a pinned baseline and exits non-zero on a regression: it's the CI and pre-commit gate. History (log, show, diff, blame) is what happens after a change is accepted: it keeps every graded revision queryable, so a question like when a cycle first appeared becomes one command instead of manually checking out old commits and re-running analysis.
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