How Relationships Become Evidence

In Code-Graph-RAG, an edge is useful only when its meaning is clear enough to inspect. This chapter focuses on what sits behind a graph relationship: where a call or import occurred, how a target was resolved, and how a value can move between code and external resources. The goal is to distinguish a graph that merely connects names from one that tells a reader what was observed, what was inferred, and what remains outside coverage.

An edge has a place and a reason

The simplest graph says that one function calls another. Code-Graph-RAG records more context when the analyzer can provide it. CALLS, REFERENCES, INSTANTIATES, and IMPORTS relationships can carry the line and column span of the producing expression or statement. A call can also record how many arguments it received and which keyword names were used. An import can record the alias bound in the importing scope and the symbol imported from a module.

That extra information changes how a reader can use the result. Suppose a service method appears to call a repository operation. An endpoint-only graph can answer “these two functions are connected.” A site-aware graph can point to the call expression, show its argument shape, and distinguish a call from another relationship in the same neighborhood. If the method calls the operation twice, the graph can preserve two call sites rather than collapsing them into one pair of endpoints. A query looking for callers may want one row for the method; a query checking every invocation site needs the separate locations.

The same principle applies to imports. An importing module may bind a symbol under an alias, import several names in one statement, or import an entire module. If a tool later needs to rename or move a definition, the statement’s location and binding information are the difference between a targeted rewrite and a text search that changes the wrong occurrence. The graph is not storing syntax merely for decoration. It is retaining the evidence needed to connect a relationship to an action.

A relationship also has a resolution story. Code-Graph-RAG documents several states rather than compressing them into one confidence percentage. An exact call is bound through information such as scope, imports, types, or a signature. An overload result represents multiple same-named candidates that remain possible, so the graph can link one site to more than one definition. A heuristic result comes from a weaker name-based or wildcard-based rule. A trace_confirmed edge is a statically known relationship observed in a runtime trace, while a dynamic edge was supplied by runtime observation rather than matched to an existing static edge.

These labels answer different questions. An exact edge supports a narrower claim about binding than a heuristic edge. An overload fan-out preserves possible targets, but it should not be narrated as proof that every candidate executes. A runtime-only edge says that a traced workload observed a route; it does not say that the route is present under all inputs. The distinction is especially important for maintenance operations. The project’s rename tooling refuses sites resolved only heuristically, through overload fan-out, or only by a trace unless the user explicitly permits that uncertainty. It is safer to leave a human with a review task than to turn an ambiguous relationship into a confident rewrite.

The frontend model reinforces this discipline. A compiler-backed frontend can report that a site binds to one declaration or that it definitely leaves the repository. If the site is not described by the frontend, the system falls back to its existing heuristic. If several targets are plausible, the frontend protocol prefers omitting a semantic fact over writing a wrong one. “The compiler found no first-party target” and “the compiler did not analyze this site” are therefore separate cases. One is evidence about the call; the other is a reason to be cautious.

The important reading habit is to keep endpoint, site, and resolution together. Consider a natural-language query asking which functions call an authentication service. The returned names are a starting set. The relationship type tells us whether a row is actually a call rather than an import or definition. The site span tells us where to inspect. The resolution label tells us whether the binding is exact, multiple, heuristic, or runtime-derived. The source file then confirms whether the graph’s interpretation matches the code. A fluent explanation that drops those distinctions may be easier to read, but it carries less evidence.

The graph also records relationships that are not calls. A function can reference another callable as a value, such as a callback stored in an object, without invoking it at that location. An INSTANTIATES edge records construction separately from a method call. An import can connect modules without proving that the imported symbol is executed. Treating every edge as interchangeable would make a graph appear more decisive while making its answers less accurate.

