enola/ Blog/ Cross-Repository Code Analysis

Blog

A repository is not an architecture.

Relationships between repositories are rarely written down in one place. They're scattered across source code, build metadata, configuration, generated files, API specs, framework conventions, and knowledge that exists only in someone's head.

Imagine you own four repositories: a Go backend, a TypeScript web application, a Kotlin Android app, and a Swift iOS app. You wrote most of the code yourself. You understand the products, the deployment, the data model, and the history behind the stranger decisions. Then someone asks a seemingly simple question:

If we change this API, which repositories and parts of the system will be affected?

You probably know the applications talk to the backend, and you might even remember roughly where the relevant clients live. But can you immediately list every call to that endpoint, every generated client, every gateway prefix, every request model, every event emitted as a consequence, and every copied protocol type that has to stay compatible?

Probably not.

You may understand the system very well and still be unable to answer the question precisely, because relationships between repositories are rarely represented in one place. They are distributed across source code, build metadata, configuration, generated files, API specifications, framework conventions, environment variables, deployment infrastructure, and knowledge that exists only in someone's head.

Cross-repository code analysis sounds like a search problem: find API calls, imports, events, and shared types across several repositories, then connect them. In practice, it is an evidence problem. The same dependency can be expressed differently across Go, TypeScript, Kotlin, and Swift, while two pieces of similar-looking code may have no directional dependency at all.

The repository boundary is visible in GitHub. The architectural boundary usually is not.

What is cross-repository code analysis?

Cross-repository code analysis reconstructs architectural relationships between independently versioned codebases. It connects evidence such as API client and server routes, package imports, event producers and consumers, generated clients, and shared contracts. More importantly, it must preserve the direction, strength, and confidence of each relationship.

At repository level, these relationships often sound obvious:

Statements like these are usually too vague to support an engineering decision. An agent changing a route, type, event, or package needs to know where the relationship is expressed, what kind of relationship it is, which direction it points in, and how certain the match is.

Why cross-repository dependency analysis is hard

Consider an HTTP interaction.

A TypeScript client might call:

client.ts
settingsClient.resolveTicket(ticketId)
// internally produces: POST settings/tickets/{id}/resolve

And the Go service might register:

server.go
POST /api/settings/tickets/:ticketId/resolve

The client knows nothing about the /api prefix, because it's baked into its configured base URL. In another system, the client might carry a gateway or BFF prefix the underlying service never registers at all. The parameter name differs, the prefix differs, and one side may use a generated client while the other registers the route through a framework. Both pieces of code still describe the same relationship.

Now suppose two loaded services expose compatible versions of the same route. Which one does the client actually use? The wrapper may name a service. The base URL may come from an environment variable. The OpenAPI file may or may not have a useful name. There may be no usable hint at all. A cross-repository linker can preserve that ambiguity, or quietly pick a provider. The second option produces a cleaner-looking graph, and it may also produce a wrong one.

Types of cross-repository dependencies

HTTP is only one kind of evidence. A repository can depend on another through an imported package, a Kafka topic, a gRPC call, generated code from a shared schema, or protocol types copied from another repository years ago.

These signals do not mean the same thing.

An HTTP client calling a route served by another repository establishes direction: the caller depends on the provider. An import establishes direction too: the importer depends on the repository providing the package. A Kafka producer and consumer establish a meaningful flow direction, though that runtime relationship behaves differently from a synchronous request.

Shared code is more complicated. If two repositories both define a type called UserDTO, that proves almost nothing; it's a predictable name in a lot of systems. Even several unusually named matching protocol types could mean generated code, vendored code, deliberate copying, shared ownership of a schema, or just a vocabulary shared across the same domain.

Matching symbols don't tell you which repository depends on which. enola.'s cross-repository linker doesn't treat shared symbols as a directional dependency on their own. They can strengthen an edge already backed by directional evidence, like an import or API call. Without that evidence, it records a symmetric coupling instead of a traversable depends_on relationship.

That distinction becomes important as soon as the graph is used for reasoning.

Dependency edges compose. If repository A depends on B, and B depends on C, an impact traversal may reasonably conclude that a change in C can affect A through B.

Shared code does not compose in the same way. If B and C happen to contain copies of the same protocol code, that does not mean every repository depending on B also reaches C.

Turning similarity into dependency would do more than add one inaccurate connection. It could corrupt every path, impact analysis, blast-radius calculation, and architectural conclusion built on top of it.

