At my day job we have a dozen repositories: two web front ends, two Flutter apps, a handful of Express APIs and some RabbitMQ workers. One question kept coming up in code review: who calls PUT /devices/:id? Nobody could answer it without grepping every repo, and the architecture doc was two years out of date.
So I built a scanner that answers it from the code, and kept going until it drew the whole map. This post is about the parts that turned out to be hard, and how I check that the answers are right.
Why grep is not enough
Searching for /devices/ finds some callers and misses most of them:
-
The host lives somewhere else.
axios.create({ baseURL: process.env.API_URL })in one file,api.put(`/devices/${id}`)in another, andAPI_URL=http://devices-api:3000in a.envor adocker-compose.yml. The call only means something once you join all three. -
Clients are wrapped. Real code calls an
http(method, url)helper, a generated gRPC stub or aGatewayclass, rarelyfetchdirectly. -
Routes are mounted. The handler says
router.put("/:id"); the/devicesprefix is added two files away withapp.use("/devices", router), and a proxy (nginx, Envoy, Spring Cloud Gateway) may add another hop in front. -
Paths are templates on one side and patterns on the other. The caller builds
`/devices/${device.id}`; the route says/devices/:id, or{id}, or<int:id>.
Matching calls to routes
The scanner is static analysis (tree-sitter for JavaScript and TypeScript, pattern readers per framework for the other languages, no LLM). For each repo it:
- finds the routes it serves, with their full mounted path;
- finds the call sites: HTTP clients, gRPC stubs, queue publishers and consumers, database connections;
- works out where each call goes: the base URL from env files, compose files, constants and proxy config, then the path, normalised so
/devices/${id}meets/devices/:id.
Then every call is matched against the routes of the service its host points at.
Evidence instead of percentages
Every connection on the map says why it is there. Real examples from the OpenTelemetry demo:
-
frontend → shipping, HTTP, config:SHIPPING_ADDR = http://shipping:50050incompose.yaml, read in the same file. -
checkout → cart, gRPC, config:CART_ADDR = cart:7070incompose.yaml. -
checkout → accounting, queue, inferred: topicorders, published atmain.go:671, consumed atConsumer.cs:46.
There are four tiers: config (the address is written down somewhere), inferred (the path matches and nothing contradicts it), declared (a person confirmed it) and observed (seen at runtime, on the roadmap). No "87% confident". When the scanner can't settle something, like a base URL assigned at runtime, it goes to a review inbox instead of becoming a guessed arrow.
How I know it's right
I keep a benchmark of 40 public systems (the OpenTelemetry demo, Online Boutique, Train Ticket, eShop, Sock Shop, Spring PetClinic microservices and more) with hand-checked answer keys. Every scanner change runs the whole set, and a change that lowers the score doesn't ship. Today it's F1 97 on connections, precision 99 and recall 96.
That number is after fixing what those systems taught. The first run on 14 systems it had never seen scored F1 79, and every fix had to be a general rule, never a special case for one repo. New systems get scored once before any fix they prompt.
What the map is for
Once you have the graph, a few things get easy:
-
A pull-request check: "this PR removes
PUT /devices/:id, whichwebcalls atsrc/api.js:42". -
An MCP server, so Claude Code or Cursor can ask what depends on a route before editing it:
claude mcp add kuuhaku -- npx kuuhaku mcp. - Exports to Mermaid, C4 (Structurizr), Backstage and a PDF report.
Try it
Nine public systems are mapped and open without an account. The OpenTelemetry demo has 23 services in 10 languages and 30 connections, and against its answer key the scanner finds 27 of 28 and invents none. All nine are at kuuhaku.dev/maps. To map your own repos, the CLI runs on your machine and only the graph leaves it.
It's called Kuuhaku (空白, "blank": it fills in the blanks of your architecture), and it's free in early access until November 14: kuuhaku.dev. I'd love to hear where it gets your stack wrong.

Top comments (0)