In Part 1, I explained why Enola extracts deterministic architectural facts before an AI agent begins reasoning.
That leaves the next design question:
How should those facts be represented?
The obvious answer is to build a graph containing every symbol, dependency, route, service, and repository.
But the difficult part is not putting nodes and edges into a graph.
It is preserving what those relationships mean.
A function call is not the same as a package dependency. A route registration is not an import. A type reference is not proof that two services share a contract.
If every relationship becomes a generic connection, the resulting graph may be traversable, but it is no longer reliable enough for architectural analysis.
Enola therefore begins with a typed architectural fact model.
A parser can tell us that a function call, string literal, import, annotation, or method declaration exists.
That is necessary, but it is not yet an architectural fact.
Consider a Go application:
api := router.PathPrefix("/api").Subrouter()
registerCourseRoutes(api)
Elsewhere:
func registerCourseRoutes(router *mux.Router) {
router.HandleFunc("/courses", listCourses)
}
The parser can expose both string literals and both function calls.
The architectural fact is:
GET /api/courses
-> handled_by listCourses
Producing that fact requires understanding the framework, how routers compose paths, and how values move through function calls.
The same problem appears in Spring annotations, Rails scopes, Axum routers, Next.js file conventions, generated clients, dependency-injection frameworks, and message-bus configuration.
Enola therefore separates parsing from architectural extraction:
Source code
↓
Language and framework interpretation
↓
Architectural facts
The parser provides syntax.
The extractor determines what the syntax means within the architecture.
A Git repository is a convenient place to begin analysis.
It provides a source revision, configuration boundary, files, build metadata, and stable source locations.
But it is not necessarily a unit of architectural truth.
A monorepo may contain several independently deployed services. A small repository may depend on schemas, infrastructure, or generated clients maintained elsewhere. Runtime behavior may also depend on deployment configuration outside the application repository.
Enola therefore treats a repository as an independently addressable extraction scope.
Within that scope, Enola may establish facts such as:
Repository contains module
Module contains file
File declares symbol
Function calls function
Package imports package
Type implements interface
Route is handled by symbol
Producer publishes to topic
These facts remain useful even when no other repositories are loaded.
The scope gives each entity a local identity and provenance. It does not imply that the entire architecture is contained inside the repository.
That distinction matters because source ownership boundaries and architectural boundaries are rarely identical.
There is no universal identifier that works for every architectural concept.
A symbol may require:
An HTTP route may require:
A gRPC method may require:
A Kafka topic may require:
This is why names alone are insufficient.
Two repositories may both contain UserDTO
without referring to the same contract. Conversely, a Go type called PublicUser
and a Swift type called ProfileResponse
may represent two sides of the same API.
Enola keeps those entities separate until there is evidence establishing a relationship between them.
A generic graph might represent:
A -> B
But architectural analysis needs to know why that edge exists.
Compare:
CheckoutController
-> calls PaymentService.authorize
php
checkout
-> imports payments
php
POST /checkout
-> handled_by CheckoutController
These relationships support different questions.
A call edge may be useful for reachability.
An import edge may be useful for dependency-cycle detection.
A route-to-handler edge may be useful for tracing request execution.
Direction matters as well. A client consumes a route. The route does not consume the client.
Enola therefore represents relationships as typed and directed facts rather than generic connectivity.
That still does not make every extracted edge equally strong.
A relationship should also retain the evidence used to produce it, including:
Without that evidence, the graph becomes another opaque answer.
The complete fact model contains more relationships than any single analysis should traverse.
The question determines which subset is relevant.
For package-cycle detection:
Nodes: packages
Edges: package dependencies
For symbol reachability:
Nodes: symbols
Edges: calls and references
For route execution:
Nodes: routes, handlers, services
Edges: handled_by and calls
This matters because using every available edge can produce technically connected but architecturally meaningless paths.
Suppose a type belongs to a package, that package depends on another package, and the second package contains an HTTP route.
There is a path through the full graph.
That does not mean the type participates in the route’s execution.
Enola therefore constructs constrained projections for specific analyses instead of treating every traversal as equivalent.
Consider a frontend and backend stored in the same monorepo.
The frontend imports an API client package:
web
-> depends_on api-client
The backend imports a routing framework:
backend
-> depends_on router
A conventional dependency graph correctly records both relationships.
But it cannot answer:
Which frontend method consumes
POST /api/orders
?
The answer requires several additional facts:
submitOrder
-> makes_request POST /api/orders
php
POST /api/orders
-> handled_by CreateOrderHandler
php
CreateOrderHandler
-> calls OrderService.Create
The useful architectural path is not a package-dependency path.
It is a projection combining request, route, handler, and call relationships.
That is the reason Enola needs a typed architectural fact model rather than only a repository dependency graph.
Enola constructs the architecture that can be established from the loaded source and configuration inputs.
It does not claim that source alone describes every aspect of a production system.
Deployment manifests, gateways, service meshes, runtime configuration, reflection, feature flags, and infrastructure may alter the architecture visible at runtime.
The model should therefore distinguish between:
This is important for both developers and agents.
A missing edge does not always mean that no relationship exists. It may mean that the relevant source, configuration, or resolver was unavailable.
A typed fact model can explain the architecture inside one extraction scope.
Production systems cross those scopes.
A mobile client calls a backend maintained elsewhere. A service publishes an event consumed by another repository. A generated client implements a contract defined in a separate codebase.
The next post will explain how Enola connects these independently extracted models without merging entities based on names or similarity alone.
Enola is open-source GitHub