Detecting that two things are related is not the same as understanding the nature of their relationship.

How cross-language code analysis works

Multi-repository architecture gets harder still when repositories use different languages, because the same architectural concept looks different in every ecosystem. A server route might be expressed through:

An HTTP client might appear as:

Connecting these directly as source text is fragile. Syntax and framework conventions differ, and the architectural meaning can hide behind generated code, wrappers, annotations, helper functions, or configuration. Even when the runtime relationship is identical, the implementations can look unrelated.

enola. extracts language-specific code into language-independent architectural facts first. A Retrofit annotation and a Go router call can both become route facts with properties like role, method, path, framework, repository, and source. Imports, symbols, storage access, dependencies, and calls are all represented through the same shared fact model.

Only after that normalization can cross-repository linking happen.

The linker doesn't need to understand every detail of Kotlin, Swift, TypeScript, Go, Java, or Ruby. The language extractors have already translated those details into a common architectural vocabulary.

Cross-language static analysis becomes tractable when language-specific syntax stops being the unit of comparison. The linker compares architectural meaning rather than textual resemblance.

Matching API routes across repositories

Normalizing the HTTP method and path, comparing both sides, and declaring the problem solved would be tempting. Real systems make that insufficient.

A server may expose:

server route
/api/settings/tickets/{}/resolve

while a client calls:

client call
settings/tickets/{}/resolve

Or the client may call:

client call
/gateway/settings/tickets/{}/resolve

while the underlying service only knows about:

server route
/tickets/{}/resolve

The repository implementing the client may know about the gateway. The repository implementing the service may not.

A route matcher therefore has to tolerate base paths and prefixes on either side while still avoiding false positives. enola. compares trailing path segments so it can connect routes across gateway and service boundaries, but it also requires enough shared structure to avoid producing dependencies from generic endpoints such as:

Those paths appear everywhere. Matching them across repositories would create noise rather than architecture.

The matcher must also preserve different confidence levels. A client route matching the complete path of one unique provider is stronger evidence than a suffix match involving inferred placeholders. A provider selected among several candidates using a service hint is useful evidence, but it is not equivalent to an unambiguous match.

enola. represents that difference instead of pretending every generated edge has the same certainty. A relationship may be verified when the available evidence is strong and unique. It may be probable when matching requires normalization, suffix comparison, inferred placeholders, or provider disambiguation.

This distinction prevents downstream users from treating an informed approximation as an established fact. Precision does not require every answer to be certain; it requires the system to preserve what it knows, how it knows it, and where uncertainty remains.

Measuring what stays unresolved

Architectural tools often present whatever they detect as though it were the complete system.

Suppose a repository contains 100 HTTP client call sites and the linker resolves 70 of them to loaded services. Showing those 70 edges without saying anything about the remaining 30 would produce a clean and useful-looking graph. It would also encourage a dangerous conclusion: that the repository has exactly those 70 internal relationships.

enola. tracks the client calls it detected, how many resolved, how many clearly target external services, and how many stay unresolved. It can also explain why a call wasn't linked:

The absence of an edge does not automatically mean the absence of a dependency. It may represent a visible blind spot.

This changes how partial cross-repository analysis should be understood. Imagine that a system can reconstruct 70% of the real relationships. I am using 70% as an example, not as a measured enola. benchmark.

Compared with a perfect architectural model, that result is incomplete. Compared with what an AI coding agent normally starts with, it is transformative.

Without a structural map, the agent begins at zero. It must discover repositories, search each one, interpret several languages and frameworks, follow wrappers, inspect generated clients, understand configuration, and decide whether every apparent match is real.

With a partial but deterministic cross-repository dependency graph, it begins with known relationships, the evidence behind each one, their direction and confidence, and a list of unresolved calls together with the reasons they remain unresolved.

A graph that finds 70% and exposes the missing 30% is more trustworthy than a system that appears to find 95% by quietly inventing some of the edges.

A partial graph with visible blind spots is still a map. A confident answer assembled from unstructured exploration may be nothing more than a plausible story.

Why AI coding agents need this

Cross-repository edges become ordinary facts in enola.'s architecture graph. Directional relationships materialize as depends_on relations between repository-level service nodes, and because those relations use the same graph model as everything else, traversal, path finding, fact queries, and impact analysis all become cross-repository-aware without any tool needing its own multi-repo discovery logic.

