Skip to main content

Overview

Eight adapters, one interface:

LanguageClassProject markerExtensions
PythonPythonAdapterpyproject.toml, setup.cfg, setup.py.py, .pyi
TypeScriptTypeScriptAdapterpackage.json, tsconfig.json.ts, .tsx, .mts, .cts
GoGoAdaptergo.mod.go
RustRustAdapterCargo.toml.rs
PHPPhpAdaptercomposer.json, or any .php.php
CCAdapterCMakeLists.txt, Makefile, meson.build.c, .h
C++CppAdaptersame.cpp, .cc, .cxx, .hpp, .hh, .h
YAMLYamlAdapterany .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:

  1. 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.
  2. 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.
  3. Boundaries, so BOUNDARY nodes 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 under src/, with inline mod 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.