Skip to main content

Migrating from graphlens

callix is a rewrite of graphlens by the same author. The graph contract did not change, so most code moves across with an import swap.

What stayed the same

The same 14 node kinds and 12 relation kinds, the same deterministic ID formula, the same 1-based spans, and the same serialization format. A graph written by graphlens reads in callix and the other way round.

The structural half of the graph is byte-identical. That is not a claim, it is the project's test harness: both implementations analyse the same repository, both graphs are serialized with sorted keys, and the texts are diffed. Verified on apache/superset, colinhacks/zod, gin-gonic/gin, BurntSushi/ripgrep and astral-sh/ruff.

What changed

Packaging. graphlens was a workspace of nine Python packages with one adapter each. callix is one wheel with every adapter inside.

Resolvers. graphlens drove language servers over JSON-RPC and asked you to install them: ty, gopls, rust-analyzer, intelephense. callix links ty and typescript-go into the module, so Python and TypeScript need nothing. Go and Rust still need their toolchain, but no separate language server — gopls in particular is no longer required.

Adapter construction. There is no registry:

# graphlens
adapter = adapter_registry.load("python")()

# callix
from callix import PythonAdapter
adapter = PythonAdapter()

Writing an adapter. Adapters are Rust modules now, not Python classes, so a new language means a change to callix rather than a package of your own. The resolver and dependency-parser hooks are still Python objects, and unlike graphlens they are accepted by every adapter rather than by one — see Custom resolvers and parsers.

What is not here

graphlens-cli, the MCP server, the Neo4j backend, the HTML visualization, graphlens-link, and the C# adapter. callix produces the graph; the layers above it have not been ported.

PHP is here — it was missing when this page was first written — and C and C++ are new in callix with no graphlens counterpart. YAML is new too.

The one deliberate divergence

When a resolver finds a definition it cannot name, the synthetic EXTERNAL_SYMBOL needs a key. graphlens used {role}@{line}:{col} — no file path — so sites sharing coordinates across different files collapsed into one node. On apache/superset that merged 98% of external nodes: 75,806 of 77,041.

callix includes the project-relative path:

graphlens: read@399:60
callix: read@superset/commands/importers/v1/utils.py:399:60

Relative on purpose — an absolute path would stop IDs from matching between machines. The consequence is that these nodes have different IDs in the two implementations, and there are more of them. Edges, structural nodes and the resolved share are unchanged.

Two bugs fixed along the way

Go stdlib classification. graphlens resolves symlinks on the target path only and reads GOROOT verbatim. On Debian-like installs /usr/lib/go-X/src is a symlink to /usr/share/go-X/src, so the entire standard library classified as unknown. callix compares both the original and the resolved path on both sides.

Path ordering. Python compares pathlib.Path component by component, not as whole strings. Sorting by the string diverges on neighbours like src/v4-mini/… and src/v4/…, which changed file order and therefore node order. callix reproduces the component-wise comparison.