enolaBlogWhat a Dependency Graph Can and Cannot Tell You

Blog

What a dependency graph can and cannot tell you

Gert8 min read

A dependency graph represents dependency relationships among parts of a system. It is usually directed, because "A depends on B" does not imply that B depends on A. The nodes might be packages, files, functions, routes, tables, or services, and depending on the problem, the graph may also carry undirected, typed, weighted, or otherwise annotated relationships alongside the directed ones.

In a directed dependency graph, an edge from A to B means A depends on B according to a rule chosen by whoever built the graph: A imports B, calls B, must be built after B, or sends requests to B. A graph does not discover one universal relation called "dependency." It records specific relations from specific evidence, and to interpret it you need to know what its nodes and edges mean, how those edges were extracted, and what the extractor could not see.

Not necessarily a DAG

A directed acyclic graph, or DAG, is a directed graph containing no directed cycles. DAGs can be topologically sorted, which is why they're useful for build ordering and scheduling. Dependency graphs in general are not DAGs, and whether cycles are allowed depends on the system:

  • Go forbids a package from importing itself directly or indirectly.
  • npm can install cyclic package dependencies.
  • Cargo normally rejects package cycles, but permits some involving development dependencies because the resulting build artifacts stay acyclic.
  • A call graph can contain recursion by design.

A cycle has no universal meaning. It may be invalid, harmless, intentional, or a sign of coupling. "This graph contains a cycle" is a fact. "This cycle is a defect" requires context.

The node and edge types define the graph

"Dependency graph" is an umbrella term, not one standard software model. More specific names describe different nodes and relations:

  • Package dependency graph. Connects packages or package versions using requirements from manifests, lockfiles, or package metadata.
  • Import graph. Connects modules or files that import one another.
  • Call graph. Connects callers and possible callees.
  • Program dependence graph. Represents control and data dependencies inside a program.
  • System dependence graph. Extends that analysis across procedure boundaries.
  • Service dependency graph. Connects deployed components through calls, messages, or shared infrastructure.

"Package-level" and "code-level" describe granularity, but they aren't a recognised two-part taxonomy. A package graph and a call graph answer different questions because they model different entities and relations, not simply because one is coarse and the other fine. A software architecture tool may combine several graph types; its model is defined by concrete questions: what does each node represent, what does each edge assert, is the edge declared, parsed, resolved, or inferred, which languages and frameworks does the extractor understand, and how are ambiguous or unresolved targets reported.

What reverse traversal proves

Suppose the graph contains these edges:

edges
checkout -> pricing -> tax

A reverse traversal from tax reaches pricing and checkout. It proves that those nodes can reach tax through edges in this graph. It does not prove that changing tax will break either one.

Forward edges vs. reverse traversal

Three nodes, checkout, pricing, and tax, connected by two forward dependency edges. A dashed reverse-traversal path runs from tax back through pricing to checkout, showing which nodes a reverse query reaches, not which ones would break. declared edges checkout pricing tax reverse traversal from tax reaches pricing and checkout
The forward edges are what someone declared: checkout depends on pricing, pricing depends on tax. The dashed path is what a reverse query returns: everything that can reach tax. Reachability is not a prediction that either caller breaks.

A caller may be unaffected by a compatible implementation change. A node missing from the result may still depend on tax through reflection, runtime dispatch, generated code, a dynamically constructed URL, or a framework convention the extractor does not support. "Blast radius" is shorthand for a candidate impact set, not a failure prediction. Its coverage is bounded by the graph, the selected relation types, and the traversal depth.

How software graphs are built

Software graphs combine several kinds of evidence, and each stage can lose information or introduce a wrong edge.

Declarations and configuration provide imports, package requirements, aliases, workspaces, project references, and framework settings. These are authoritative for some questions, such as which package requirements were declared or which versions a lockfile resolved, but they don't prove what was installed or describe every runtime interaction.

Parsed source provides symbols, calls, inheritance, implementations, route declarations, queries, and other language constructs. Parsing is more precise than searching text, but parsing alone does not resolve every target.

Linking and framework analysis connect the extracted facts: resolving a call through an imported alias, composing a route prefix registered in another function, or matching a client request with a server endpoint.

Reflection, metaprogramming, dependency injection, interface dispatch, generated files, and runtime configuration all complicate static resolution, and language and framework support varies by tool. Completeness should not be assumed. A useful tool reports unresolved edges and coverage gaps instead of turning "not observed" into "does not exist."

What dependency graphs are good for

Within those limits, dependency graphs support several concrete tasks.

Ordering. An acyclic build or task graph can be topologically sorted. Independent nodes have no dependency ordering between them, although resource constraints may still prevent parallel execution.

Cycle detection. Strongly connected components identify groups of nodes that can reach one another. Whether that's actionable depends on the edge type and ecosystem.

Change investigation. Reverse traversal finds known dependents to inspect or test after a change. It narrows the search; it does not replace compilation, tests, or runtime verification.

