{"slug": "building-enola-part-2-from-source-code-to-an-architectural-fact-model", "title": "Building Enola, Part 2: From Source Code to an Architectural Fact Model", "summary": "Enola, an architectural analysis tool, introduces a typed architectural fact model that separates parsing from extraction to preserve the meaning of relationships in source code. The model treats repositories as extraction scopes and avoids relying on names alone, keeping entities separate until evidence establishes relationships.", "body_md": "In Part 1, I explained why Enola extracts deterministic architectural facts before an AI agent begins reasoning.\n\nThat leaves the next design question:\n\nHow should those facts be represented?\n\nThe obvious answer is to build a graph containing every symbol, dependency, route, service, and repository.\n\nBut the difficult part is not putting nodes and edges into a graph.\n\nIt is preserving what those relationships mean.\n\nA 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.\n\nIf every relationship becomes a generic connection, the resulting graph may be traversable, but it is no longer reliable enough for architectural analysis.\n\nEnola therefore begins with a typed architectural fact model.\n\nA parser can tell us that a function call, string literal, import, annotation, or method declaration exists.\n\nThat is necessary, but it is not yet an architectural fact.\n\nConsider a Go application:\n\n```\napi := router.PathPrefix(\"/api\").Subrouter()\nregisterCourseRoutes(api)\n```\n\nElsewhere:\n\n```\nfunc registerCourseRoutes(router *mux.Router) {\n    router.HandleFunc(\"/courses\", listCourses)\n}\n```\n\nThe parser can expose both string literals and both function calls.\n\nThe architectural fact is:\n\n``` php\nGET /api/courses\n  -> handled_by listCourses\n```\n\nProducing that fact requires understanding the framework, how routers compose paths, and how values move through function calls.\n\nThe same problem appears in Spring annotations, Rails scopes, Axum routers, Next.js file conventions, generated clients, dependency-injection frameworks, and message-bus configuration.\n\nEnola therefore separates parsing from architectural extraction:\n\n```\nSource code\n    ↓\nLanguage and framework interpretation\n    ↓\nArchitectural facts\n```\n\nThe parser provides syntax.\n\nThe extractor determines what the syntax means within the architecture.\n\nA Git repository is a convenient place to begin analysis.\n\nIt provides a source revision, configuration boundary, files, build metadata, and stable source locations.\n\nBut it is not necessarily a unit of architectural truth.\n\nA 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.\n\nEnola therefore treats a repository as an independently addressable **extraction scope**.\n\nWithin that scope, Enola may establish facts such as:\n\n```\nRepository contains module\nModule contains file\nFile declares symbol\nFunction calls function\nPackage imports package\nType implements interface\nRoute is handled by symbol\nProducer publishes to topic\n```\n\nThese facts remain useful even when no other repositories are loaded.\n\nThe scope gives each entity a local identity and provenance. It does not imply that the entire architecture is contained inside the repository.\n\nThat distinction matters because source ownership boundaries and architectural boundaries are rarely identical.\n\nThere is no universal identifier that works for every architectural concept.\n\nA symbol may require:\n\nAn HTTP route may require:\n\nA gRPC method may require:\n\nA Kafka topic may require:\n\nThis is why names alone are insufficient.\n\nTwo repositories may both contain `UserDTO`\n\nwithout referring to the same contract. Conversely, a Go type called `PublicUser`\n\nand a Swift type called `ProfileResponse`\n\nmay represent two sides of the same API.\n\nEnola keeps those entities separate until there is evidence establishing a relationship between them.\n\nA generic graph might represent:\n\n``` php\nA -> B\n```\n\nBut architectural analysis needs to know why that edge exists.\n\nCompare:\n\n``` php\nCheckoutController\n  -> calls PaymentService.authorize\nphp\ncheckout\n  -> imports payments\nphp\nPOST /checkout\n  -> handled_by CheckoutController\n```\n\nThese relationships support different questions.\n\nA call edge may be useful for reachability.\n\nAn import edge may be useful for dependency-cycle detection.\n\nA route-to-handler edge may be useful for tracing request execution.\n\nDirection matters as well. A client consumes a route. The route does not consume the client.\n\nEnola therefore represents relationships as typed and directed facts rather than generic connectivity.\n\nThat still does not make every extracted edge equally strong.\n\nA relationship should also retain the evidence used to produce it, including:\n\nWithout that evidence, the graph becomes another opaque answer.\n\nThe complete fact model contains more relationships than any single analysis should traverse.\n\nThe question determines which subset is relevant.\n\nFor package-cycle detection:\n\n```\nNodes: packages\nEdges: package dependencies\n```\n\nFor symbol reachability:\n\n```\nNodes: symbols\nEdges: calls and references\n```\n\nFor route execution:\n\n```\nNodes: routes, handlers, services\nEdges: handled_by and calls\n```\n\nThis matters because using every available edge can produce technically connected but architecturally meaningless paths.\n\nSuppose a type belongs to a package, that package depends on another package, and the second package contains an HTTP route.\n\nThere is a path through the full graph.\n\nThat does not mean the type participates in the route’s execution.\n\nEnola therefore constructs constrained projections for specific analyses instead of treating every traversal as equivalent.\n\nConsider a frontend and backend stored in the same monorepo.\n\nThe frontend imports an API client package:\n\n``` php\nweb\n  -> depends_on api-client\n```\n\nThe backend imports a routing framework:\n\n``` php\nbackend\n  -> depends_on router\n```\n\nA conventional dependency graph correctly records both relationships.\n\nBut it cannot answer:\n\nWhich frontend method consumes\n\n`POST /api/orders`\n\n?\n\nThe answer requires several additional facts:\n\n``` php\nsubmitOrder\n  -> makes_request POST /api/orders\nphp\nPOST /api/orders\n  -> handled_by CreateOrderHandler\nphp\nCreateOrderHandler\n  -> calls OrderService.Create\n```\n\nThe useful architectural path is not a package-dependency path.\n\nIt is a projection combining request, route, handler, and call relationships.\n\nThat is the reason Enola needs a typed architectural fact model rather than only a repository dependency graph.\n\nEnola constructs the architecture that can be established from the loaded source and configuration inputs.\n\nIt does not claim that source alone describes every aspect of a production system.\n\nDeployment manifests, gateways, service meshes, runtime configuration, reflection, feature flags, and infrastructure may alter the architecture visible at runtime.\n\nThe model should therefore distinguish between:\n\nThis is important for both developers and agents.\n\nA missing edge does not always mean that no relationship exists. It may mean that the relevant source, configuration, or resolver was unavailable.\n\nA typed fact model can explain the architecture inside one extraction scope.\n\nProduction systems cross those scopes.\n\nA 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.\n\nThe next post will explain how Enola connects these independently extracted models without merging entities based on names or similarity alone.\n\nEnola is open-source [GitHub](https://github.com/enola-labs/enola)", "url": "https://wpnews.pro/news/building-enola-part-2-from-source-code-to-an-architectural-fact-model", "canonical_source": "https://dev.to/gert_/building-enola-part-2-from-source-code-to-an-architectural-fact-model-djm", "published_at": "2026-08-04 19:09:12+00:00", "updated_at": "2026-08-04 19:46:32.783475+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["Enola"], "alternates": {"html": "https://wpnews.pro/news/building-enola-part-2-from-source-code-to-an-architectural-fact-model", "markdown": "https://wpnews.pro/news/building-enola-part-2-from-source-code-to-an-architectural-fact-model.md", "text": "https://wpnews.pro/news/building-enola-part-2-from-source-code-to-an-architectural-fact-model.txt", "jsonld": "https://wpnews.pro/news/building-enola-part-2-from-source-code-to-an-architectural-fact-model.jsonld"}}