What CodeGraph Knows: Nodes, Edges, and Evidence

CodeGraph’s graph is not a prettier file tree. This chapter asks what the project treats as the smallest useful unit of code intelligence, how different relationships answer different engineering questions, and how provenance limits what a graph result can prove. The goal is practical: learn to read a graph-backed answer as evidence, not as an oracle.

A codebase is more than its files

A file is where source is stored. It is not necessarily the unit a reader needs. A single file can contain several classes, functions, routes, tests, and configuration-related declarations. Conversely, one user-visible behavior can cross many files. If a graph treated only files as its objects, it would know where text lives but not what the text means inside the project.

CodeGraph therefore represents both files and the symbols declared within them. Its node vocabulary includes functions, methods, classes, interfaces, properties, routes, and components, among other kinds. Each symbol carries identifying information such as its name, qualified name, language, source location, and—where available—signature, visibility, decorators, and return type. Those details are not decoration. They let a query distinguish a method from a file, one definition from another, and a symbol’s declaration from an incidental text match.

The distinction becomes concrete with an ordinary question: “What calls this function?” A file search can find the function’s name, but it cannot by itself say whether a match is a call, a declaration, a comment, or an unrelated symbol with the same spelling. A node gives the graph an object to start from. Its source location gives the reader somewhere to inspect. Its kind and qualified name give the query a better chance of choosing the intended definition.

A node is still not enough. CodeGraph also needs edges, because software behavior is expressed through relationships. A function can contain a nested declaration, call another function, import a module, implement an interface, or be referenced by a route. Those are different relationships with different meanings. The graph’s usefulness begins when it keeps the objects and their relationships separate instead of flattening everything into “these files mention similar words.”

