code-graph
by osadan
README.md
# code-graph
A static **connection graph** of the Example platform. It scans `app-web`
(Laravel + React) and `app-services` (NestJS + Go + Python) and records how the
code is wired together — which files call which HTTP routes, which tables and
queues they read or write, and what they import — into an embedded
[Kùzu](https://kuzudb.com/) graph database you can query from the terminal, from
Claude Code (MCP), or with raw Cypher.
It answers the cross-repo reachability questions that are otherwise a
multi-file grep-and-guess exercise:
- Who actually calls `GET /resource-a`?
- What code touches the `table_b` table?
- How does the web frontend reach a backend service?
## Quickstart
```bash
uv sync # install (Python 3.11–3.13)
uv run code-graph scan # build graph.kuzu from the sibling repos
uv run code-graph stats # node + per-edge-type counts
uv run code-graph query "callers:/resource-a" # who calls a route
```
By default `scan` looks for `app-web` and `app-services` as **siblings** of this
project (`../app-web`, `../app-services`) and reads route data from
`../route-inventory/dist/openapi.json`. Point it at your own repos with env vars
instead of editing the code:
```bash
export CODE_GRAPH_REPOS="/path/to/web-repo,/path/to/services-repo" # or name=path pairs
export CODE_GRAPH_OPENAPI="/path/to/route-inventory/dist/openapi.json"
```
Missing repos are skipped, not errored. The scan is a **static snapshot** —
re-run it after any change you want reflected. A full scan of two large repos
takes roughly 15–20 minutes.
Each `scan` writes two gitignored artifacts at the project root: `graph.kuzu`
(the database) and `parser-report.txt` (scan counts).
## The three interfaces
### CLI
```bash
uv run code-graph scan
uv run code-graph stats
uv run code-graph query "<preset-or-cypher>"
uv run code-graph export --format html|graphml|dot
```
`query` takes one argument. If it looks like `<preset>:<arg>` it runs a preset;
otherwise it runs as raw Cypher.
| Preset | Finds |
|---|---|
| `callers:<route>` | files that issue an HTTP call to a matching route |
| `calls:<node>` | outbound edges from matching nodes |
| `touches:<table\|queue>` | code that reads/writes a table or uses a queue |
| `neighbors:<node>` | one-hop neighbours of matching nodes |
| `search:<text>` | nodes whose id/name contains the text |
`export` writes `graph.{html,graphml,dot}` (all gitignored, regenerated on
demand). `graph.html` is a self-contained offline viewer; GraphML/DOT are for
external graph tools.
### MCP (in Claude Code)
```bash
CODE_GRAPH_DB=/home/ohad/sh/projects/code-graph/graph.kuzu uv run code-graph-mcp
```
Exposes the query layer over stdio as seven read-only tools: `find_callers`,
`what_calls`, `path_between`, `who_touches`, `neighbors`, `search_nodes`,
`run_cypher`. The server refuses to start until `graph.kuzu` exists — run `scan`
first.
### Raw Cypher
The CLI `query` command (when the argument isn't a preset) and the MCP
`run_cypher` tool run arbitrary Cypher against the model below. The database is
opened **read-only at the Kùzu engine level**, and `run_cypher` additionally
rejects write/DDL keywords before executing — the graph cannot be mutated
through either path.
```bash
uv run code-graph query "MATCH (f)-[c:HTTP_CALL]->(r) WHERE r.kind='route' RETURN f.id, r.id LIMIT 20"
```
## Graph model
One `Node` table (discriminated by `kind`) and one relationship table per edge
type — the idiomatic Kùzu shape, so filtering is a `kind` predicate and every
traversal is a single relationship type.
| Node `kind` | Represents | Example `id` |
|---|---|---|
| `file` | a source file | `file:app-web/app-frontend/src/components/callRecord/StatusCheck.jsx` |
| `route` | an HTTP route (seeded from route-inventory) | `route:service-b:GET:/system/health-check-full` |
| `db_table` | a database table | `db_table:table_b` |
| `queue` | a Redis stream / channel | `queue:service-c-stream` |
| `package` | an imported package/module | `package:@nestjs/common` |
| `external` | an HTTP call that didn't resolve to a known route | `external:https://example.com/x` |
| Edge type | Meaning | `confidence` |
|---|---|---|
| `HTTP_CALL` | file → route it calls (or `external` if unresolved) | 0.8 resolved / 0.5 external |
| `DB_ACCESS` | file → table it reads/writes (Laravel `$table`/`DB::table`, Kysely) | 0.7 |
| `QUEUE_PUB` / `QUEUE_SUB` | file → queue/stream it publishes/consumes | 0.6 |
| `IMPORTS` | file → package or file it imports | 0.9 |
Every edge carries `source_file`, `line`, and `raw` (the matched snippet), plus
`mode` (`read`/`write`/`unknown`) on `DB_ACCESS`. `confidence` describes *how the
match was made*, not a statistical accuracy — treat low-confidence and
`external` edges as leads to verify against `source_file:line`, not ground truth.
## How it works
- **Parsing.** [tree-sitter](https://tree-sitter.github.io/tree-sitter/) plus
targeted regex over PHP, TS/TSX, JS/JSX, Go, and Python. No type resolution or
control-flow analysis, and deliberately **recall-biased** — it favours
catching real edges over avoiding every false positive.
- **Routes come from route-inventory.** Route nodes aren't discovered by parsing;
they're loaded from [`route-inventory`](../route-inventory)'s generated
`dist/openapi.json`. `HTTP_CALL` edges resolve a call's `(METHOD, path)`
against that catalog (with a suffix-match fallback and same-origin tie-break);
unmatched calls become `external` nodes.
- **What's scanned.** Both repos are walked recursively for the source
extensions above, **excluding** `node_modules`, `vendor`, `dist`, `build`,
`.git`, `.claude` (covers `.claude/worktrees`), `.next`, `coverage`. Symlinked
directories are followed once, guarded by resolved real path.
## Example output
From a scan of the real `app-web` + `app-services` trees (2026-07-16):
```
4248 files scanned → 10108 nodes, 0 parse errors
nodes: file 6653 · package 1594 · route 1559 · db_table 261 · external 41
edges: IMPORTS 21212 · DB_ACCESS 794 · HTTP_CALL 189 · QUEUE 0 (22195 total)
```
```
$ uv run code-graph query "callers:/system/health-check-full"
[
{
"caller": "file:app-web/app-frontend/src/components/callRecord/StatusCheck.jsx",
"route": "route:service-b:GET:/system/health-check-full",
"line": 168,
"confidence": 0.8
}
]
```
`IMPORTS` dwarfs the other edge types by design — one edge per import statement
across thousands of files, versus per-call-site matches elsewhere.
## Known limitations
- **Minified build bundles add noise.** `app-web/public/js/*` hashed
Vite/webpack output is currently scanned, producing some spurious `HTTP_CALL`
edges and file nodes. Excluding build output at the walker level is a planned
follow-up.
- **Relative imports aren't resolved.** A relative specifier (`./x`, `../y`)
becomes a literal `file:` node id rather than being resolved to the real file
it points at, so a share of `file` nodes are synthetic import targets. Bare
package imports resolve correctly.
- **`DB_ACCESS.mode` is `unknown` for PHP.** Only the TS/Kysely path
(`selectFrom`/`insertInto`/…) infers read vs. write; Laravel `$table`/
`DB::table` hits have no per-call-site signal, so `mode` is left `unknown`
rather than guessed.
- **Queues need string literals.** `QUEUE_*` edges are only created for
string-literal stream/channel names; Kafka-style `subscribe(variable)` calls
aren't named. (`QUEUE = 0` on the current tree is accurate, not a miss.)
## Development
```bash
uv run pytest # 42 tests
```
```
src/code_graph/
cli.py CLI entry point (scan / stats / query / export)
mcp_server.py MCP entry point (code-graph-mcp)
config.py ScanConfig: sibling-repo paths, exclude list, db path
model.py Node/Edge dataclasses + stable id helpers
catalog.py RouteCatalog: loads route-inventory OpenAPI, resolves calls
walker.py source-file walker (language detection, excludes, symlink guard)
parsing.py tree-sitter parse cache + AST helpers
matchers/ HTTP_CALL / DB_ACCESS / QUEUE_PUB+SUB / IMPORTS extraction
graph/
loader.py Kùzu schema + CSV bulk loader
queries.py GraphDB: read-only presets + guarded raw-Cypher escape hatch
scan.py orchestrates walker → matchers → loader, writes parser-report.txt
report.py parser-report.txt writer
skill/SKILL.md Claude Code skill definition
docs/superpowers/ design spec + implementation plan
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues