Building Enola, Part 1: Why Deterministic Architecture Analysis Matters

작성자

카테고리:

← 피드로
DEV Community · Gert · 2026-07-21 개발(SW)

This is the first post in a series about the engineering decisions behind Enola, an open-source architecture analyzer for developers and AI coding agents.

Open source: https://github.com/enola-labs/enola

We did not start Enola because we wanted to build infrastructure for AI coding agents.

We started it while building a golf platform that had grown across club management, memberships, learning, web, mobile, and backend services. Over time, it became increasingly difficult to tell what was still used, what had been abandoned, and what could be removed without breaking something elsewhere.

Tests helped, but they could not prove that an apparently unused API, type, or code path was not consumed by another repository, an older client, or a less obvious integration.

AI coding agents made the problem worse in a different way.

We already used architectural rules and fitness functions to keep generated code within the boundaries we had defined. But an agent could still duplicate an existing module, carry the pattern all the way up to a new API we never needed, and produce code that passed tests and respected every rule.

The implementation looked valid. The architecture remained compliant. The result was still unnecessary code and future maintenance.

What became difficult was not reviewing individual lines. It was knowing whether the agent had done exactly what we asked, whether it had quietly done more, and what that change had actually added to the system.

ASTs, language servers, and RAG provide the pieces

None of the foundations behind Enola were new.

Compilers already had ASTs. Language servers already exposed symbols, definitions, references, and type information. Static analysis tools already built call graphs and dependency graphs.

The industry did not lack parsers.

Once AI coding agents became popular, another pattern appeared everywhere: index files or symbols, add embeddings or search, retrieve relevant fragments, and let the agent work out the rest.

That is useful, but it is not the same as representing the architecture of a system.

An AST describes the syntax of a language. A language server understands code within the boundaries of the language and workspace it supports. A RAG system retrieves fragments that might be relevant to the question.

They provide the pieces. The agent still has to connect them.

Ask what will be affected by changing an API in a backend repository, and retrieval may return the route, handler, request type, client calls, documentation mentioning the endpoint, and similarly named types from other repositories.

The agent must determine whether a Swift model and a Go model represent the same contract, whether a TypeScript route is a proxy or an unrelated path, which direction the relationships go, and which results are noise.

That is no longer retrieval. It is architectural reconstruction.

Because the agent performs that reconstruction, the answer depends heavily on the agent itself. A stronger model may follow the chain correctly. A smaller one may stop early. One agent may inspect another repository; another may decide it already has enough context.

The same codebase and the same question can therefore produce different architectural conclusions.

For questions involving engineering judgment, that variation is expected.

For questions such as “what calls this?”, “which repositories depend on this contract?”, or “what became reachable after this change?”, it should not be the default.

A codebase is a network of relationships

Source code is stored as files and directories, but architecture does not follow that shape.

A file can contain several unrelated symbols. A package can depend on another package without that relationship being obvious from the directory tree. Two repositories can be connected through an API, event, schema, generated client, route, or data model even though there is no direct import between them.

The useful representation is therefore not another collection of indexed files. It is a graph of explicit relationships.

Functions call other functions. Types reference other types. Packages depend on other packages. Interfaces are implemented by concrete types. Routes connect handlers to application logic. Clients in one repository consume contracts exposed by another.

The important part is not merely extracting those relationships. Individual tools already do that.

The difficult part is combining them into a consistent architectural model across languages, frameworks, and repositories.

A route in Go does not look like a route in TypeScript. A controller in Java is not represented like an endpoint in Swift. The same name may refer to different concepts in different repositories, while two differently named types may represent the same contract.

Placing every AST node into a graph does not solve that problem. Neither does embedding every symbol and asking an agent to infer what the graph probably means.

The relationships need to be discovered, typed, directed, normalised, and connected in a way that remains useful beyond the parser that produced them.

Deterministic where possible

Not every fact in software can be established with complete certainty.

Dynamic dispatch, reflection, configuration, generated code, runtime dependency injection, and external systems all introduce ambiguity. Any tool claiming to understand every possible relationship perfectly would be overstating what static analysis can do.

But a large part of a system’s structure can still be established deterministically, or represented together with the evidence used to infer it.

A symbol is defined in a particular place. An import exists. A function contains a call. A route is registered. A type implements an interface. A client references a path. A package depends on another package.

Enola starts from those facts, then uses language and framework specific analysis to connect them into a broader model.

The purpose is not to remove reasoning from software engineering. It is to stop spending probabilistic reasoning on facts that can be extracted more reliably.

Facts first, reasoning second

There are questions where we want the agent’s judgment:

  • Should this interface change?
  • Is this design maintainable?
  • What is the safest migration path?
  • Should this dependency be removed?
  • Is the architectural impact acceptable?

And there are questions that should begin with evidence:

  • Which symbols exist?
  • What references them?
  • Which functions call them?
  • Which packages depend on each other?
  • Which relationships cross repository boundaries?
  • What was added or removed by this change?
  • What is reachable from this entry point?

An agent can reason much better about the first category when it does not have to reconstruct the second category every time.

This is also where the value goes beyond saving tokens.

A smaller model can work with the same architectural facts as a larger one. Different agents can begin from the same representation of the system. A result does not depend entirely on whether one particular agent happened to search the correct files in the correct order.