erDiagram FILE ||--o{ NODE : defines NODE ||--o{ EDGE : sources NODE ||--o{ EDGE : targets FILE ||--o{ UNRESOLVED_REFERENCE : contains NODE ||--o{ UNRESOLVED_REFERENCE : anchors FILE { string path string language string content_hash string modified_at int node_count boolean generated } NODE { string kind string name string qualified_name int start_line int end_line string signature } EDGE { string kind int line string provenance string metadata } UNRESOLVED_REFERENCE { string reference_name string reference_kind string status int line }

The model is deliberately layered. A file owns source-level facts such as its path, language, content hash, modification time, and indexing errors. A node identifies a meaningful symbol or file-level object within that source. An edge relates two nodes and can carry a kind, source location, metadata, and provenance. An unresolved reference records a relationship the extractor noticed but the resolver could not safely complete. This separation lets later stages improve or qualify the map without confusing a missing link with a missing file.

It also makes the graph reusable. A browser view can display a symbol and its callers. An agent can request a flow between two symbols. An impact query can walk dependents. These are different views over the same underlying objects and edges, rather than separate indexes that may disagree about what a symbol is.

One graph, different relationship questions

The edge kind determines what a relationship means. A calls edge answers a control-flow question: one callable invokes another at a particular source location. An imports edge answers a dependency question: one source unit depends on another for names or declarations. extends and implements describe type relationships, which can matter even when no direct call is visible. A references edge can connect a route or framework declaration to the handler it names. contains describes lexical ownership rather than runtime use.

These distinctions are valuable because the same symbol can participate in several kinds of analysis. Consider a controller method. A route reference can explain how an external request reaches it. Call edges can show which service it invokes. An import edge can show how that service becomes available. A type relationship can point from an interface to an implementation. A dependency traversal can then estimate which tests or modules are downstream of a change. The graph does not need one universal notion of “related.” It needs enough typed relationships for the question to be explicit.

This also limits accidental conclusions. If a symbol is imported by a file, that does not mean every function in the importing file calls it. If a class implements an interface, that does not mean every method participates in the same runtime path. If a test is a dependent, that does not mean it covers every behavior of the production symbol. Typed edges preserve the distinction between “these objects are connected in one way” and “these objects are interchangeable for every question.”

Imagine changing a shared request validator. A caller view can show the code that invokes the validator. An import view can show which modules make it available. An implementation relationship can reveal alternate validators behind a common contract. An impact traversal can then expose the broader neighborhood, including tests or routes that may be affected. Those answers overlap, but none subsumes the others. The quality of the result comes from choosing the edge that matches the decision being made.

That is what makes impact analysis different from caller lookup. A caller query usually asks for the next layer in one direction. Impact analysis follows a transitive radius to estimate what a change may touch. Neither result is simply a list of files containing a matching word. One is a local relationship view; the other is a bounded traversal over the graph. The distinction matters when a developer is deciding whether a change is isolated or whether it crosses shared infrastructure.

Routes illustrate another difference. A URL pattern is not a function call written in the same syntax as the handler it reaches. Framework-aware resolution can represent the route as an object and connect it to the handler with a reference relationship. That lets a reader begin with an externally visible behavior—an endpoint or screen—and move inward to implementation. Without that typed relationship, the agent would have to search for a string, guess which function is registered, and reconstruct the binding manually.

The same principle applies to tests. A test that calls a symbol and a test file that merely mentions its name should not be treated as equivalent evidence. The graph can use actual relationships to identify dependents and likely affected tests, while still leaving final test selection and execution to the engineering workflow. The graph supplies a structured candidate set; it does not claim that a dependency walk has run the tests or observed runtime behavior.

The source location attached to an edge is part of this usefulness. A caller is more actionable when the graph can show the line where the call occurs. A route is more interpretable when the reader can locate the registration. A flow view can open each hop at the line that makes the next connection. This is why CodeGraph’s retrieval surfaces line-numbered source: the relationship is not only an abstract arrow. It should lead back to inspectable text.

File metadata supplies another kind of evidence. Content hashes, modification times, indexing timestamps, node counts, parse errors, and generated-file status help answer whether a file was actually considered and whether the current graph is likely to reflect the source on disk. These facts do not explain the program’s runtime behavior, but they explain the conditions under which the graph’s structural claims were produced. A relationship without source currency is weaker than the same relationship in a known-fresh file.

The result is a useful division of labor. Nodes identify what exists. Edges identify how things relate. Locations show where the relationship is grounded. File metadata describes the state of the evidence. Queries then choose which relationship to traverse. This is the operating model behind CodeGraph’s promise of surgical context: return the part of the map that answers the question, not every file that happens to share a word.

The database representation reinforces that division. Full-text search helps locate candidate symbols by names, qualified names, docstrings, and signatures, but search is only the entry point. Once a candidate is found, the graph can walk typed edges and return source at the relevant locations. This is why a name search and a flow query should not be judged by the same standard: the first narrows the field, while the second attempts to explain a relationship.

The file record also gives the graph a memory of its own conditions. A content hash can distinguish a changed file from the version that was indexed. An error record can show that parsing was incomplete. A generated-file marker can help retrieval avoid spending the answer budget on mechanical output unless the user asks for it. These facts do not make the underlying code more correct, but they stop the retrieval layer from treating every source byte as equally informative or equally current.

That matters for surgical context because a relationship without its conditions can be misleading. The most relevant edge in the database may still point to source that has changed on disk. A high-scoring generated file may still be the wrong explanation for a domain question. A node that looks unique by name may be ambiguous once its qualified name and language are considered. The graph’s metadata gives later stages the information needed to make those distinctions instead of hiding them inside a single relevance score.

Provenance is part of the answer

Not every edge can be extracted from an explicit call expression. Static parsing handles many direct forms well, but real applications also use callbacks, observers, framework conventions, interface dispatch, event channels, and cross-language bridges. CodeGraph can add selected relationships through resolver logic and synthesizer passes. The resulting edge may be useful enough to guide an agent, but it has a different origin from a direct AST-derived call.

The project marks such relationships with heuristic provenance and can retain metadata about the mechanism that synthesized them. In practical terms, the graph is saying: “This connection is supported by a recognized registration and dispatch pattern,” not “the source contains an ordinary call from A to B.” That distinction gives a reader a way to use the edge without forgetting how much interpretation stands between the source text and the displayed path.

A direct call edge is not automatically perfect either. Names can be ambiguous, definitions can move, generated files can create collisions, and a resolver may have to choose among candidates. But an indirect edge has an additional inference step. Provenance makes that step visible, which supports more careful actions: follow the path for orientation, inspect the wiring site, and use direct source or tests when the decision is consequential.

This is a useful balance between automation and inspection. If every relationship required a person to reconstruct it from scratch, the graph would not save discovery work. If every relationship were presented as equally certain, the graph would make inspection harder by creating false confidence. Provenance lets CodeGraph carry a useful interpretation forward while showing the point at which a reader should slow down.

The inverse case is just as important. CodeGraph may retain an unresolved reference when it cannot safely connect the observed name to a definition. The missing edge can result from a computed member, a runtime key, reflection, a metaprogrammed method, an anonymous handler, an unsupported framework shape, or a genuine ambiguity. Treating every unresolved reference as failure would encourage the system to over-link. Treating it as proof that no behavior exists would hide the frontier. The more honest interpretation is that the available static evidence ends there.

This is why a boundary can be more useful than a guess. Suppose a flow reaches a dispatch through a computed key. A guessed edge could lead the agent into an unrelated handler and make the final explanation look coherent while being wrong. A boundary report can instead identify the dispatch site, show a literal key if the source provides one, and list plausible candidates without writing any of them into the graph as fact. The reader receives a place to continue investigating and a reason not to overstate the path.

The same discipline protects the system from broad, low-confidence fan-out. A generic event name or a callback field used by many unrelated components can produce a large set of superficially possible targets. Connecting all of them would make the graph appear complete while making every flow less informative. Precision gates, confidence thresholds, and explicit refusal are ways of preserving the meaning of an edge. A smaller graph with defensible links is more useful for surgical context than a larger graph whose arrows cannot be trusted.

That does not make CodeGraph a passive database. Its resolvers and synthesizers are active interpretations of source structure. The important boundary is that the interpretation remains inspectable and qualified. A reader should be able to ask whether an edge came from a direct relationship, a recognized framework convention, or a heuristic bridge—and what the system did when none of those was safe.

The practical reading rule is simple: first identify the node, then identify the edge, then inspect its provenance and source location. Ask whether the relationship answers the question being asked. Finally, ask whether the file is current and whether the conclusion needs behavioral validation. A graph result becomes trustworthy not because it hides uncertainty, but because it attaches uncertainty to the exact place where reasoning begins.

That vocabulary turns CodeGraph from a mysterious source of context into an interpretable evidence system. The next question is how the project constructs these nodes and edges from a repository in the first place: how parsing, storage, resolution, and context building divide the work, and how the boundaries described here arise during that pipeline.

References