erDiagram MODULE { string qualified_name boolean flow_covered } FUNCTION { string qualified_name } RESOURCE { string kind string identity } MODULE ||--o{ FUNCTION : DEFINES MODULE }o--o{ MODULE : IMPORTS FUNCTION }o--o{ FUNCTION : CALLS FUNCTION }o--o{ RESOURCE : READS_FROM FUNCTION }o--o{ RESOURCE : WRITES_TO RESOURCE }o--o{ RESOURCE : FLOWS_TO FUNCTION }o--o{ FUNCTION : FLOWS_TO

The diagram uses the graph’s main entities and relationship families, but it intentionally leaves site properties on the relationships rather than turning every call site into a separate node. The CALLS relationship carries location and resolution when available. The two FLOWS_TO lines represent different endpoint shapes under one relationship type: resource-to-resource flow and code-to-code flow whose kind and via properties say whether taint crossed as an argument or a return. The flow_covered attribute matters when interpreting a negative result, not when deciding whether a function exists.

How a value becomes a path

Code-Graph-RAG’s data-flow feature begins with a different kind of node: a synthetic Resource representing something outside ordinary program variables. A file, environment variable, network endpoint, database, socket, or standard stream can be modeled as a resource. A call such as os.getenv("K") can read from an environment resource identified as ENV::K; a print operation can write to a standard-output resource whose target is dynamic when no literal identity exists.

Two direct relationships describe the boundary crossing. READS_FROM connects the immediate function, method, or module containing a source call to the resource it reads. WRITES_TO does the same for a sink. These edges answer direct-touch questions: which callable reads a particular environment variable, or which function writes to a file. They do not by themselves say that the value read at one point is the value written at another point.

FLOWS_TO supplies that missing value relationship. Its first shape is resource to resource. If a function reads an environment variable into a variable and later prints that variable, the graph can connect the environment resource to the standard-output resource. This is a provenance statement: a modeled value that originated at the first resource reached the second modeled resource.

Its second shape crosses into a callee. If a tainted local value is passed as the first positional argument to a first-party helper, the edge runs from the caller to the callee and carries a kind such as arg with a conduit such as arg:0. A keyword argument records the keyword name instead. The edge says that the value entered the callee through an argument slot; it does not say that the callee necessarily writes the value anywhere.

Its third shape crosses out of a callee. If a helper returns a value that originated at an environment resource, and a caller receives that return value, the flow edge runs from the callee to the caller with a return marker. The direction may look surprising because the caller is the function whose body contains both operations. The rule is simpler than the picture: a value entering a callee travels from caller to callee; a value leaving through a return travels from callee to caller. The assignment that gives the returned value a local name does not reverse the relationship. It only makes the value available for later propagation.

The analyzer follows these relationships with deliberately limited rules. A plain assignment copies a value’s origin from one variable to another. Rebinding a variable to a new source replaces the previous origin. Assigning a clean value kills the tracked taint, so a later print does not produce a false source-to-sink edge. Merely reading one resource near an unrelated untainted call is not enough to create a flow. Co-occurrence is not provenance.

The implementation model is a forward walk through a function body. The analyzer keeps a table that maps live variables to their origin resources. When it sees a source, it records the origin. Each copy carries the same origin forward, and each clean overwrite removes it. At a sink, the analyzer can look up the variable’s current origin without searching backward through every previous statement. Intermediate variables do not become graph nodes; the chain collapses to an edge between the resource endpoints it connects.

That approach has two benefits. It makes a long chain of assignments no more conceptually mysterious than a short one, and it makes the kill rule explicit. It also defines a boundary. This is not a complete symbolic execution engine or an unrestricted security analysis. The documented phase is conservative and can lose precision around control-flow alternatives, complex language semantics, or cases outside the registered source-and-sink model. Its value comes from giving a supported provenance question a visible, queryable answer, not from claiming that every information path in a program has been modeled.

Scope is part of the same contract. A read or write is attributed to the immediate enclosing function, method, or module. A nested function’s activity is not silently bubbled into the outer function. A resource handle created in one scope can be resolved when used in another documented scope, but the I/O edge is credited to the callable that actually performs the operation. These rules keep the graph’s attribution close to the code that owns the action.

The feature is also opt-in. The io capture group is excluded from the default capture set, so a graph built with ordinary settings does not automatically contain resource nodes or flow edges. Enabling it is a decision to spend analysis work and to interpret the resulting coverage. A project can contain files whose languages are parsed structurally but are not in the registered source-and-sink coverage. A missing edge in those files cannot support the same negative conclusion as a covered file with no path.

That is why the project exposes three flow verdicts. FOUND means that a FLOWS_TO path exists and the path can be returned. NO_FLOW means that no path was found and every relevant file-backed module is within the project’s flow-analysis coverage. UNKNOWN means that no path was found but some modules are outside coverage, so the system cannot rule out a path through them. The distinction is not pedantic. In a provenance or assurance question, silently converting UNKNOWN into NO_FLOW would manufacture confidence from an absence of evidence.

The same reading discipline applies to relationship evidence and value-flow evidence. First identify the claim: direct read, direct write, call-site binding, or end-to-end provenance. Then inspect the graph object that can support that claim. Next check its site information, resolution or flow properties, language and capture coverage, and source location. A FOUND flow path is meaningful evidence of a modeled route. It is not by itself proof of a vulnerability, an exploit, or every route the running program could take. An exact call edge is useful evidence of a static binding. It is not a guarantee that runtime dispatch cannot alter the behavior.

Once relationship semantics are read this way, the graph becomes more than a network of names. It becomes a record of how a fact was produced and how far that fact can travel. The next task is not to collect more edge types, but to decide whether a particular graph answer has enough provenance and coverage to support the conclusion a reader wants to draw.

References