Overview
Eight adapters, one interface:
| Language | Class | Project marker | Extensions |
|---|---|---|---|
| Python | PythonAdapter | pyproject.toml, setup.cfg, setup.py | .py, .pyi |
| TypeScript | TypeScriptAdapter | package.json, tsconfig.json | .ts, .tsx, .mts, .cts |
| Go | GoAdapter | go.mod | .go |
| Rust | RustAdapter | Cargo.toml | .rs |
| PHP | PhpAdapter | composer.json, or any .php | .php |
| C | CAdapter | CMakeLists.txt, Makefile, meson.build | .c, .h |
| C++ | CppAdapter | same | .cpp, .cc, .cxx, .hpp, .hh, .h |
| YAML | YamlAdapter | any .yaml / .yml | .yaml, .yml |
YAML is the odd one out: it declares no symbols, so it produces boundaries and service wiring rather than functions and calls. It earns its place because an OpenAPI document states routes that no source file mentions.
The others discover every project root under the path you give them, so a monorepo with several packages works without configuration. A marker in the root you pass does not hide nested roots — otherwise a monorepo that is itself a package would swallow its own subprojects.
The three phases
analyze() always runs the same three phases, and the order is not cosmetic:
- Structure for every root, with no resolution. Nodes, containment and import edges are created here. This has to finish for the whole workspace first, or a cross-root definition would have nothing to bind to.
- Resolution, once for the whole call. One resolver serves every root: per-root resolvers would both lose cross-root references and re-index the workspace once per root.
- Boundaries, so
BOUNDARYnodes land in the graph after the resolver's edges.
Node schemes differ
Do not assume Python's shape carries over. A MODULE is:
- a dotted name in Python and TypeScript (
app.services.billing); - a package directory in Go, whose qualified name equals the import path — which is why internal imports bind by direct lookup;
- a module path in Rust (
crate::net::http), derived from the file's place undersrc/, with inlinemod foo { … }adding segments; - a namespace in PHP (
App\Service), backslash-separated at every level; - the directory in C and C++ — a namespace groups declarations but does not contain files, and namespaces live in the qualified names instead.
One constructor
Every adapter takes the same four keyword-only arguments:
Adapter(resolve=True, resolver=None, dep_parsers=None, boundary_extractors=None)
resolver replaces the language's native backend, dep_parsers replaces its
manifest reader, and boundary_extractors runs in addition to the built-in ones.
YamlAdapter takes the first and last only — there is no resolution phase to
redirect and no project-root manifest. See
Custom resolvers and parsers.
Resolver status
Every graph reports how complete it is:
graph.metadata["resolver_status"] # 'ok' | 'degraded' | 'unavailable'
unavailable means resolution did not run — either resolve=False, or the
toolchain the language needs was missing. degraded means it ran and produced
a partial answer. See Resolvers.