Skip to main content

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 origininternal, 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.