Metadata-Version: 2.4
Name: heimdall-code
Version: 0.1.1
Summary: A code knowledge graph that sees everything -- indexes any repository into a SQLite property graph and serves it to AI agents over MCP.
Project-URL: Homepage, https://github.com/stPhoenix/heimdall
Project-URL: Source, https://github.com/stPhoenix/heimdall
Project-URL: Issues, https://github.com/stPhoenix/heimdall/issues
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: code-intelligence,knowledge-graph,mcp,static-analysis,tree-sitter
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.12
Requires-Dist: mcp>=1.2
Requires-Dist: tree-sitter-language-pack>=1.13
Requires-Dist: tree-sitter>=0.25
Description-Content-Type: text/markdown

# Heimdall

**A code knowledge graph that sees everything.**

Heimdall indexes any repository into a property graph — every symbol, call, import and
inheritance edge — then serves it to coding agents over MCP so they can answer
*"what calls this?"*, *"what breaks if I change it?"* and *"how does A reach B?"*
without grepping the whole tree.

Apache-2.0. No server, no cloud, no embedded graph engine. **The graph is a SQLite file.**

```console
$ heimdall index ~/code/django
pass 1/4  discovering files
  2974 source files
pass 2/4  parsing (24 workers)
pass 3/4  building graph
pass 4/4  building search index
done in 1.49s          # 43,482 symbols, 353,101 edges

$ heimdall impact django/shortcuts.py:get_object_or_404 -d 2 -c 0.7
impact of django/shortcuts.py:get_object_or_404: 6 symbols, 4 files

depth 1 (5):
  Function   django/contrib/flatpages/views.py:flatpage (0.80)
             django/contrib/flatpages/views.py:22
  ...

$ heimdall trace flatpage HttpResponse
path of length 2:
  -> django/contrib/flatpages/views.py:flatpage
    -> django/contrib/flatpages/views.py:render_flatpage
      -> django/http/response.py:HttpResponse
```

A second and a half, cold, for the whole of Django on a laptop.

## Why

