Quick start
Analyse a project and look at what came out:
from callix import NodeKind, PythonAdapter
graph = PythonAdapter().analyze("path/to/project")
print(len(graph.nodes), len(graph.relations))
for node in graph.nodes_by_kind(NodeKind.FUNCTION):
print(node.qualified_name, graph.callers(node.id))
analyze() takes a path and returns a Graph. It is a pure function of the
source tree: no daemon, no cache directory, no side effects.
Swap the language
Every adapter shares one interface, so nothing above changes:
from callix import (
CAdapter, CppAdapter, GoAdapter, PhpAdapter,
PythonAdapter, RustAdapter, TypeScriptAdapter, YamlAdapter,
)
graph = TypeScriptAdapter().analyze("path/to/ts-project")
graph = GoAdapter().analyze("path/to/go-module")
graph = RustAdapter().analyze("path/to/workspace")
graph = PhpAdapter().analyze("path/to/php-project")
graph = CAdapter().analyze("path/to/c-project")
Each one discovers its own project roots — pyproject.toml, package.json,
go.mod, Cargo.toml, composer.json, CMakeLists.txt — and handles monorepos
with several of them.
Structure only
Symbol resolution is the expensive half. Turn it off when you only need the skeleton:
graph = PythonAdapter(resolve=False).analyze(root)
graph.metadata["resolver_status"] # 'unavailable'
You get PROJECT, MODULE, FILE, CLASS, FUNCTION, IMPORT and their
containment edges, but no CALLS, REFERENCES, HAS_TYPE or INHERITS_FROM
— those are what resolution produces.
Check the result before trusting it
A graph always tells you how complete it is — and for three languages the answer
is never ok, because PHP, C and C++ resolve through a symbol table rather than
a type checker:
graph.metadata["resolver_status"] # 'ok' | 'degraded' | 'unavailable'
graph.metadata["resolver_metrics"]
# {'queries': 28, 'resolved': 25, 'internal': 12, 'external': 13,
# 'unresolved': 3, 'seconds': 0.034, 'resolved_pct': 89.3}
Without this, a fast run that resolved nothing is indistinguishable from a fast
run that resolved everything. Pass strict=True to analyze() to raise
AdapterError instead of returning a degraded graph.
Save it
graph.to_json(indent=2) # str
Graph.from_json(text) # back again
old.diff(new) # structural diff
The format matches graphlens byte for byte, and node IDs are deterministic, so two runs over unchanged source produce identical output.