That matters during the decisions an agent makes before and while changing code.

Suppose an agent receives this task: add a new notification preference to the user settings API.

Without cross-repository structure, it may find the backend endpoint, modify the Go request type and handler, update the database logic, run the backend tests, and conclude that the task is complete.

The running system may also contain:

The biggest risk is not that the agent overlooks one file. It is that it misunderstands the boundary of the change.

With cross-repository facts available during planning and decision making, the agent can ask better questions:

The graph doesn't make the decision for the agent. It changes the evidence available while the agent makes it, which is the difference between giving an agent more context and giving it structure.

Why AGENTS.md and CLAUDE.md aren't architecture models

Skills, prompts, AGENTS.md, and CLAUDE.md are useful. They can describe repository conventions, build commands, testing expectations, coding styles, known constraints, and workflows that cannot be reliably extracted from source code.

A file can tell an agent: when changing the settings API, remember to inspect the mobile applications.

That is valuable advice, but it does not identify which mobile repositories call the endpoint, which methods and paths they use, whether the calls are generated or handwritten, whether another repository consumes a related event, or which apparent relationships are ambiguous.

A prompt can instruct an agent to inspect every affected repository. It cannot make the agent know which repositories are affected.

Natural-language instructions also come without a guarantee that the agent will apply them exactly as their author intended. It may fail to load the relevant file, misunderstand a sentence, lose an instruction within a long context, prioritize conflicting information, or fail to follow the guidance during one of many intermediate decisions.

This is not a criticism of a particular model. It is a consequence of asking a probabilistic system to interpret and apply natural-language guidance.

Deterministic structure has a different role. A prompt can tell the agent how it should behave; a structural graph can give it concrete facts to behave upon.

The two belong together. Instructions provide intent, conventions, and local knowledge. Architecture facts provide stable evidence about the actual code and the relationships between repositories.

They are not interchangeable.

Repository boundaries are not architecture boundaries

A repository is a convenient unit for version control, permissions, ownership, deployment, and team organization. Sometimes it also represents a clean architectural boundary. Often, it does not.

The real system exists across those boundaries. Its architecture is formed by calls, imports, events, shared contracts, storage access, generated clients, and dependencies that may be implemented in completely different languages and frameworks.

Reconstructing those relationships is difficult even for the people who created the system. Expecting an agent to rediscover them correctly from scratch during every task is not a serious strategy.

The objective is not to convince an agent that it understands the whole system. It is to stop forcing it to reconstruct the whole system every time it needs to make a decision.

That requires more than repository descriptions and prompts. It requires a stable structural representation of the relationships we can establish, honest confidence about the relationships we can only approximate, and explicit visibility into the parts we still cannot resolve.

Repositories may be separate.

Their architecture is not.

FAQ

What is cross-repository code analysis?

It reconstructs architectural relationships between independently versioned codebases: API routes, package imports, event producers and consumers, generated clients, and shared contracts. The hard part is preserving each relationship's direction, strength, and confidence, since the same dependency is expressed differently in every language and framework.

Why is cross-repository dependency analysis difficult?

Because the same HTTP call looks different on each side of the wire. A client's base URL can hide a prefix the server never registers, a gateway can add one the backend doesn't know about, and when two services expose compatible versions of the same route, nothing in the code may say which one a given client actually targets. A linker can guess and get a cleaner graph, or preserve the ambiguity and get a correct one.

Does shared code between repositories always mean a dependency?

No. Two repositories both defining a UserDTO type proves almost nothing by itself, since it's a common name across many systems. Matching symbols can strengthen a relationship already backed by directional evidence like an import or API call, but without that evidence they only establish a symmetric coupling, not a traversable dependency.

Why isn't an AGENTS.md or CLAUDE.md file enough for multi-repo context?

Those files can tell an agent to check the mobile apps when it touches an API. They can't tell it which mobile repositories actually call that endpoint, which methods and paths they use, or which relationships are ambiguous, because that's not something natural-language instructions reliably encode. Instructions provide intent; a structural graph provides the evidence to apply it to.

What happens to cross-repository client calls that can't be resolved?

enola. records them as unresolved instead of dropping them, with a reason attached: no usable HTTP method, a path too generic to match safely, a matching path served under a different method, no known server route sharing enough of the path, or insufficient evidence to pick a provider among several candidates.

enola. is open source under Apache 2.0: free, local, no data leaves your machine.

shell
curl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | sh