Kevin indexes its own source tree into a queryable graph of files, symbols, calls and bus topics. No model is called: it is AST extraction, so a refresh is deterministic and costs nothing. Every architecture diagram on this site is then tied by manifest to the code paths that determine it, and when those paths change the diagram is marked stale and a render job is queued. That is why the catalogue at /architecture/ tracks the system instead of describing a version of it that stopped existing in March.
Every node below is a real file. One script composes them: scripts/graphify-subsystem.ps1
The refresh runs once per delivery, before the commit that gets pushed. A nightly full pass catches drift.
Compiles the allow/deny list into a marked generated block in .graphifyignore, so a scope edit takes effect on this run.
daemon/src/graphify_index.py
Third-party engine: the graphifyy package. We did not write the parser and do not claim to.
Folds the declared bus domains and topics in as first-class nodes, so what the system says it publishes sits in the same graph as what the code does.
daemon/src/data_dictionary_graph_export.py
The working graph. Local and gitignored: large, and regenerable from source at any time.
graphify-out/graph.json
The same graph packed into a stamped container that names its own build, checksums its payload, and tombstones the artifact it replaces.
graphify-out/graph.kvg
The refresh is advisory infrastructure, never a delivery gate. Every internal step degrades to a warning and the script exits zero unless you ask it not to. A knowledge artifact that can block a release is a knowledge artifact people learn to route around.
Two things decide whether a codebase graph is still usable a month after you build it: whether it indexes your code rather than your dependencies, and whether a consumer can tell how old it is. Both are explicit here.
graphify-index.yaml: an allow-list, a deny-list, and nothing else
# only: non-empty = index ONLY these (allow-list) only: [] # exclude: vendored + generated trees exclude: - tools/frontend-dev # vendored - tools/kevin-mail # vendored - tools/mqttclient # vendored - daemon/.test_runs
Built-in skips (node_modules, dist, venv, __pycache__) always apply and are not listed. The refresh compiles this file into .graphifyignore, and you never hand-edit the generated block. It is also editable from the operations console, because the person who notices a vendored tree landed is not always the person holding a shell.
On this repository, scoping is the difference between a scan of roughly 3,700 first-party files and one of 8,700, the figures recorded in the repo's own CLAUDE.md when the scope control was added. An unscoped graph is not more complete. It is slower, and most of it is someone else's code.
daemon/src/kvgraph_format.py: magic header, JSON metadata, compressed payload
| Meta field | Why it is there |
|---|---|
| generated | When this artifact was built. A consumer can decide it is too old without opening the payload. |
| supersedes | The stamp of the artifact this one replaces. Latest always wins, so two copies are never ambiguous. |
| commit | The git short SHA the graph was extracted from. |
| nodes edges communities | Readable from the header alone, without decompressing. |
| raw_sha256 | Integrity of the uncompressed graph. A mismatch raises on decode rather than returning a plausible-looking graph. |
| codec | Defaults to lzma because it is stdlib. Any consumer decodes a .kvg with zero third-party dependencies. |
# header only, no decompression python daemon/src/kvgraph_format.py info graphify-out/graph.kvg # back to plain JSON python daemon/src/kvgraph_format.py unpack \ graphify-out/graph.kvg out.json
Packing this repository's graph on one recorded build took it from about 45 MB of JSON to about 2.2 MB. The compression is lossless, so that ratio is a property of that particular graph and yours will differ. The pack step prints its own. Treat the figure above as an example of the output, not as a benchmark.
A graph nothing queries is a diagram with extra steps. Four things read this one, and each is a code path you can open.
query, path and explain traverse the graph under a token budget: breadth-first for broad context, depth-first to trace one route, shortest path between two symbols. An agent answering "what calls this" walks edges instead of grepping a tree.
The retrieval router pulls structured graph context alongside vector search and prepends it to the domain prompt, but only for domains on an explicit list. Injecting code-graph context into unrelated domains adds noise and blows the token budget, so it is opt-in per domain rather than global.
plugins/rag-router/graph_bridge.py
Decodes the .kvg and answers "which modules does this ticket touch". One implementation, served over the API, with the graph's generated stamp and a staleness flag on every result. A stale answer is visible, never silent.
daemon/src/kanban_index.py
Two HTML views are tracked in git and served by the daemon at /api/graphify/tree.html and /api/graphify/callflow.html. The force-directed viewer is deliberately not generated: this repository's graph runs to roughly 95,000 nodes, past the point where an interactive force layout is useful, so the tree and the call-flow are what ship.
The call-flow view groups graph communities under our own section names, compiled from a repo-local config on every run. Left alone, the upstream package scores communities against a table describing its own repository, which is how a delivered view ends up narrating your system using someone else's headings.
The ticket id is the correlation key across the platform. The graph turns a ticket into the modules it touches; the fabric ledger writes that same ticket onto every request row it records. One key, two artifacts: what a change touched, and what the work on it cost.
Stated precisely, because the distinction matters. The ledger records routed cost, tokens, provider, latency and the routed-versus-baseline counterfactual per request, so a savings rollup is a query. What it will not do is backfill: rows written before those columns existed carry an unknown baseline, are stored as unknown rather than zero, and are excluded from the figure rather than counted as having saved nothing.
This is the claim worth being exact about, because the loose version of it is a lie. The SVGs are not plotted automatically out of graph.json. They are derived: each one declares the source paths that determine it, staleness is detected mechanically against git, and the render follows a fixed contract that a validator enforces.
One catalogue entry per diagram: slug, category, summary, status, last built, and the watch paths it is derived from. Everything else in the pipeline reads this file.
architecture/MANIFEST.yaml
The refresh asks git which watch paths changed in the window, then queues a render job for each diagram those paths belong to. A diagram whose source moved cannot quietly stay current.
scripts/update-diagrams.ps1
Five shape classes, four edge classes, fixed meanings, a required region order, and acceptance bounds: minimum and maximum file size, well-formed XML, ISO-dated subtitle, version in the title. An SVG using any class outside the canonical set is rejected.
architecture/STYLE_GUIDE.md
The grammar is the one this page is built in, which is the point. Cyan means code that does work. Pink means stateful data. Dashed green means a third-party system. Two people reading the same diagram get the same answer about what a box is, because the colour is a type and not a decoration. Filenames carry the version and subtitles carry the build date, both from a resolver rather than from whoever last edited the file.
A knowledge graph is an easy thing to oversell, so here is the shape of it that survives a code review.