The reasoning may still differ.

The underlying architecture should not.

The architectural model becomes a DSL

There is another useful way to look at Enola’s output.

The files Enola generates under .enola are not ordinary documentation, source-code summaries, or chunks prepared for retrieval. Together, they form a domain-specific language for describing the architecture of a software system.

Their vocabulary is intentionally narrower than the vocabulary of the source code itself. They describe architectural concepts such as repositories, modules, symbols, relationships, routes, calls, dependencies, and cross-repository connections without requiring every consumer to parse every language and framework again.

This closely matches the argument Unmesh Joshi makes in DSLs Enable Reliable Use of LLMs. The article describes how a constrained semantic model and domain-specific vocabulary can reduce the number of possible interpretations available to an LLM, while deterministic tooling around the DSL can provide a harness for validating what the model produces. It also argues that, once a suitable abstraction exists, an LLM can act as a natural-language interface to it rather than inventing the domain model from scratch.

Enola applies a related idea to software architecture, although the direction is different from a DSL primarily intended for code generation.

It starts with general-purpose source code written across different languages and frameworks, analyzes it, and produces a constrained architectural representation. Developers and agents can then query that representation without reconstructing the underlying architecture from raw source files each time.

The .enola files become a shared vocabulary between the codebase, Enola, developers, and coding agents.

Instead of asking the agent to determine what a Go route, a Swift client, a TypeScript type, and a Java handler might collectively mean, Enola maps them into architectural concepts and relationships first. The agent reasons over a smaller and more explicit domain.

This does not make the model infallible, and it does not turn every architectural question into a deterministic one. But it changes where uncertainty begins.

The extraction and representation of the architecture happen before the agent starts reasoning. Different agents can therefore consume the same architectural DSL, even when their ability to independently search and understand a large multi-repository codebase differs significantly.

The enduring artifact is not the prompt that asked an agent to investigate the system. It is the architectural model extracted from the code.

From repositories to an architectural graph

This deterministic architectural model, and the DSL used to expose it, became the foundation of Enola.

Enola analyzes source code, extracts symbols and relationships within repositories, normalises them into a common representation, and connects repositories where the code provides evidence of a relationship.

The result is not just an index of files, a symbol database, or a collection of disconnected repository graphs.

It is an architectural graph that can represent relationships both inside a repository and across repository boundaries.

Depending on the language and framework, Enola can identify information such as:

  • Symbol definitions and references
  • Function and method calls
  • Type and interface relationships
  • Module and package dependencies
  • Routes and handlers
  • Architectural boundaries
  • Cross-repository relationships
  • Change-impact paths

This model can be queried directly by developers or exposed to AI coding agents through MCP.

Enola does not replace ASTs, language servers, search, or coding agents. It uses similar underlying sources of structural information but solves a different problem: turning language- and repository-specific facts into a system-level architectural model.

It also does not make architectural decisions on behalf of the agent. It gives the agent a more reliable representation of the system on which to base those decisions.

Enola is not primarily a visualization tool either. Its data can be used by agents or other tools to generate diagrams, reports, and visualizations, but its core responsibility is extracting, normalising, and connecting the underlying architectural facts.

Why this matters more as systems grow

For a small repository, repeated searching and local code navigation may be enough.

As a system grows across languages, frameworks, services, and repositories, the cost of reconstructing its architecture rises quickly.

The agent must understand that a route in Go, a controller in Java, an endpoint in TypeScript, and a client call in Swift may be parts of the same architectural path, even though they are represented differently in code.

It also has to distinguish between things that share a name but not a meaning.

A UserDTO in one repository may not be identical to a UserDTO in another. Two services may use different representations of the same business concept. A cross-repository connection may exist through an HTTP contract, event, generated client, or schema rather than a direct code reference.

This is where architecture analysis becomes more than parsing syntax, running a language server, or putting embeddings on top of symbols.

The difficult part is building a model that preserves the meaning and direction of relationships across the entire system.

That is the problem Enola is being built to solve.

What comes next

This article explains why deterministic architecture analysis is the starting point.

The next post will look at how Enola represents software as a graph of graphs: why repository-level graphs are not enough, how cross-repository relationships are discovered, and why seemingly identical concepts can mean different things across languages and frameworks.

We are building Enola in the open and will continue documenting the engineering decisions, trade-offs, and mistakes behind it as the project develops.

GitHub logo enola-labs / enola

enola – MCP Architectural Snapshot Server and Knowledge Graph

enola

A deterministic structural model of your codebase for AI coding agents — your real architecture, extracted from source, not guessed.

enola is a local Model Context Protocol (MCP) server. Point it at one or more repositories and it builds a precise graph of your code’s architecture — modules, types, routes, dependencies, and how they all connect — straight from your source. It then exposes tools your AI agent can use to read, traverse, query, and reason over that structure. So before your agent writes a line of code, it already knows the shape of the thing it’s editing.

TL;DR — try it in 30 seconds

1. Install

curl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"   # if not already on PATH

Enter fullscreen mode Exit fullscreen mode

2. Connect to your agent

Claude Code:

claude mcp add enola enola

Enter fullscreen mode Exit fullscreen mode

Cursor (add to mcp.json):

{
  "mcpServers": {
    "enola": { 

Enter fullscreen mode Exit fullscreen mode

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다