This is optional. You do not need it to install, build, use, or contribute to deep-memory. It's a worked example — and a developer convenience — that shows how an AI assistant can use Deep Memory itself to understand a codebase. Skip it entirely and everything else still works.
The code graph is a small map of this repository: its packages, the pluggable providers, the MCP tools, and the docs and tests — and how they all connect. A script reads the source code and writes that map into a local graph database (Neo4j). Nothing is sent to the cloud, no AI model is called, and it costs nothing to build.
Why bother? Because some questions are slow to answer by searching through files one by one, but instant for a graph:
- "If I change the
StorageProviderinterface, which implementations and tests will break?" - "Does the
corepackage secretly depend on anything at runtime?" (it must not) - "Which MCP tools change data versus only read it?"
- "Which documentation mentions this thing I'm about to edit?"
An AI assistant working on the repo can ask the graph these questions in one step instead of running many searches — so it spends less effort finding things and more effort getting the change right.
This repository is the Deep Memory library. The code graph uses that very library to store a knowledge graph about the library's own source code. So it doubles as a real, self-contained demonstration: the same technique works for any codebase, not just this one. (The approach was first proven on a large production application, then brought back here — see How it works.)
Building the graph needs a local Neo4j database running, which most people won't have set up — and
which has nothing to do with shipping the library. So the code graph is deliberately kept
separate from pnpm build. The normal build never depends on it and can never be broken by it.
You build the graph only when you choose to, by running one command.
pnpm install
pnpm build # the rebuild script imports the built packagesStart a local Neo4j (the repo ships a Docker Compose file — see quickstart-neo4j.md):
docker compose -f docker-compose.neo4j.yml up -d
docker compose -f docker-compose.neo4j.yml ps # wait for STATUS: healthyThat gives you Neo4j on bolt://localhost:7687, a browser at http://localhost:7474, and the
development credentials neo4j / DeepMem-Dev-1234.
pnpm code-graph:rebuildIt reads the repo and writes the graph into Neo4j in a second or two. It is safe to run as often as you like — it only writes what actually changed (re-running with no changes writes nothing), and it never calls an embedding model, so it is free. The output prints the graph's size plus a few sample insights, e.g.:
[code-graph] extracted 9 packages, 6 contracts, 10 impls, 38 tools, 55 docs, 56 tests, 186 modules, 24 error types
[code-graph] entities +0 ~0 -0 (=392 unchanged)
[code-graph] core zero-runtime-dependency invariant: HOLDS (0 external runtime/peer deps)
[code-graph] provider contracts and their implementations:
4 StorageProvider
...
0 LockProvider (no implementation)
[code-graph] source files by import fan-in (most depended-on — biggest blast radius):
31 packages/mcp-server/src/tools/base/BaseToolController.ts
...
[code-graph] MCP tools with a covering test: 6/38
[code-graph] source modules with no covering test: 108/186
[code-graph] DEFINES edges: 80 (80 constructs bridged to their defining file)
[code-graph] import cycles: none (the Module→Module import graph is acyclic)
[code-graph] provenance: d63d5f7 (main) built 2026-06-23T…
If a cycle exists, that line instead reads
N import cycle(s) detected (M modules in a cycle)followed by each cycle's file chain — a real defect to fix, since the project forbids circular deps.
For a terse, copy-paste query reference (recipes, slugs, caveats) — the doc to load when you just need to query — see code-graph-usage.md.
The easiest way is through the deep-memory MCP server (already configured in this repo's
.mcp.json), so your AI assistant can query it directly. Point it at the code-graph repository
first:
- Repository id:
d33c0de0-9a1b-4c2d-8e3f-1a2b3c4d5e6f - Repository label:
Deep Memory Codebase Architecture
Open it with memory_open_repository, then ask questions like "what implements StorageProvider?"
or "which tools mutate state?". The assistant uses tools such as memory_explore_neighborhood
(blast radius), memory_find_entities, and memory_query_graph to answer.
No semantic search here. This graph is built without embeddings, so
memory_search_by_conceptwon't find anything for it. Navigate by entity type, by exact name (slug), or by following relationships — not by fuzzy text search.
You can also explore visually in the Neo4j browser at http://localhost:7474, or run raw Cypher.
Everything below is derived deterministically from the source — no AI, no guessing.
| Type | What it represents | Mined from |
|---|---|---|
Package |
A workspace package (the 9 here) or an external npm package one of them depends on at runtime. | every package.json |
ProviderContract |
A pluggable provider interface — the seam an implementation plugs into (StorageProvider, SearchProvider, EmbeddingProvider, GraphTraversalProvider, LockProvider, LLMProvider). |
export interface *Provider declarations |
ProviderImpl |
A concrete provider implementation (e.g. SqlServerStorageProvider, CosmosDbProvider, Neo4jStorageProvider). |
implements <*Provider> clauses, plus structural conformance (a *Provider-named class that declares a contract's full required surface but omits the clause) |
McpServer |
An MCP server (the local memory server and the indexer server). | the two server packages |
McpTool |
A tool an AI can call (e.g. memory_create_entities, indexing_execute); flagged mutates if it writes. |
each tool's get name() |
Doc |
A markdown doc (under docs/, the root quickstarts/README, and each package README). |
the markdown files |
Test |
A co-located test file. | *.test.ts / *.spec.ts |
Module |
A non-test source file — the file-level layer beneath the package graph. Carries a package property, a throwsRawError flag, and an inImportCycle flag (set when the file is part of a circular import). |
every non-test packages/*/src/**/*.ts |
ErrorType |
An exported error class in the typed-error hierarchy (e.g. RepositoryNotFoundError). |
exported class … extends *Error |
| Edge | Meaning |
|---|---|
Package ─DEPENDS_ON_PACKAGE→ Package |
declared dependency (the depType says runtime / peer / dev) |
ProviderImpl ─IMPLEMENTS→ ProviderContract |
the class implements the interface; detection says whether it was found via a nominal implements clause or inferred structurally |
Package ─CONTAINS→ ProviderContract | ProviderImpl | McpServer | ErrorType |
which package owns the construct (Modules are excluded — see caveats) |
McpServer ─ADVERTISES→ McpTool |
the server exposes the tool |
Module ─DEFINES→ ProviderContract | ProviderImpl | McpServer | McpTool | ErrorType |
the file that declares the construct — the bridge from the file tier to the architectural tier (a construct ←DEFINES— Module —IMPORTS→ its importers) |
Doc ─DOCUMENTS→ ProviderContract | ProviderImpl | McpServer | McpTool | Module |
a markdown link to a source file (precise); a link to a provider/server/tool file resolves to that construct, any other .ts to its Module |
Doc ─MENTIONS→ ProviderContract | ProviderImpl | ErrorType |
a symbol/error-class name in prose (broader) |
Doc ─DESCRIBES→ Package |
a package README documents its own package (the reliable doc↔package link) |
Test ─COVERS→ ProviderImpl | ProviderContract | McpTool | Module |
the test imports the symbol (provider/tool) or resolves a relative import to a source file (Module); role separates the subject (matches the test filename) from a fixture — so "which tools/files have no test" is a query |
Module ─IMPORTS→ Module | Package |
a file imports another file (intra-repo, resolved) or a package (bare import) — file-level blast radius |
Module ─THROWS→ ErrorType |
a file constructs/throws that error (throw new XError()) |
ErrorType ─EXTENDS→ ErrorType |
subclass → superclass — the catchability chain |
The most useful headline query is the core zero-runtime-dependency invariant: core should
have no outgoing runtime/peer DEPENDS_ON_PACKAGE edge. The graph turns that rule into a check
you can run, rather than a convention you hope holds.
A second invariant rides on the file tier: no import cycles. The rebuild runs Tarjan's
strongly-connected-components over the Module ─IMPORTS→ Module graph and flags every file in a
cycle with inImportCycle: true (and logs the cycle). The project forbids circular deps, so any
hit is a real defect — memory_query_graph on Module filtered by inImportCycle surfaces them.
- Deterministic extraction, no LLM. Each layer is mined straight from the source:
package.jsonfor the dependency graph,implementsclauses and structural member-conformance (viats-morph) for the provider seam, each tool'sget name()literal for the tool surface, markdown links/symbols for docs, import declarations for tests (resolved against providers, tool classes, and the source files they import — Test→Module COVERS), per-file import/export specifiers for the module graph,throw newsites for THROWS, andextends *Errorfor the typed-error hierarchy. Each construct's defining file becomes aDEFINESedge, and Tarjan's SCC over the module-import graph flags import cycles (inImportCycle) — both pure graph analysis over the already-extracted data, still no model. - Build provenance. Each run stamps the repository metadata with the git commit, branch, and
dirty flag the graph reflects, plus a
builtAttimestamp — so a consumer can tell how stale the graph is and against which commit (written only when something actually changed). - Delta reconciliation. Every node gets a deterministic id (so the same logical thing always maps to the same graph node) and a content fingerprint. A run re-extracts the whole desired graph, diffs it against what's in Neo4j, and writes only the difference. That makes it idempotent (re-running with no source change writes nothing), self-healing (any drift is corrected), and always reflecting your working tree (uncommitted edits included).
- No embeddings, by design. The rebuild script wires no embedding provider, so writing entities computes no vectors and costs nothing. The trade-off is that semantic search is disabled for this repo (see above) — which is fine, because a code graph is navigated by structure, not by fuzzy text.
- The vocabulary evolves in place. Adding or changing an entity/relationship type updates the stored schema on the next run without rebuilding the data.
| File | Role |
|---|---|
| scripts/code-graph/vocabulary.ts | The repo id/label/constants and the graph schema (entity + relationship types). |
| scripts/code-graph/extract-packages.ts | Reads every package.json → packages + their dependency edges. |
| scripts/code-graph/extract-providers.ts | ts-morph: provider interfaces and the classes that implement them. |
| scripts/code-graph/extract-tools.ts | ts-morph: each MCP tool's wire name, server, and whether it mutates. |
| scripts/code-graph/extract-docs.ts | Markdown: titles, source links (DOCUMENTS), and symbol mentions (MENTIONS). |
| scripts/code-graph/extract-tests.ts | ts-morph: the symbols each co-located test imports and the source files its relative imports resolve to (COVERS → ProviderImpl/Contract/McpTool/Module). |
| scripts/code-graph/extract-modules.ts | ts-morph: per-file imports/re-exports (IMPORTS) and throw new sites (THROWS / throwsRawError). |
| scripts/code-graph/extract-errors.ts | ts-morph: exported error classes and what they extend (ErrorType / EXTENDS). |
| scripts/code-graph/scc.ts | Pure, deterministic Tarjan SCC over the module-import graph — finds import cycles (Module.inImportCycle). |
| scripts/code-graph/rebuild.ts | The orchestrator — extract → connect Neo4j → reconcile (delta) → verify. Entry point of pnpm code-graph:rebuild. Emits DEFINES and runs cycle detection. |
- Add the new entity/relationship type to
vocabulary.ts. - Add a small extractor (mirror one of the existing ones) or extend one.
- Wire the new nodes/edges into
rebuild.ts. - Re-run
pnpm code-graph:rebuild— the schema updates in place; all existing data is kept.
Building this for a different codebase? This doc describes this repo's graph; the generalised method — how to design a focused, bespoke vocabulary and extract it deterministically for any codebase — is in code-graph-guide.md.
Keep everything deterministic. A future, separate step could add a semantic layer (the "why", design invariants, cross-runtime contracts) using the deep-memory indexer with an LLM — but the structural graph described here intentionally uses no model.
- Static facts, not runtime proof. An edge means "this relationship exists in the source", not that a code path actually runs. Use the graph to find the right code fast; then read the code.
- Some
IMPLEMENTSedges are inferred, not declared. Most come from a nominalimplementsclause, but a*Provider-named class that declares a contract's full required surface without the clause (e.g.Neo4jStorageProvider, which defers its declaration until the surface is complete) is matched structurally. Those edges carrydetection: structural— high-confidence, but inferred. A nominal-only scan would silently drop the storage/traversal backend that powers this graph. DOCUMENTSis still link-bound. A markdown link to any modelled.tssource file now resolves — to its specific construct (provider/server/tool) or otherwise to itsModulenode — so DOCUMENTS reaches the whole file tier, not just the handful of provider/tool files. But docs still have to actually link the file; prose that only names a symbol lands in the broaderMENTIONSedges (now including error classes), andDESCRIBESremains the reliable package↔README link.- An import cycle isn't always a runtime crash.
inImportCycleis computed over the fullIMPORTSgraph, which includes type-only imports and barrel re-exports. A cycle routed only through type-only edges erases at compile time and won't crash; a value-import cycle is the real hazard. The flag is a high-signal lead for the "fix the circularity" convention — read the cycle the rebuild logs, then confirm whether the back-edge carries runtime values. Moduleis a file, not a symbol.IMPORTSanswers "which files import this file/package", not "which files use this specific function". That's the 80/20 for refactors; symbol-level precision is a deliberate non-goal of this layer. Modules are also intentionally notCONTAINS-anchored to their package (186 of them would swamp a package's neighborhood) — filter on theModule.packageproperty instead.throwsRawErroris a lead, not a verdict. It flags a file that throws an error not in the modelled typed hierarchy (the builtinError, or a module-private error) — a starting point for the "typed errors only" convention, not proof of a violation.- It can go stale. The graph reflects the repo only as of the last
pnpm code-graph:rebuild. This is intentional — refreshing is a one-command, opt-in step, never wired into the build. Check the repository metadata'sgitCommit/builtAtto see how current it is.