Core concepts
Graph
A Graph holds nodes keyed by ID and an ordered list of relations.
Order is part of the value: nodes come out in insertion order, so serializing
the same source twice gives identical bytes.
Adapter
An adapter turns a source tree into a graph. It owns project discovery, file collection, parsing, and the resolution pass. There is one per language, and they share the same surface — the same constructor and the same five methods:
Adapter(resolve=True, resolver=None, dep_parsers=None, boundary_extractors=None)
adapter.language() # 'python'
adapter.file_extensions() # {'.py', '.pyi'}
adapter.can_handle(root) # bool
adapter.collect_files(root) # list[Path]
adapter.analyze(root, files=None, *, strict=False)
Every constructor argument is keyword-only. resolver and dep_parsers replace
the language's built-in backend and manifest reader; boundary_extractors runs
alongside the built-in ones. YamlAdapter is the exception and takes the first
and last only, because YAML declares no symbols to resolve. See
Custom resolvers and parsers.
Occurrence
Adapters do not create call or reference edges while walking the syntax tree.
They record occurrences — a position plus a role (call, read, write,
annotation, base) — and a later pass asks the resolver where each one is
defined, turning the answer into an edge.
This split is why the same visitor serves both a structural run and a fully resolved one.
Resolver
A resolver answers "what is defined at this position". Each language has one,
and it is the only part that needs real type information. The result carries an
origin — internal, stdlib, third_party or unknown — which decides
whether the edge lands on a node in the graph or on an EXTERNAL_SYMBOL.
An edge is never dropped: a target outside the graph still gets a node.
How much a resolver actually knows varies by language, and the graph says which
you got in metadata["resolver_status"]: a real type checker for Python,
TypeScript, Go and Rust, a symbol table reporting degraded for PHP, C and C++.
See Resolvers.
Boundary
A boundary is a contract between services that no compiler checks: an HTTP
route, a gRPC method, a queue topic, a Temporal activity. Adapters detect both
sides — the server EXPOSES, the client CONSUMES — and the node's ID comes
from the mechanism and the normalized key alone.
That last detail is what makes cross-language analysis work: a Python server
declaring @app.get("/users/{id}") and a TypeScript client calling
fetch("/users/1") produce the same node, because both keys normalize to
GET /users/{}.
Deterministic IDs
Every node ID is sha256("{project}::{kind}::{qualified_name}")[:16]. Nothing
about the machine, the run, or the absolute path enters it. Two consequences:
- graphs from different runs and different machines can be diffed and merged;
- incremental updates are possible, because an unchanged declaration keeps its identity.