Skip to main content
Glama
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
```