Skip to content
BACK TO DOSSIER

// Developer intelligence

graphyn

Understand the blast radius before you change code. A Rust engine that turns a repository into a deterministic symbol graph you — and your agents — can query.

Active experiment · installable CLI + MCP server4 min read

The problem

The useful context around a symbol is distributed across files, imports, calls, and configuration. Searching text can find mentions, but it cannot explain the shape of a change.

Ask "what breaks if I change this function signature?" and grep hands you a list of string matches. It cannot distinguish a call site from a comment, a re-export from a shadowed local, or an aliased import from an unrelated identifier that happens to share a name. The information needed to answer the question is right there in the code — it is just not in a form anything can query.

That gap matters more now than it used to. Coding agents make changes quickly and confidently, and the thing they most need before editing is exactly the thing text search cannot give them: the blast radius.

The approach

Graphyn separates scanning, symbol extraction, relationship building, and query surfaces. That separation is the whole design — the graph becomes reusable, so a CLI, an MCP server, or a future editor integration can ask the same model different questions without any of them owning the model.

code
source files ──> scanner ──> symbols ──> relationships ──> query surface

                                            └──> the graph is the product

That shape is enforced by the crate layout rather than by convention:

CrateResponsibility
graphyn-corescan, IR, symbol identity, resolution, graph, queries
graphyn-adapter-*one per language: parser, extractor, scope, import resolver
graphyn-adapter-dispatchroutes a file to the adapter that understands it
graphyn-storeRocksDB persistence and caching under .graphyn/db
graphyn-clianalyze, query, watch, status, serve
graphyn-mcpthe same queries, exposed as tools to agents

Each stage has one responsibility and a stable output shape. The scanner does not know what a query looks like. The query layer does not know how files were read. Adding a language means writing an adapter, not touching anything downstream.

Languages

LanguageResolves
TypeScript / JavaScripttsconfig paths, barrel re-export chains, decorator DI
Vue / Svelte / Astroscript blocks within the component
Pythonrelative imports, __init__ re-export chains, Pydantic / Django
Rustmodule tree, use groups and aliases, trait impls, #[derive]
Gopackage imports via go.mod, structural interface satisfaction
C / C++#include resolution, typedef aliases, base classes

Java and Kotlin are next. Every adapter attributes member access to the type a value was declared as, so payload.user_id is recorded against UserPayload however the local variable was named.

The properties that took the most work

Alias-aware. import { A as B } has to resolve to the same node as A, or every answer about usage is quietly wrong. Name-based matching gets this consistently backwards.

Property-aware. Tracking accessed members is what makes a refactor safe rather than merely plausible — changing a field matters only to the code that reads that field.

Deterministic. No LLM participates in graph construction. The same repository produces the same graph every time, which is the only reason an answer is worth trusting. A probabilistic blast radius is not a blast radius.

Using it

code
graphyn analyze ./my-repo          # build the graph
graphyn query blast-radius Symbol  # what breaks if this changes
graphyn query usages Symbol        # every use, aliases included
graphyn query deps Symbol          # what it depends on
graphyn watch ./my-repo            # keep the graph current while coding

The MCP server exposes the same queries to Cursor, Claude Code, and Codex, so an agent can check impact before editing rather than after.

Being wrong out loud

The limits are in the README rather than an issue tracker, because an analysis tool that hides its blind spots is worse than one with more of them — you cannot calibrate against it. Imports resolve within one language. Chained access a.b.c is attributed to the first receiver only. C++ templates are parsed but not instantiated. Go structural matching is per-package. Diagnostics are categorised, so you can ask the tool what it could not see.

What I learned

The hard part of code intelligence is not parsing. Parsers are largely solved. The hard part is deciding what a relationship is and committing to that definition consistently — because every ambiguity left in the model becomes an ambiguity in every answer the system gives.

I wrote the long version of that up in What breaks if I change this?, including the alias-detection bug that flagged every reference in a repository as high risk and buried the three that mattered.

Writing it in Rust was partly for traversal performance and partly because an in-memory graph with strict ownership rules forces you to be explicit about what references what. That turned out to be the right pressure for this problem.

Current state

An active experiment with an installable CLI. Scanning and graph construction are stable enough to build on; the query surface is where the design is still moving.

← ALL WORKCASE STUDY / GRAPHYN