Path finding. A path between two nodes explains one recorded chain of relationships. Absence of a path means only that the graph contains no such path within the query's limits.

Structural comparison. Two snapshots can show that a change added or removed nodes, edges, cycles, or other measured properties, which is different from predicting whether the software still behaves correctly.

Where enola. fits

enola. builds a heterogeneous, directed, typed software graph. Its nodes include modules, symbols, routes, storage objects, dependency records, and repositories. Its traversable edges have a source, a target, and a relation such as declares, imports, calls, implements, handled_by, or depends_on. Symmetric evidence, such as shared code between repositories, is recorded as coupling rather than converted into a directional dependency.

Parsing is one stage of the process. An abstract syntax tree can show that a file contains client.get('/users') or foo.bar(). It does not, by itself, establish which server route handles the request or which declaration bar names. Parsing code is not the same as mapping architecture covers that resolution problem in more detail.

generate_snapshot runs a fixed pipeline:

generate_snapshot pipeline

A vertical sequence of seven stages: walk, extract, index facts, link, build the graph index, explain, and render and persist. Each stage operates on artifacts produced by earlier stages. The final stage, which writes the snapshot and llm_context.md, is highlighted. 01 Walk Selects repository files via exclusions and discovery rules → file list 02 Extract Language and spec extractors parse source, manifests, APIs → typed facts 03 Index facts Stored by kind, file, name, and repository → normalized model 04 Link Binders resolve references, including cross-repo signals → resolved edges 05 Build graph index Forward and reverse adjacency, plus synthetic bridges → adjacency index 06 Explain Deterministic analyses: Tarjan's algorithm, depth, hotspots → findings 07 Render and persist Writes artifacts and a content-derived snapshot ID → snapshot + llm_context.md Same seven stages whether called once locally or wired into CI.
Each stage operates on artifacts produced by earlier stages. Resolution begins after source extraction; cross-repository linking happens after the participating repositories have been indexed.

Forward and reverse indexes support queries in either direction over the same recorded relations. traverse walks that index, find_path searches for a path between nodes, and impact_analysis follows reverse edges to find recorded dependents within a configurable depth. Before an edit, an agent can query the current structure. After an edit, enola. can generate another snapshot and compare it with a pinned baseline: diff_snapshot reports measured architectural changes such as added or removed facts, coupling, and findings, instead of repeating every pre-existing finding.

Those operations answer graph questions: which measured nodes depend on this one, is there a recorded path from A to B, did this change add a cycle or another measured relationship. They do not answer "what will break?" with certainty. The result is a static model of the code the extractors support and the edges they resolved; dynamic behavior, unsupported constructs, ambiguous targets, and extractor defects can produce missing or unresolved relationships, which is why impact_analysis defaults to a bounded traversal rather than an unlimited search.

The coverage gap this post keeps returning to is not incidental for enola.: every snapshot carries a receipt (version, git ref, dirty-tree flag, extractor set, and a sha256 snapshot ID over the facts), and coverage_report states, per service, how many outbound calls it found, how many it resolved, and how many it could not, rather than presenting an unresolved edge as one that doesn't exist. Enola is an architecture fitness sensor covers the receipt and the determinism testing behind it in more detail.

enola. is open source under Apache 2.0. Analysis runs locally; source code is not uploaded. Install the binary:

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

Then run one read-only command against a repository you already have, with no baseline and no repository artifacts written:

shell
enola --explain /path/to/your/repo

If you want to see how it stacks up against the other open-source code graphs first, that comparison is here, including where each of the others is the better pick.

FAQ

Is a dependency graph the same thing as a call graph?

No. A call graph is one specific kind of dependency graph: it connects callers to possible callees. Import graphs, package dependency graphs, program dependence graphs, and service dependency graphs are other specific kinds, each modeling a different set of nodes and edges rather than a coarser or finer view of the same one.

Are dependency graphs always DAGs?

No. Cycle rules vary by ecosystem: Go forbids a package from importing itself, Cargo normally rejects package cycles but allows some through development dependencies, npm can install cyclic package dependencies, and a call graph can contain recursion by design. A cycle is a fact about the graph; whether it's a defect depends on the system it came from.

Does a dependency graph prove that changing one component will break another?

No. Reverse traversal proves reachability through recorded edges, not a failure prediction. A caller can tolerate a compatible change, and a dependency the traversal missed, through reflection, runtime dispatch, or an unresolved framework convention, is still a real dependency the graph simply didn't record.

What is blast radius if it is not a guaranteed impact prediction?

A candidate impact set: whatever a traversal found reachable, bounded by the graph's coverage, the relation types it followed, and how deep it searched. Treat it as a list of what to check, not a guarantee of what will fail.

How does enola build its dependency graph?

A fixed seven-stage pipeline behind one command, generate_snapshot: walk the repository, extract typed facts, index them, link references across files and repositories, build forward and reverse adjacency indexes, run deterministic explainers, then render and persist the snapshot with a content-derived snapshot ID.