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 says how it was resolved — and for three languages the status 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, 'to_value': 2, 'unlocated': 1,
# 'resolved_pct': 89.3}
resolved means the backend answered, not that it found the callee: to_value
counts the calls that stopped at a variable, and every edge records in
metadata["resolution"] how it was found (definition, value, flow,
external, unlocated, name). 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, and read how accurate resolution is for
what each language's resolver follows.
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.