DEV Community

Gert
Gert

Posted on

Building Enola, Part 3: Linking Architecture Across Repositories

The earlier article, Cross-repository code analysis for multi-repo architectures, explains why repository boundaries hide system dependencies. This part describes how Enola reconstructs those dependencies.

The process starts after extraction. Language-specific extractors have already converted source code and contracts into a shared fact model. The cross-repository linker reads those facts, collects evidence from protocol-specific signals, and materializes only the relationships that evidence supports.

Linking starts with normalized facts

Suppose a TypeScript repository contains a client call:

route  /api/orders
       role=client
       method=POST
       source=typescript-http-client
Enter fullscreen mode Exit fullscreen mode

A Go repository exposes:

route  /api/orders
       role=server
       method=POST
       source=go-router
Enter fullscreen mode Exit fullscreen mode

The linker does not compare TypeScript and Go syntax. It compares two route facts expressed in the same vocabulary.

That separation keeps the responsibilities clear:

  • extractors interpret languages and frameworks;
  • binders resolve facts that require a wider extraction scope;
  • cross-repository signals identify evidence between repositories;
  • the linker turns accumulated evidence into graph facts and relations.

Adding support for a new client framework belongs in an extractor. Adding a new way to establish a relationship between repositories belongs in a cross-repository signal.

Signals contribute evidence

A signal reads the complete multi-repository fact set and reports evidence through a common interface. It does not create dependency facts directly.

Enola runs two signal phases.

Directional signals establish a consumer and provider. HTTP calls, package imports, Kafka topic ownership, and GraphQL consumption belong here.

Symmetric signals identify coupling without inventing a direction. Shared code belongs here. If two repositories contain matching protocol symbols, Enola can record that relationship as coupling. It becomes supporting evidence for a directional dependency only when another signal has already established the direction.

This matters because dependency edges compose during traversal. Symmetric similarity does not. Treating shared symbols as depends_on edges would corrupt paths and impact analysis.

HTTP route matching

The HTTP signal indexes server routes by normalized method and path. It then evaluates every client route against that index.

Exact string comparison is insufficient. A client may call:

POST settings/tickets/{id}/resolve
Enter fullscreen mode Exit fullscreen mode

while the server exposes:

POST /api/settings/tickets/:ticketId/resolve
Enter fullscreen mode Exit fullscreen mode

Enola normalizes parameter syntax and leading slashes. It also compares trailing path segments so a client with a base-path or gateway prefix can match the route implemented by the service.

Suffix matching needs limits. Generic paths such as /health, /status, and /metrics occur in many services. Thin or ambiguous evidence is left unresolved rather than assigned to an arbitrary provider.

When several repositories expose a matching route, Enola can use a target hint retained by the client extractor. A strong hint can disambiguate providers; a weak or missing hint cannot.

The resulting confidence reflects the match:

  • verified when one provider matches the complete path without inferred placeholders;
  • probable when the match depends on suffix normalization, placeholders, or provider disambiguation.

gRPC uses wire identity

The same route-linking signal also handles gRPC facts.

A .proto service produces server routes using the wire path:

/users.v1.UserService/GetUser
Enter fullscreen mode Exit fullscreen mode

Recognized Go, Python, and TypeScript gRPC client calls produce client routes with the same identity. Matching the declared RPC identity is stronger than matching similar URL fragments, so the relationship can be verified.

Generated-client support is explicit. Enola recognizes supported gRPC client forms and openapi-typescript output; it does not infer arbitrary generated clients from type or schema similarity.

Kafka uses topic ownership

Kafka has no request route to join. Enola instead uses topic facts and an ownership convention.

For a topic such as:

orders.order_created
Enter fullscreen mode Exit fullscreen mode

the leading segment identifies the owning service. If that name resolves to a loaded repository, a consumer of the topic contributes evidence for a dependency on the owner.

The edge is consumer to producer, matching the direction used for HTTP client to server dependencies. The topic name remains attached as evidence.

This is intentionally narrower than claiming that any equal pair of topic strings proves a producer-consumer relationship. Computed topic names, wrappers the extractors do not recognize, and naming schemes without an identifiable owner remain outside cross-repository resolution.

Materializing the dependency

Signals may contribute several kinds of evidence for the same repository pair. The accumulator combines them before producing one cross-repository dependency fact.

A materialized result can contain:

dependency  web -> orders
            type=cross_repo
            via=[http-client]
            confidence=verified
            endpoint_count=1
            endpoints=["POST /api/orders"]
Enter fullscreen mode Exit fullscreen mode

The corresponding service relation makes the edge traversable:

web --depends_on--> orders
Enter fullscreen mode Exit fullscreen mode

Evidence is grouped by type. Depending on the contributing signals, a dependency can retain endpoint, import, topic, or symbol counts and samples. If several signals support the same direction, their evidence is combined rather than emitted as competing edges.

Coverage records what did not resolve

For supported outbound route signals, detection happens before matching. Each service receives a coverage tally containing detected, resolved, external, declared, and unresolved calls.

edge_coverage:
  edge_type: http_client
  detected: 4
  resolved: 2
  external: 1
  unresolved: 1
Enter fullscreen mode Exit fullscreen mode

An unmatched call to a known third-party host is external, not an internal blind spot. A call attributed only through a declared seam is labeled as declared but is not promoted into a resolved edge. Everything else remains visible as unresolved.

enola coverage cluster.yaml
Enter fullscreen mode Exit fullscreen mode

The command reports coverage and always exits 0. A team can promote new coverage findings into its change policy with:

enola check --fail-on=coverage
Enter fullscreen mode Exit fullscreen mode

That fails on qualifying new findings. It does not certify that every system dependency has been discovered.

Reproducibility belongs to the snapshot

Dependency facts retain their resolution channel, confidence, and representative evidence. The wider provenance belongs to the snapshot receipt, which records:

  • Enola and extractor versions;
  • repository revisions and dirty state;
  • enabled extractors and effective configuration;
  • a content-addressed snapshot ID.

This division avoids repeating snapshot metadata on every edge while preserving the context needed to reproduce the graph. Enola does not yet store a complete normalization trace for each individual match.

The result

Once materialized, cross-repository dependencies use the same graph model as local relationships. Traversal, path finding, and impact analysis can cross repository boundaries without implementing their own discovery logic.

The graph remains bounded by its inputs and extractor coverage. It shows supported relationships, their evidence, and visible blind spots. Source inspection remains the final verification step.

The next problem is comparing that graph over time. Part 4 covers Enola's ability to make architecture changes testable with content-addressed snapshots, comparable analysis conditions, and evidence that survives CI.

Top comments (0)