[GitNexus](https://github.com/abhigyanpatwari/GitNexus) showed that agents work far better
with a precomputed structural index than with raw search. Heimdall takes that idea and
rebuilds it from scratch under **Apache-2.0**, in Python, on a much lazier stack:

| | GitNexus 1.6.9 | Heimdall |
|---|---|---|
| License | PolyForm Noncommercial | Apache-2.0 |
| Graph store | LadybugDB (native + WASM) | `sqlite3` (stdlib) |
| Traversal | Cypher engine | recursive CTEs |
| Search | BM25 + embeddings + RRF | FTS5 BM25 (stdlib) |
| Runtime | Node.js / browser WASM | CPython |
| Dependencies | 33 | **2** (tree-sitter, mcp) |
| Ingestion | 15 phases | 4 passes |

GitNexus is the larger tool and does things Heimdall does not: a program dependence graph
and taint analysis, API route and DI extraction, cross-repository contract graphs, MRO
resolution, embeddings, a web UI. If you need those, use it — the honest comparison is in
[bench/README.md](bench/README.md), not in this table. The same goes for graphify, which
indexes docs, images and video into the same graph and has an LLM extraction pass on top;
only its AST code extraction is measured here, because that is the part an `ast` oracle
can adjudicate.

Heimdall's bet is narrower: that the structural core of that idea fits in
`sqlite3` + tree-sitter, and that everything traversal and ranking needs is already in the
standard library. Measured on the same repository — [Django](https://github.com/django/django),
2,927 Python files — graded by an independent CPython `ast` oracle, against GitNexus and
against [graphify](https://pypi.org/project/graphifyy/) in its AST-only mode:

| | Heimdall | GitNexus | graphify |
|---|---|---|---|
| `self.m()` bound correctly | 100.0% | 97.1% | 92.6% |
| same-file call bound correctly | 100.0% | 93.4% | 89.3% |
| imported call bound correctly | 99.7% | 71.5% | 75.4% |
| class instantiation, same file / imported | 99.5% / 98.8% | not modelled | 95.2% / 98.2% |
| mean targets emitted per call | 1.00–1.06 | 1.00 | 1.00 |
| edges the referee can refute outright | **0** of 157,036 | 0 of 23,736 | 202 of 30,882 |
| live symbols wrongly shown as uncalled | **1** (0.0%) | 1,373 (29.4%) | 205 (4.4%) |
| hand-written edge cases missed or fabricated | **1** of 13 | 4 of 13 | 3 of 13 |
| *who calls `Model.check`*, precision | 94% | 0% | 95% |
| index from scratch | **1.7 s** | 23.0 s | 28.8 s |
| index on disk | **48 MB** | 642 MB | 151 MB |
| `impact X` from cold | **56 ms** | 949 ms | 856 ms |

Six of those Heimdall numbers only got there because the benchmark existed. It found that
imports were resolved file-wide rather than per-scope, that a Python `pkg/main.py`
registered as `pkg` only, that `as` renames were never followed, that a class name used as
a receiver was ignored, that recursion produced no edge, and — the moment the benchmark
moved to Django — that `@decorator(...)` was booked against the enclosing class instead of
the function it wraps, which alone cost 26% of imported-call recall. All fixed, all covered
by a test. See [bench/README.md](bench/README.md) for the oracle, the 31 cases, and the
caveats — including the ones that do not flatter us.

## Install

```console
uv tool install heimdall-code    # or: pip install heimdall-code
heimdall install                 # wire it into every agent on this machine
```

Or nothing at all: `uvx heimdall-code index .`. The distribution is `heimdall-code`
because an unrelated `heimdall` already sits on PyPI; the command is `heimdall`.

Python 3.12+. Two dependencies: `tree-sitter-language-pack` for parsing and `mcp` for
the server. Everything else is the standard library.

## Use

```console
heimdall index [PATH]              # build .heimdall/graph.db (idempotent, re-run any time)
heimdall search "retry backoff"    # BM25 ranked symbols
heimdall context MyClass.method    # definition, callers, callees, members, supertypes
heimdall impact process_batch      # blast radius, grouped by call depth
heimdall trace main save_to_s3     # shortest path between two symbols
heimdall stats                     # what is in the graph
heimdall sql "SELECT ..."          # read-only SQL escape hatch
heimdall projects                  # every repo you have indexed
heimdall install [claude|codex]    # register with an agent, as MCP or as a CLI skill
```

Every command takes `--json` for machine consumption and `-p/--project NAME` to query a
repository other than the current directory.

## MCP

Indexing registers the repository in `~/.heimdall/registry.json`, so **one** MCP server
serves all of them. `heimdall install` writes that server into whichever agents you name:

| | writes |
|---|---|
| `heimdall install` | both of the below, whichever are present |
| `heimdall install claude` | `~/.claude.json` |
| `heimdall install claude --project` | `./.mcp.json`, committed with the repo |
| `heimdall install codex` | `~/.codex/config.toml` |
| `heimdall install codex --project` | `./.codex/config.toml` — Codex reads it once the project is trusted |
| `heimdall install … --cli` | the [skill](skills/heimdall/SKILL.md) instead of the server, so the agent drives the CLI: `~/.claude/skills/heimdall/`, `~/.agents/skills/heimdall/` |

The command written is the copy of heimdall that ran the install, or
`uvx --from heimdall-code heimdall mcp` when that copy is a throwaway `uvx` environment
that uv would later collect. Your other settings are merged around it, and a config file
that does not parse is reported rather than replaced.

### As a Claude Code plugin

```console
/plugin marketplace add stPhoenix/heimdall
/plugin install heimdall
```

Server and skill, no Python install — the plugin's `.mcp.json` runs it through `uvx`.

<details>
<summary>Raw config for other clients</summary>

```json
{
  "mcpServers": {
    "heimdall": { "command": "heimdall", "args": ["mcp"] }
  }
}
```
</details>

| Tool | Answers |
|---|---|
| `overview` | What is this repo? Languages, hot files, entry points |
| `search` | Where is the thing called roughly *this*? |
| `context` | Definition, members, callers, callees, supertypes of one symbol |
| `impact` | What breaks if I change this? Grouped by call depth |
| `trace` | How does A reach B? |
| `read_symbol` | The source of one function, not the whole file |
| `sql` | Anything else — the schema is two tables |
| `projects` | Which repos are indexed |

Every tool takes an optional `project` argument, so an agent can reason across
repositories in one session. Results are compact JSON, and any truncation is reported
in the payload rather than silently applied.

## How it works

Four passes, all precomputed at index time so a query is one SQL statement:

1. **discover** — `git ls-files` when the repo is a git checkout (so `.gitignore` is
   honoured for free), a filtered walk otherwise.
2. **parse** — tree-sitter, one process per core. Symbols, calls, imports and heritage
   clauses come out of a *data table* of node types per language, not per-language code.
   A decorator counts as a call by the function it wraps, so `@override_settings(...)`
   puts the test in that helper's blast radius.
3. **resolve** — imports are matched by longest dotted-suffix, per scope, so a function
   with its own `from x import y` is not resolved against the whole file. `as` renames are
   followed home, but only when the import landed inside the repo, so `from asyncio import
   run as arun` cannot become a call to your own `run`. Calls are matched by name and
   scored: same file `0.95`, imported file `0.80`, globally unique `0.70`, ambiguous
   `0.30`. Once any candidate has real evidence behind it the name-only matches are
   dropped rather than listed. Confidence is stored on the edge so callers can filter.
4. **index** — FTS5 over names, qualified names and signatures.

Receivers are used wherever they carry information: `self.foo()` resolves inside the
enclosing class, `utils.foo()` inside the file imported as `utils`, and `Model.foo()`
inside the class `Model` — including when *several* classes are called `Model`, because
the one that defines `foo` is the one that was meant. On Django that leaves 46,260 of
286,346 call edges resolved by name alone — and every one of them is labelled
`confidence = 0.3`.

### Schema

```sql
node(id, kind, name, qname, path, line, end_line, lang, signature)
edge(src, dst, kind, confidence, line)
```

`kind` on nodes: `Folder`, `File`, `Class`, `Interface`, `Function`, `Method`.
`kind` on edges: `CONTAINS`, `DEFINES`, `IMPORTS`, `CALLS`, `EXTENDS`.

That is the whole data model — `heimdall sql` gives you direct access to it.

## Languages

Python, JavaScript, TypeScript, TSX, Go, Rust, Java, Kotlin, C#, Ruby, PHP, C, C++,
Swift, Scala, Lua, Dart, Bash. Every one of them is covered by a test that asserts it
still yields symbols and call edges, so a grammar rename fails the build rather than
silently degrading the graph.

Adding a language means adding rows to three dicts in `heimdall/parse.py` — node types
for definitions, for calls, and for imports — plus a snippet in the test.

One known gap: in Ruby, a receiver-less call without parentheses (`foo`) is
indistinguishable from a variable reference in the grammar, so only `foo()` and
`x.foo` produce call edges.

## Limitations

Call resolution is name-based, not type-based. `self.x()` and `module.x()` resolve
exactly; a call on an inferred variable (`client.get()`) produces up to five
`confidence = 0.3` edges rather than one certain edge. Filter them out with
`--min-confidence 0.7`; `trace` already does by default.

Inherited members are not followed when resolving `self.x()`, indexing is full-rebuild
rather than incremental, and there are no embeddings, no community detection and no
control-flow graph.

One gap has a test standing on it, so it is known rather than suspected: a local name that
shadows a module-level function (`parse = json.loads` inside a function that lives next to
a `def parse`) still resolves to the module-level one — GitNexus gets this wrong too, and
fixing it means real scope analysis.

*"Who writes into this table"* used to be the other one. A receiver whose name several
classes share — Django has 236 classes called `Model` — made the resolver give up and
fall back to matching `check` by name, which buried the 103 real callers of `Model.check`
in 442 rows. Ambiguity in the receiver is not ambiguity in the call, though: exactly one
of those 236 classes defines `check`. Keeping every candidate class and letting the member
decide answers it in 109 rows at 94% precision, with no schema change and no change in
index time. What is left is the variable receiver — `field.check(...)` in the same file as
`Model.check` — which is name ranking working as designed, and marked as such by its
confidence.

Community detection is the one of those with a number attached, and Django moved it. On
GitNexus's own call graph, 15 lines of stdlib label propagation still beat the vendored
Leiden outright — 0.801 Newman modularity against 0.613, with the directory tree at 0.479.
On Heimdall's much denser graph it does not: label propagation collapses to 74 groups and
scores 0.188, below Leiden's 0.257 and below the directory tree's 0.224. So clustering is
worth having and the cheap algorithm is not uniformly enough. If it lands here it lands
with that caveat measured, not assumed.

## Contributing

Layout, how to add a language, and the release steps are in
[CONTRIBUTING.md](CONTRIBUTING.md); what changed is in [CHANGELOG.md](CHANGELOG.md).
A resolution bug is worth a fixture in `tests/test_heimdall.py` — every one found so far
has one.

## License

Apache License 2.0 — see [LICENSE](LICENSE). Independent implementation; GitNexus is
credited as inspiration only, no code was taken from it.
