# Technical Documentation: DeusData/codebase-memory-mcp

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/DeusData/codebase-memory-mcp)

### Overview (README.md)

# codebase-memory-mcp: local code intelligence for AI agents via MCP

[](https://github.com/DeusData/codebase-memory-mcp/releases/latest)
[](LICENSE)
[](https://github.com/DeusData/codebase-memory-mcp/actions/workflows/release.yml)
[](#tests)
[](#mcp-tools)
[](https://github.com/DeusData/codebase-memory-mcp)
[](#hybrid-lsp)
[](https://github.com/DeusData/codebase-memory-mcp)
[](https://github.com/DeusData/codebase-memory-mcp)
[](https://github.com/DeusData/codebase-memory-mcp/releases/latest)
[](https://scorecard.dev/viewer/?uri=github.com/DeusData/codebase-memory-mcp)
[](https://slsa.dev)
[](https://github.com/DeusData/codebase-memory-mcp/releases/latest)
[](https://arxiv.org/abs/2603.27277)
[](https://discord.gg/KguGXxg2X)

**Understand your repository, from source code to the systems around it.** codebase-memory-mcp is an MCP server that connects code, documentation, configuration, and infrastructure in a persistent local knowledge graph. Use it to find an unfamiliar feature, follow a call across services, trace a deployment back to its source, or assess which parts of a project a change could affect.

Give your coding agent structural context through [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), or query the graph directly from the CLI. **The MCP server is the core of the project.** Its optional visual companion, **CBM Lens, is in open beta (v0.0.1)**, with a source reader, architecture maps, the Galaxy graph, coverage diagnostics, architecture decisions, and local code chat.

The native C backend exposes **17 MCP tools**, parses [languages and file formats](#language-support) with vendored grammars, and adds [Hybrid LSP](#hybrid-lsp) type resolution for supported languages. The installer supports **45 automatic or conditional client integrations**, including client configuration, skills, agent profiles, and applicable hooks. Indexing and graph queries need no hosted service or API key. Focused graph queries can reduce repeated file reads and token use; see the [measured results](#performance).

[Quick start](#quick-start) · [Capabilities](#features) · [Documentation](#documentation-connected-to-code) · [Infrastructure](#infrastructure-and-deployment) · [Smart CI](#smart-ci-and-test-impact) · [Explore CBM Lens](#cbm-lens-beta-code-visualization-and-source-navigation) · [Agent setup](#agent-setup-skills-profiles-and-hooks) · [MCP tools](#mcp-tools)

> **Local processing:** Source analysis and graph storage run on your machine. CBM Lens can optionally download a model for browser AI after you enable it; inference stays on the device. The installer writes agent configuration for the clients you select. See [Security](#security) for release verification and [SECURITY.md](SECURITY.md) for reporting issues.

**See the graph with CBM Lens (open beta v0.0.1).** Folders become nested platforms, files and symbols become selectable blocks, and arrows show indexed relationships. The recording opens `src/foundation` in the map of this repository, selects `arena.c` and opens its source. Height can show source size; hotspot gravity reflects static fan-in. [Still image](docs/images/lens-architecture.png).

<p align="center">
  <img src="docs/images/lens-overview.gif" alt="CBM Lens Beta architecture walkthrough: open src/foundation, select arena.c and read its source beside the map" width="1000">
</p>

**Galaxy.** Explore the code knowledge graph in 3D. The recording follows the outgoing relationships of `cbm_arena_alloc` one layer further, switches to the hierarchy and shows the two-hop call path to `cbm_alloc`. Edge pulses show relationship direction; they do not represent live execution. [Still: two dependency layers around `cbm_max_file_bytes`](docs/images/lens-galaxy.jpg).

<p align="center">
  <img src="docs/images/lens-galaxy.gif" alt="Code knowledge graph in CBM Lens Beta Galaxy: expand cbm_arena_alloc, switch to the hierarchy and follow its call path through arena_grow to cbm_alloc" width="1000">
</p>

**Explore.** Browse files in an IDE-style workspace, read syntax-highlighted source in Monaco, and keep the code graph alongside it. File impact shows what depends on the open file, and Code details shows the callers and calls of the symbol under the caret. Select code to [ask the local chat about it](#chat-with-your-codebase-locally).

<p align="center"><img src="docs/images/lens-explore.jpg" alt="CBM Lens Beta Explore: limits.c in the source reader beside its file impact summary and direct graph dependencies" width="1000"></p>

<details>
<summary>More views: system structure and behavior</summary>

<p>System structure groups symbols into components and lets you inspect their dependencies.</p>
<p align="center"><img src="docs/images/lens-system-structure.png" alt="CBM Lens Beta system structure: source groups of this repository with their dependencies, src selected" width="1000"></p>

<p>Behavior shows possible calls from a selected operation and the source evidence behind them.</p>
<p align="center"><img src="docs/images/lens-behavior.png" alt="CBM Lens Beta Behavior view: the possible direct calls of compileCards and its declaration" width="1000"></p>

</details>

The high-resolution GIFs show CBM Lens Beta on 20 October 2026; the still images were captured on 12–13 October. All show public source from this repository. See [capture details](docs/images/README.md) for each capture's build, indexed scope and recording setup.

## Why codebase-memory-mcp

- **Find the right code with context**: combine symbol, text, and semantic search with callers, dependencies, source snippets, and file outlines. Targeted queries and compact, paged results can reduce token use and repeated file reads; see [Performance](#performance) for scoped measurements.
- **Connect documentation to implementation**: index document sections, PDF text pages, architecture decisions, and supported references into code. Find the explanation alongside the symbol.
- **Follow the system beyond source files**: trace supported relationships through Docker, Kubernetes, Helm, Kustomize, Terraform/OpenTofu, Terragrunt, Ansible, scripts, Make, and CI configuration.
- **Understand a change before running it**: inspect affected symbols, compare indexed snapshots, and find related tests. [Experimental Smart CI](#smart-ci-and-test-impact) combines static relationships and per-test coverage for supported C/C++ test harnesses, falling back to broader runs when evidence is insufficient.
- **Explore visually with CBM Lens Beta**: read source, follow call paths, inspect architecture and coverage gaps, manage architecture decisions, and ask a local model about selected code.
- **Equip your coding agents**: `install` sets up supported client configuration, durable guidance, skills, Scout/Verify/Auditor profiles, and applicable context hooks. Support varies by client; see the [integration matrix](#multi-agent-support).
- **Keep context local and reusable**: indexes persist between sessions, eligible active MCP sessions can keep them synchronized, and optional graph artifacts let a team share a baseline.
- **Run a native tool on your machine**: standard releases include CBM Lens for macOS, Linux, and Windows. The native runtime needs no Docker or language runtime; parser grammars are included.

## Quick Start

**Install the latest release** (macOS / Linux):
```bash
curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash
```

The standard native release includes CBM Lens. Start it after installation with `codebase-memory-mcp daemon start --open --port=9749`. The [Nix packages](#nix-flake) and [source builds](#build-from-source) also offer builds without the UI.

**Windows** (PowerShell):
```powershell
# 1. Download the installer
Invoke-WebRequest -Uri https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.ps1 -OutFile install.ps1

# 2. (Optional but recommended) Inspect the script
notepad install.ps1

# 3. Unblock the downloaded file (removes Mark-of-the-Web restriction added by browsers/Invoke-WebRequest)
Unblock-File .\install.ps1

# 4. Run it
.\install.ps1

```

> **Note:** If you see a script execution policy error, run `Set-ExecutionPolicy -Scope Process Bypass` first, or invoke with `PowerShell -ExecutionPolicy Bypass -File .\install.ps1`.

Options: `--skip-config` (binary only, no agent setup), `--dir=<path>` (custom location).

> **Antivirus note:** Microsoft Defender may flag a release binary as
> `Trojan:Script/Wacatac.B!ml`. This is a known false positive; typically 61 of
> ~62 engines return clean, and the same detection family hits `gh`, llama.cpp,
> Godot and Microsoft's own Go toolchain. See
> [Antivirus False Positives](SECURITY.md#antivirus-false-positives) for the
> evidence, how to verify the artifacts yourself, and how to report it if you
> think we are wrong.

Restart your coding agent. Say **"Index this project"**: done.

<details>
<summary>Manual install</summary>

1. **Download** the archive for your platform from the [latest release](https://github.com/DeusData/codebase-memory-mcp/releases/latest):
   - `codebase-memory-mcp-<os>-<arch>.tar.gz` (macOS/Linux) or `.zip` (Windows)

2. **Extract and install** (each archive includes `install.sh` or `install.ps1`):

   macOS / Linux:
   ```bash
   tar xzf codebase-memory-mcp-*.tar.gz
   ./install.sh
   ```

   Windows (PowerShell):
   ```powershell
   Expand-Archive codebase-memory-mcp-windows-amd64.zip -DestinationPath .
   Unblock-File .\install.ps1
   .\install.ps1
   ```

3. **Restart** your coding agent.

The `install` command automatically strips macOS quarantine attributes and ad-hoc signs the binary; no manual `xattr`/`codesign` needed.
</details>

### Agent setup: skills, profiles, and hooks

Installation also equips supported coding agents to use the graph in daily work. The native `install` command detects clients and installs the integration pieces their documented formats support:

| Component | What it adds |
|-----------|--------------|
| **MCP configuration** | Connects the client to the local server and its tools. |
| **Instructions and skills** | Explains graph-first exploration, index freshness, coverage checks, and when to read source. |
| **Scout, Verify, and Auditor profiles** | Offers fast provisional lookup, task-directed verification, or bounded full verification, where custom agents are supported. |
| **Session and compaction hooks** | Restores graph context at supported lifecycle events. |
| **Search and read hooks** | Adds relevant graph context to supported searches and flags recorded coverage gaps after supported file reads. |

Hook augmentation is context-only and fail-open: it does not replace or block the requested tool call. Features vary by client and platform; some clients use durable instructions or a parent-agent handoff instead of hooks or direct subagent MCP access. Codex users review and trust installed hooks through `/hooks`. See [Multi-Agent Support](#multi-agent-support) and [Sessions, compaction, and subagents](#sessions-compaction-and-subagents) for exact support and activation requirements.

### Session Coordination Daemon

CBM automatically shares one per-account coordination daemon across Claude Code, Codex, OpenCode, and every other configured client. There is no opt-in setting for MCP servers or hook clients: the first daemon-backed CBM session starts it, each session registers its own work, and a session-managed daemon stops after the final session. A daemon started with `daemon start` remains running until stopped. The daemon owns long-lived background services such as watchers, shared indexing jobs, and the optional UI. Closing one session cancels work owned only by that session, while work still needed by another session continues.

The detached daemon does not depend on an MCP frontend's stderr. It keeps owner-only durable records under the canonical `${CBM_CACHE_DIR}/logs` directory (default `~/.cache/codebase-memory-mcp/logs`):

| File | Contents |
|------|----------|
| `cbm-daemon.log` | Daemon lifecycle, watcher/indexing, UI, resource, and error events. |
| `daemon-conflicts.ndjson` | Exact-build, coordination-ABI, and cache-root admission conflicts. |
| `activation-events.ndjson` | Install/update/uninstall activation progress and outcomes. |

Thin frontends still write immediate startup and session-specific errors to their own stderr; MCP JSON-RPC stdout remains clean.

All active CBM processes must run the exact same version, executable build, coordination ABI, and canonical cache root. Equivalent `CBM_CACHE_DIR` aliases resolve to the same root; a genuinely different root is rejected while any CBM process is active. MCP servers, hooks, one-shot CLI commands, temporary index workers, and the daemon share a crash-safe OS admission barrier; starting an ordinary conflicting process fails before doing work and records an explicit conflict in `${CBM_CACHE_DIR}/logs/daemon-conflicts.ndjson`.

The native `install` and `uninstall` commands are the deliberate exception to that conflict rule. For installation, download, verification, and private same-filesystem staging finish before activation so a bad candidate never disrupts active work. The native `update` command prints the installer command that performs the update. Activation then publishes account-wide maintenance intent, asks the daemon and every temporary local operation to cancel, and waits to a finite deadline for all coordinated CBM processes to exit. It holds the admission and lifetime barriers exclusively while changing the active binary, configuration, PATH, or indexes. New CBM work cannot enter during this window. Activation progress and results are recorded in `${CBM_CACHE_DIR}/logs/activation-events.ndjson`, and a successful command tells you to restart open coding-agent sessions so they launch the activated build.

Package-manager setup (npm, PyPI, or Go) verifies and caches the native executable for that package. It does not replace the active native installation and therefore does not stop running CBM sessions. When that cached binary is executed, it still enters the same exact-build admission barrier. The shell and PowerShell installers invoke the verified candidate's native `install` command, so they do receive the full account-wide activation guarantee.

The ordinary `cli` mode runs one command locally, but it is not disconnected from the daemon above: it connects to the shared coordination daemon (starting one if none is already running) for that same admission barrier plus per-project locks on graph mutations. Its connection is a `cli_session`: it does not register a project watcher or open a browser, and it holds the admission lease only for the command's lifetime. The shared daemon may serve Lens according to its UI configuration and available assets. If the command's own connection is what started the daemon, that daemon exits again once the command closes and no other session is attached; if a daemon was already running, the command simply joins and leaves it exactly as found. While the command is running, a temporary monitor separately lets activation cancel that operation and its supervised worker safely; the monitor exits with the command and never becomes a standing daemon. See [CLI Mode](#cli-mode) for details.

### CBM Lens Beta: code visualization and source navigation

**CBM Lens is an optional visual companion to the MCP server, currently in open beta (v0.0.1).** Standard native releases include its browser interface, so you can launch Lens directly after installation; UI-enabled source builds include it too. It builds on [CodeAtlasWeb](graph-ui/README.md) by [Bernhard Jackiewicz](https://github.com/BernhardJackiewicz) (design and tracking in #1964). Lens is served on loopback. Browser AI downloads its optional model only after you enable it; inference stays on-device. Each panel names where its facts come from, and what the index did not record is shown as a gap. Start the local server:

```bash
codebase-memory-mcp daemon start --open --port=9749
```

The browser opens at `http://127.0.0.1:9749`. A permanent daemon keeps the UI available after the command exits. Use `daemon status` to inspect it and `daemon stop` when you are finished. For an MCP session, `--ui=true --port=9749` enables the same shared UI. Active clients must use the same executable build and cache directory.

**Open your first repository**

1. Open the project menu and choose **Add project index**. You can also use `Alt+P`.
2. Choose a local repository folder and name the index. Files stay in place.
3. Start indexing and follow its reported status. You can close the dialog while it runs and reopen its activity from the project menu.
4. Open the indexed project. Start with Architecture for an overview or Explore to read a file.

To bookmark a view, use `?project=<name>&workspace=architecture`. Other workspace values are `explore`, `galaxy`, `adr`, `coverage`, and `system`.

| Workspace | What you can do |
|-----------|-----------------|
| **Explore** | Navigate the indexed file tree and read source in Monaco. File impact shows what depends on the open file; Code details shows the callers, calls, references and related tests of the symbol under the caret. |
| **Galaxy** | Search and focus graph nodes, load their neighbors layer by layer, switch to hierarchy, follow a path, and open source. Back and Forward return to earlier views. The coverage shadow marks known gaps around the loaded graph. |
| **Architecture** | Explore the folder map, entry points, routes, hotspots, system structure, and behavior paths. Select a node or connection to inspect its evidence. |
| **ADR** | Read and edit the project's architecture decision record. Save explicitly; the document is stored with the index and shared with `manage_adr`. |
| **Coverage** | Check indexed, partially parsed, skipped, and excluded paths, together with available freshness information. |
| **System** | Inspect daemon activity, indexes, and logs. Filter logs by project, severity, or text. |

**Architecture views**

- **Overview:** move through source folders and files on a 3D map. Choose source-size or uniform height, language or kind colors, and relationship filters. Switch to Entry points to follow a static call graph from a starting symbol.
- **Routes:** switch between the service map and endpoint connections. Deployment metadata and indexed route evidence are shown separately; unresolved registrations remain visible.
- **Hotspots:** inspect symbols using measured signals such as fan-in and complexity. Gravity wells help locate frequently referenced areas. They are not a code-quality score.
- **System structure:** inspect component dependencies, expand groups, isolate cycles, and follow connections back to source evidence.
- **Behavior:** choose a source and a reachable destination to inspect static call paths and their source statements. These paths show possible relationships, not a recorded execution order. Runtime observations require separately ingested traces.

Drag a 3D view to orbit, scroll to zoom, and select a node or edge to inspect it. Edge colors distinguish relationships, and pulses indicate their direction. Reduced-motion preferences pause the pulses. Large views show their limits and omitted counts. A missing edge can reflect an indexing or display limit, so check Coverage and the source before drawing conclusions.

#### Chat with your codebase locally

Open **Chat** with the panel button at the top right to ask about the code you are looking at. In Explore it uses the open file or the exact text you have selected; in Galaxy it uses the selected node and its loaded neighbors. You can also attach graph evidence through **Add context**.

1. Click the robot icon at the top right to open the agent configuration.
2. Choose a local model. The default Qwen2.5 Coder 0.5B downloads about 567 MB; Qwen3 0.6B, LFM2.5 1.2B and Qwen3.5 2B are larger options. All need WebGPU with `shader-f16` support.
3. Click **Download & load**. Model files come from Hugging Face; inference runs in your browser, with no hosted chat API or API key. A model that is already in the browser cache loads with **Load model (cached, no download)**.
4. Ask a question. The composer shows the file or selection that will accompany it. Select a smaller range when you want to focus the answer.

Questions about callers and callees, such as "Who calls cbm_pipeline_run?", are answered from the loaded graph without the model, and the answer says so. When you select a symbol, the chat lists its indexed facts and the lines of its code, and the model adds one sentence; that sentence is left out when it claims something the source does not show. Text written by the model is labelled as such.

The recording asks who calls `arena_grow` and what it calls, with both answers listed directly from the indexed graph beside the selected symbol. [Still image of a model explanation and graph answer](docs/images/lens-chat.png).

<p align="center"><img src="docs/images/lens-chat.gif" alt="Chat with your codebase locally in CBM Lens Beta: callers and callees of arena_grow answered directly from the indexed graph" width="1000"></p>

The chat history is kept for 24 hours in this browser, per project. Each new message uses the source currently open in the reader; earlier source snapshots are not silently accumulated, and messages about another file or selection are not sent along. If a prompt is too long for the model, older messages are left out first; if it still does not fit, CBM Lens asks you to select less code instead of cutting the code. This chat explains the context you give it; it cannot edit files or run tools. Check its answers against the source. The small default model words things loosely, especially in languages other than English.

#### Search

`Cmd+K` or `Ctrl+K` opens the Galaxy search. It finds symbols, files and folders by name or path in the whole index, also nodes that are not loaded yet. For callers and callees, ask in the chat; the answer is listed from the graph.

**Architecture decisions**

The ADR workspace keeps an unsaved draft per project in this browser tab. It checks whether the stored document changed before saving, but the API does not provide an atomic document lock. The editor accepts up to 8,000 UTF-8 bytes. Saving changes the indexed decision document, not a file in your repository.

<details>
<summary>Experimental agent activity</summary>

The Agents workspace and activity overlays are hidden in normal builds. To include them in a source build:

```bash
VITE_EXPERIMENTAL_AGENTS=true scripts/build.sh --with-ui
```

Activity comes from explicitly configured tool hooks. The bundled adapter supports Claude Code PostToolUse; other clients need their own adapter. Recorded events go to the local daemon, and the reader must enable activity before the browser polls them. This does not automatically observe every agent. See the [frontend setup guide](graph-ui/README.md#experimental-agent-activity).

</details>

For troubleshooting, open System logs. Frontend errors and failed requests are recorded locally. `GET /api/ui-log` reads frontend entries from the daemon's SQLite journal; `${CBM_CACHE_DIR}/logs/ui.log` is also retained as a rotating JSONL export. See the [frontend README](graph-ui/README.md) for log scope, model setup, and development details.

### Auto-Index

Enable automatic indexing on MCP session start:

```bash
codebase-memory-mcp config set auto_index true
```

`auto_index` is off by default. When enabled, new projects are indexed automatically on first connection. The configurable file limit is `config set auto_index_limit 50000`. Previously indexed projects can be watched independently through `auto_watch` (on by default), subject to the session rules below.

The watcher follows MCP sessions: it watches the project each open MCP session is rooted in (the client's working directory) and stops when the last session for that project closes. A repository indexed with `cli index_repository`, or indexed from a session rooted somewhere else, is not watched; re-run `index_repository` after changing it. `index_status` reports this in its `watch` object: `watched`, and either the poll cadence and last scan time or the `reason` it is not watched.

Watcher registration is controlled separately by `auto_watch` (default `true`). Set `config set auto_watch false` to keep a session from registering its project with the background watcher; useful when working across many projects and you want each session contained to explicit indexing.

To turn the watcher off entirely, set `config set watcher_enabled false` (default `true`): the background poll thread never starts and no project is registered, while `auto_index` and manual `index_repository` keep working. Unlike `auto_watch`, which is consulted per session, `watcher_enabled` is read once when the background daemon starts, so run `codebase-memory-mcp daemon stop` after changing it; reconnecting your MCP client alone will not restart the daemon. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md#2-cli-managed-runtime-settings).

The watcher follows **git** projects only by default: a project indexed from a directory that is not a git repository is not watched and stays at its last index until you run `index_repository` again. To have such roots polled too, set `config set watch_non_git true` (default `false`, read when the daemon starts): they are then checked on the same cadence with a file-tree scan that uses the indexer's own ignore rules.

### Keeping Up to Date

**Updates run from the install script on every platform, not from inside the running binary.** `codebase-memory-mcp update` validates your flags and then prints the exact command to run:

```bash
# macOS / Linux
bash "<install-dir>/install.sh"
```

```powershell
# Windows
powershell -File "<install-dir>\install.ps1"
```

The install script is placed next to the binary at install time, so the printed path resolves beside the executable. It is idempotent, so re-running it *is* the update: it stops the daemon, retires the running binary, installs the new one, and cleans up.

Why it works this way. On Windows it is a hard requirement; a running executable cannot replace its own image, so the swap has to happen from a process that is not the binary being replaced. On macOS and Linux it is a deliberate choice: an in-process updater is structurally a downloader (fetch an archive, verify it, unpack it, mark a file executable, run it), and shipping that composite in every binary to serve a command most people run a handful of times is a poor trade. The release archives now carry no download URLs at all, and **cbm makes no network request of its own accord**: it does not check for new versions in the background, and nothing phones home. You find out about releases from the install script, your package manager, or GitHub.

If PowerShell refuses to run the script because the file came from the internet, `Unblock-File` it first.

Installed through **npm or pip**? Update with your package manager on every platform (`npm install codebase-memory-mcp@latest` / `pip install -U codebase-memory-mcp`).

### Uninstall

```bash
codebase-memory-mcp uninstall
```

Removes owned agent config entries, skills, hooks, instructions, and the installed binary. Existing graph indexes are listed and **kept by default**. `-y`/`--yes`, `-n`/`--no`, and noninteractive input keep them unless `--delete-indexes` is explicitly given. An interactive terminal without that flag is asked separately; the default answer is to keep them.

```bash
codebase-memory-mcp uninstall -y --delete-indexes   # also delete every project index
codebase-memory-mcp uninstall --dry-run --delete-indexes   # preview; change nothing
```

`--delete-indexes` is explicit consent and takes precedence over `--no`; `--dry-run` always preserves the files. When indexes are kept, uninstall prints their cache directory (`${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}`) and how to remove them.

The install script placed beside the binary is **reported, not deleted**: uninstall prints its path and the `rm` command for it. It is left alone on purpose: it may be your own copy, a symlink into a checkout, or managed by a package manager, and an uninstaller should not delete a file it cannot prove it owns.

## Features

### Graph & analysis
- **Architecture overview**: `get_architecture` returns a compact summary by default. Request specific `aspects` for routes, hotspots, dependencies, clusters, or cycles. `system_structure` provides the projection used by CBM Lens and must be requested on its own.
- **Architecture Decision Records**: `manage_adr` persists the project's decision record across sessions; indexing also extracts supported ADR metadata and supersession from documentation files
- **Louvain community detection**: Discovers functional modules by clustering call edges
- **Change impact**: `detect_changes` maps uncommitted changes to affected symbols with risk classification and can report test impact with `scope: "tests"`; `compare_graphs` compares indexed snapshots
- **Runtime observations**: `ingest_traces` adds observed call relationships from separately collected traces alongside static analysis; static call paths alone do not show what ran
- **Call graph**: Resolves function calls across files and packages (import-aware, type-inferred)
- **Dead code candidates**: find functions with no indexed callers, excluding entry points. Check dynamic dispatch, coverage gaps, and framework conventions before removing code.
- **Cypher-like queries**: `MATCH (f:Function)-[:CALLS]->(g) WHERE f.name = 'main' RETURN g.name`

### Search
- **Semantic search** (the `semantic_query` parameter of `search_graph`, not a separate tool): vector search across the entire graph, powered by bundled Nomic `nomic-embed-code` embeddings (40K tokens, 768d int8) compiled into the binary; no API key, no Ollama, no Docker. 11-signal combined scoring (TF-IDF, RRI, API/Type/Decorator signatures, AST profiles, data flow, Halstead-lite, MinHash, module proximity, graph diffusion).
- **BM25 full-text search** via SQLite FTS5 with `cbm_camel_split` tokenizer (camelCase / snake_case aware)
- **Structural search** (`search_graph`): regex name patterns, label filters, min/max degree, file scoping
- **Code search** (`search_code`): graph-augmented grep over indexed files only

### Documentation connected to code

Find the explanation behind a symbol and follow supported references back to its implementation. Documentation participates in the graph alongside source files:

- **Markdown, reStructuredText, and AsciiDoc** become navigable document sections; **PDFs with text layers** become searchable text pages. PDF extraction does not perform OCR.
- **Document-to-code references** and supported language doc-comment references create `MENTIONS` relationships when the target can be resolved.
- **Architecture decision records** contribute metadata such as status, deciders, and supersession, so decisions can be explored with the code they describe.
- **Supported diagrams and OpenAPI descriptions** contribute structured entities. Extraction and reference resolution differ by format; not every textual mention becomes a graph edge.

Use graph and text search to locate the relevant section, follow its recorded relationships, and open the source evidence. Check `check_index_coverage` before treating a missing result as proof that the repository has no such documentation or reference.

### Infrastructure and deployment

Ask **"What deploys this service?"**, **"Which image runs it?"**, or **"Where does this configuration reach code?"** CBM connects supported infrastructure forms to the source they build, configure, and run:

- **Containers and clusters**: Dockerfiles, Compose services, Kubernetes resources, Helm charts and releases, and Kustomize overlays and patches.
- **Provisioning and configuration**: Terraform/OpenTofu resources and modules, tfvars overrides, Terragrunt dependencies, Ansible tasks and handlers, and supported configuration keys and environment-variable access.
- **Build and delivery**: shell commands, Make targets, and supported CI steps that build images, invoke repository scripts, or deploy resources.

For resolvable inputs, a trace can follow **Service → workload → image → Dockerfile → program**, or follow deployment and configuration relationships. Use `trace_path` modes for topology, deployment, and run paths. These are static relationships from supported source forms; dynamic values and unresolved templates can leave gaps. See [Edge types](#edge-types-selected) for the exact relationship rules and limitations.

### Cross-service linking
- **HTTP** route ↔ call-site matching with confidence scoring
- **gRPC, GraphQL, tRPC** service detection with protobuf Route extraction
- **Channel detection** (`EMITS` / `LISTENS_ON`) for Socket.IO, EventEmitter, and generic pub-sub patterns across 8 languages with constant resolution

### Cross-repo intelligence
- **`CROSS_*` edges** link nodes across multiple repos indexed under the same store
- **Cross-repo architecture summary** combining services, routes, and dependencies across the indexed fleet
- **Run status**: `index_status` (and the UI layout next to `linked_projects`) carries a `cross_repo` object, `{"status":"never_run"}` until `index_repository(mode="cross-repo-intelligence")` has run with that project as the source, then `"ran"` with `last_run_at`, `outcome`, `targets`, `projects_scanned`, `total_cross_edges` and `skipped_projects`. That is how you tell "never linked" apart from "linked, nothing matched". The status is stored in the project's own index next to its `CROSS_*` edges, so a reindex that rebuilds the index drops both at once and the status goes back to `never_run`.
- **Skipped stores**: `target_projects: ["*"]` skips an index built before the #768 schema change rather than failing the whole run. The response lists each one as `skipped_projects: [{"name", "reason": "pre_768_schema", "hint": "reindex this project"}]`.

### Smart CI and test impact

Use the graph to explain which tests a change reaches, and use that evidence to focus supported CI runs. **Smart CI is experimental** and has two layers:

| Layer | What it does | What you supply |
|-------|--------------|-----------------|
| **CI job selection** | Chooses jobs from the changed-file list. | Repository-specific path and job rules; this repository's selector is an example. |
| **Test selection** | Combines static reachability, changed tests, and admitted per-test coverage to select suites or tests. | A supported C/C++ test harness, configuration, a coverage map, and a runner that accepts named selections. |

`codebase-memory-mcp test-impact select` produces a selection with reasons; `test-impact publish` produces the graph-and-coverage bundle used as its baseline. For agent-assisted investigation, `detect_changes` with `scope: "tests"` exposes test impact through MCP.

**Narrowing currently supports C/C++ family code with this repository's test conventions or declared equivalents.** It is not general test selection for every language CBM indexes. Missing, stale, incompatible, or incomplete evidence broadens the selection or returns `run_all`; static reachability without admitted coverage never omits a suite. Begin with report-only shadow mode and validate your harness before enabling gating.

In this repository, test narrowing is wired to the Linux x86_64 test leg when `TEST_IMPACT_MODE=gate`; release and dry-run workflows retain full execution. See the [Smart CI guide](docs/SMART_CI.md) for supported conventions, coverage admission, setup, version requirements, and fallback behavior.

### Edge types (selected)
- `CALLS`: a callable is invoked at the source site
- `CALL_REFERENCE`: a callable is used at a supported reference site (for example, a direct value argument) and resolves to one exact target
- `USAGE`: an identifier is used, but a unique callable target is not proven (including ambiguous or complex expressions)
- `IMPORTS`, `DEFINES`, `IMPLEMENTS`, `INHERITS`
- `HTTP_CALLS`, `ASYNC_CALLS` (cross-service); a shell script, Make recipe or CI step running `curl` / `wget` on a URL it states (constant words resolved, unknown parts `{}`) to its `Route` with the method its flags give: a Kubernetes Service of the repository by the path, as code clients do, any other host (a download, localhost) with the host kept; the edge keeps `via`, `method`, `url_path`, `host`, never the user info or the query
- `SPAWNS`: the caller starts another program (`subprocess.run`, `exec.Command`, `posix_spawn`, `Process.Start`, a shell command or Make recipe running a program by path, ...); the target is a `Process` node named after the program, or `<dynamic>` when the program is not a literal. When a shell script, Make recipe, GitHub Actions step or Prow job runs a script of the repository (`./hack/x.sh`, `bash hack/x.sh`), the target is that script's `Module` (else its `File`); `make TARGET` (also `$(MAKE)`, `-C`, `-f`) targets the rule of that target, followed through included makefiles; edges from CI carry `ci` and `job`. A Dockerfile's last stage `ENTRYPOINT` (else `CMD`) spawns what the container runs (`api: dockerfile`): the repository script or source copied into the image, the Go `main` of a binary a stage builds with `go build -o`, else a `Process`
- `INFRA_MAPS`: a Kubernetes Service to each workload whose pod labels carry every `key: value` of the Service's selector, within one namespace
- `CONNECTS_TO`: a Kubernetes workload or compose service to the Service its containers' environment addresses (`PRODUCT_CATALOG_SERVICE_ADDR: productcatalogservice:3550`, a URL's host, `<svc>.<ns>.svc...`, a bare name on an `*_ADDR` / `*_HOST` / `*_URL` / ... key; compose: the project's `.env` interpolated): a Kubernetes Service of the same bundle and namespace, or a compose service of the same file. The edge keeps `via: env`, `key` and `container`, never the value
- `USAGE` with `ref: image`: a Kubernetes workload (any pod template: containers, initContainers, CronJobs, custom resources) or a docker compose service to the `Image` it runs
- Helm templates are rendered for their images: with the chart's values (the `Chart` `DEPLOYS` them, `default_of`: the values keys read) and with each release's (a Flux HelmRelease, a kustomize `helmCharts` entry, a Terraform `helm_release`: `override_of`). An image the render cannot know is left out, never guessed: after a change of values it does not run exactly, a `tpl` or include it cannot look into, a release value it cannot read, or a work budget it runs past. Known limit: each template file renders on its own, while Helm renders a chart's files with one shared `.Values` (a subchart's is its parent's `.Values.<alias>`), so a chart that changes `.Values` in one file and reads it in another can still show a wrong image
- `DEPLOYS`: what puts manifests, kustomizations or charts on a cluster, to what it puts there: a Flux Kustomization or Argo Application to the kustomizations / manifests / chart of its path, a Flux HelmRelease to its chart (`spec.chart` or `chartRef` → OCIRepository), a shell command or Make recipe (`kubectl apply -f/-k`, `kustomize build | kubectl apply`, `helm install/upgrade/template`), a Go installer loading a chart with the Helm SDK, a Terraform `helm_release`, a Terraform `kubernetes_*` resource the object it declares (a `Resource` like a manifest's, `ecosystem: terraform`, when its name is a literal), a kustomization inflating a chart (`helmCharts`; its values file an `IMPORTS`). A chart outside the repository is an external `Chart`, keyed by its publisher when its repository URL is stated (`__chart__<repository>/<name>`, the same node in every project, the URL in `repository`), else one per project and name; the edge says `via` (`flux`, `argo`, `helmrelease`, `kubectl`, `kustomize`, `helm`, `helm-sdk`, `terraform`, `helmcharts`, ...)
- `DEPENDS_ON`: a Helm chart to the charts it declares (the local chart for a resolvable `file://` repository, otherwise an external `Chart`; the edge keeps `repository`), and a `go.mod` / `requirements.txt` file to an external `Package`; a Dockerfile to the `Image`s it builds on (`via: from`, `copy-from`); an `Image` to the Dockerfile it is built from (`via: build`; `tool`: `skaffold`, `compose`, `docker-build` for `docker` / `podman` / `nerdctl` / `buildah` build commands in scripts, Make and CI); a compose service to the services its `depends_on` / `links` name
- `CONFIGURES`: code reading an environment variable to the shared `EnvVar` of that name (`strategy: env_access`); a Kubernetes container, compose service or Dockerfile (`ENV` of the image) setting one to that container's own `EnvVar` (`strategy: env_set`, with `source`: `value`, `configMapKeyRef`, `secretKeyRef`, `fieldRef`, ..., `compose`, `dockerfile`; values are never stored); and that container's `EnvVar` to the code reading the name inside the files its image is built from (`strategy: env_read`, with `dockerfile`): the image's files are the sources its Dockerfile's `COPY` / `ADD` put into it, less the context's `.dockerignore`
- `CONFIGURES` with `strategy: key_symbol`: a code symbol to the config key it names (`config_key`). A YAML, JSON, HCL (Terraform, OpenTofu, tfvars), TOML or INI key links by exact name (`maxRetryCount` = `max_retry_count`) and only to code inside the config file's component: its directory, or the one around a `config/`, `configs/`, `conf/`, `settings/` or `etc/` directory. A `.env` or `.properties` key also links by substring (confidence 0.75)
- `OVERRIDE` with `via: patch`: a Kustomize patch to each object of its kustomization's build it changes (child → base): a patch file's documents to the object of the same kind and name; an inline patch (from the kustomization) or a JSON6902 operation file to what its `target` selects (group, version, kind, name, namespace, label selector); a component's in the build of each kustomization including it. The edge keeps `kustomization`, `patch` and `line`; a patch file is an `IMPORTS` (`local_name: patches`) of its kustomization, never a member of the build
- `OVERRIDE` with `via: namePrefix` / `nameSuffix`: an object a kustomization renames, from its variant: a `Resource` named with the deployed name (`capi-` + `controller-manager`; the prefixes of nested kustomizations outermost first, suffixes innermost first) in the namespace that build gives it, `ecosystem: kustomize`, which the kustomization's file `DEFINES`. CRDs, Namespaces and API services keep their names, as kustomize does. The edge keeps `kustomization` and `line`
- `USAGE` with `via: terraform`: a Terraform / OpenTofu reference (`var.X`, `local.X`, `module.X`, `data.T.N`, `T.N`) from the top-level block holding it to the block of its module (the `.tf` / `.tofu` files of its directory) that declares it (a `locals` entry: its `locals` block); an entry of the block's `depends_on` list is a `DEPENDS_ON` instead. The edge keeps `ref` (`var`, `local`, `module`, `data`, `resource`) and `line`; a name only mentioned in string text is no reference. A module call with a local source (`module "net" { source = "./modules/net" }`) `IMPORTS` (`local_name`: the module's name) the `Folder` of that directory (the root directory: the `Project`), and `module.net.id` (also `module.net[0].id`, `module.net[*].id`) `USAGE`s (`ref: output`) the `output "id"` there, beside `module.net`
- `OVERRIDE` with `via: tfvars`: a `.tfvars` assignment to a variable the module of its own directory declares, from the file's `Variable` to that `variable` block (the edge keeps `line`); a `.tfvars` file whose own directory declares none of its names sets the nearest ancestor module's that does, the edge marked `inferred` (such a file is applied by a `-var-file` the repository does not state)
- Terragrunt (`via: terragrunt`): a unit (`terragrunt.hcl`), evaluated in its own context: an `include` `IMPORTS` (`local_name`) the file its `path` names; a `read_terragrunt_config` `IMPORTS` (`read`: the file holding the call) the file it names, from the block holding the call or, for a call in an included file, from the unit's `include` block that brings it in; a `dependency` `DEPENDS_ON` the other unit; a local `terraform` `source` `IMPORTS` its `Folder`. Paths may use `find_in_parent_folders`, `dirname` and `get_terragrunt_dir`; anything else is unknown and links nothing
- Ansible (`via: ansible`): a playbook or task file (a YAML file whose top level is a sequence) `IMPORTS` (`local_name`: the directive) what its directives name: `import_playbook` a playbook, `include_tasks` / `import_tasks` / `include` a task file (from its directory, else its role's `tasks/`), `roles:` items and `include_role` / `import_role` a role's `Folder` (`roles/<name>` at or above its directory). A templated target links nothing. Each task and handler is a `Function` (named by its `name:`, else its module; `source: ansible`, `kind`, `module`) that its file `DEFINES`: in a playbook's plays, a role's `tasks/` and `handlers/` files, and the files those include; a block's tasks, not the block. A `notify` `CALLS` (`via: notify`) each handler of that name or `listen` topic in the task's own role (else its file). A `kube` (kubespray's module) or `kubernetes.core.k8s` task `DEPLOYS` (`via: ansible`, `module`) the repository file its play rendered the applied manifest from: the `template` (in the role's `templates/`) or `copy` (its `files/`) task whose `dest` is the task's `filename`, item by item through `register` / `with_items` / `loop` lists the play states (a literal list, a `set_fact`, the role's `defaults` / `vars`), or a `k8s` `template:` argument
- `EMITS`, `LISTENS_ON` (channels)
- `DATA_FLOWS` with arg-to-param mapping + field access chains
- `SIMILAR_TO` (MinHash + LSH near-clone detection, Jaccard scored)
- `SEMANTICALLY_RELATED` (vocabulary-mismatch, same-language, score ≥ 0.80)

### Indexing pipeline
- **162 vendored tree-sitter grammars** compiled into the binary
- **Generic package / module resolution**: bare specifiers like `@myorg/pkg`, `github.com/foo/bar`, `use my_crate::foo` resolved via manifest scanning (`package.json`, `go.mod`, `Cargo.toml`, `pyproject.toml`, `composer.json`, `pubspec.yaml`, `pom.xml`, `build.gradle`, `mix.exs`, `*.gemspec`)
- **Infrastructure-as-code indexing**: Dockerfiles (`Dockerfile.*`, `*.Dockerfile`, `Containerfile`: bases, what the container runs, `ENV`), container images (`Image`: the normalised repository, tag and digest on the edges; a repository built from several Dockerfiles, one per component tag, gets an `Image` per component), docker compose services (`Resource`), Kubernetes manifests (`Resource`, with `group`, `version`, `kind`, `namespace`, `labels` as `k=v,k2=v2`), Kustomize overlays (`IMPORTS` to their entries and patch files, `OVERRIDE` from their patches), Helm charts (`Chart`, `DEPENDS_ON`), Terraform / OpenTofu (`.tf`, `.tofu`, `.tfvars`) as graph nodes. A Service walks to the code it runs: Service → workload → `Image` → Dockerfile → `main`
- **[Hybrid LSP semantic type resolution](#hybrid-lsp)** for Python, TypeScript / JavaScript / JSX / TSX, PHP, C#, Go, C, C++, Java, Kotlin, Rust, and Perl; a lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers including tsserver / typescript-go, pyright, gopls, Roslyn, Eclipse JDT, and rust-analyzer (parameter binding, return-type inference, generic substitution, JSX component dispatch, JSDoc inference for plain JS files, namespace + trait + late-static-binding resolution for PHP, file-scoped namespaces + records + LINQ method syntax for C#, class-hierarchy + overload + lambda resolution for Java, extension-function + scope-function resolution for Kotlin, trait-method + UFCS resolution for Rust)
- **RAM-first pipeline**: LZ4 compression, in-memory SQLite, single dump at end. Memory released after.

### Distribution & operation
- **Native runtime set, zero infrastructure services**: SQLite-backed, persists to `~/.cache/codebase-memory-mcp/`
- **Auto-sync**: Background watcher detects file changes in the project an MCP session is working in and re-indexes it automatically (other indexed repos: re-run `index_repository`)
- **Route nodes**: REST endpoints are first-class graph entities
- **CLI mode**: `codebase-memory-mcp cli search_graph --project my-project --name-pattern ".*Handler.*"`
- **Available on**: npm, PyPI, Homebrew, Scoop, AUR, `go install`

### Indexing modes

| Mode | Use it for |
|------|------------|
| `full` | Full indexing with semantic analysis. This is the default. |
| `moderate` | A filtered index with semantic analysis. |
| `fast` | A filtered index without semantic analysis. |
| `cross-repo-intelligence` | Link services across indexed projects, optionally limited by `target_projects`. |

## Team-Shared Graph Artifact

Share a compressed graph snapshot so teammates can reuse an indexed baseline.

`.codebase-memory/graph.db.zst` is a zstd-compressed snapshot of the knowledge graph that lives next to your source. The artifact is opt-in: `index_repository` writes it only with `persistence: true` (the default is `false`), and once it exists later indexes refresh it. Local indexes persist regardless. When a teammate indexes a clone with an existing artifact, CBM imports that baseline and applies their local changes incrementally.

- **Format**: SQLite database, indexes stripped, `VACUUM INTO` compacted, then zstd 1.5.7 compressed (8 to 13:1 ratio typical)
- **Two tiers**:
  - **Best** (`zstd -9` + index strip + `VACUUM INTO`); written by `index_repository` with `persistence: true`
  - **Fast** (`zstd -3`); refreshes an existing artifact on every other index, including the watcher's low-latency incremental updates
- **Bootstrap**: when no local DB exists but the artifact is present, `index_repository` imports the artifact first, then runs incremental indexing, reusing the shared baseline where possible
- **Merge policy**: the first export creates `.codebase-memory/.gitattributes` with `merge=ours`. Check your repository's Git merge-driver setup before relying on it for concurrent artifact updates.
- **Commit it deliberately**: the artifact is rewritten on every index, including the watcher's Fast tier, and git stores each rewrite as a full new blob. Committing every refresh is what turns a 20 MB file into gigabytes of history; one team reached ~6 GB across ~350 commits of this single path. Pick a cadence (a release, a milestone, a nightly job) rather than committing every save.
- **Git LFS, if it must move on every commit**: track it from the **repo-root** `.gitattributes` and leave the auto-created `.codebase-memory/.gitattributes` in place; the nearer file goes on supplying `merge=ours`, and only `filter` comes from the root:
  ```gitattributes
  .codebase-memory/graph.db.zst filter=lfs diff=lfs merge=lfs -text
  ```
  Track only the `.zst`; `artifact.json` is small and carries the schema version. The attribute applies to future commits only. Removing existing blobs requires a separate history migration. Two costs to weigh before adopting it: GitHub meters LFS storage and bandwidth, and its objects cannot be pruned without contacting support; and every teammate needs `git lfs install`; without it their checkout leaves a pointer file where the artifact should be, the integrity-checked import refuses it, and they fall back to a full reindex.
- **Optional**: shared export is opt-in, and Git commits remain your choice. Add `.codebase-memory/` to `.gitignore` if you prefer everyone to reindex from scratch.

The artifact provides a shared baseline while each developer keeps a local query database.

## How It Works

codebase-memory-mcp is a **structural analysis backend**: it builds and queries the knowledge graph. The backend does not require a generative model. It relies on your MCP client (Claude Code, or any MCP-compatible agent) to be the intelligence layer.

```
You: "what calls ProcessOrder?"

Agent calls: trace_path(function_name="ProcessOrder", direction="inbound")

codebase-memory-mcp: executes graph query, returns structured results

Agent: presents the call chain in plain English
```

With MCP, your coding agent translates a question into tool calls. CBM Lens can also use an optional browser model for explanations; this does not change how the backend indexes or queries code.

## Performance

Published backend benchmarks on Apple M3 Pro. These are separate from the CBM Lens checks:

| Operation | Time | Notes |
|-----------|------|-------|
| **Linux kernel full index** | **3 min** | 28M LOC, 75K files → 4.81M nodes, 7.72M edges |
| Linux kernel fast index | 1m 12s | 1.88M nodes |
| Django full index | ~6s | 49K nodes, 196K edges |
| Cypher query | <1ms | Relationship traversal |
| Name search (regex) | <10ms | SQL LIKE pre-filtering |
| Dead code detection | ~150ms | Full graph scan with degree filtering |
| Trace call path (depth=5) | <10ms | BFS traversal |

**Memory-aware pipeline**: Indexing keeps hot data in memory and can spill extraction results to disk when needed. Memory is released back to the OS after indexing completes.

**More focused context, fewer tokens**: The project's [31-repository preprint](https://arxiv.org/html/2603.27277v1#S4.SS1) reports **10× lower token use** and **2.1× fewer tool calls** compared with file-by-file exploration. These March 2026 backend measurements cover the study's workloads and agent setup; the paper documents the full evaluation. An earlier [five-query comparison](https://github.com/DeusData/codebase-memory-mcp/blob/4ff6feeab639c4668da0f491930326ed5ec63093/README.md#performance) reported ~3,400 versus ~412,000 tokens for structural queries. The study's [system-performance measurements](https://arxiv.org/html/2603.27277v1#S4.SS3) include the Linux indexing and Cypher timings above.

Explore the [measurement guide](docs/MEASURING_SAVINGS.md) to evaluate quality, latency and token use on your own workload.

## Troubleshooting & Diagnostics

codebase-memory-mcp **indexes and queries locally and collects no telemetry**. When you hit something we can't reproduce on our side (a slow memory climb over hours, a performance regression, a leak that only appears after days of real use), **we have no diagnostic data unless you choose to send it.** Here is how to capture it yourself.

### Capture a diagnostics log

Set `CBM_DIAGNOSTICS=1` before the first daemon-backed MCP session starts, then reproduce the problem (let it run as long as it takes; a slow leak needs time to show in the trend). The shared daemon captures this setting from the session that starts it. If it is already running, close all daemon-backed sessions so it exits before changing the setting. The daemon creates a fresh owner-private `cbm-diagnostics-<pid>-<random>` directory below the system temp directory (`$TMPDIR` or `/tmp` on macOS/Linux, `%TEMP%` on Windows). The exact paths are recorded by the `diagnostics.start` event in `${CBM_CACHE_DIR}/logs/cbm-daemon.log`:

| File | What it is |
|------|------------|
| `trajectory.ndjson` | **The memory trajectory**: one JSON line every 5 s with `rss`, `committed` (Windows commit charge), `peak_*`, `page_faults`, `fd`, and `queries`. **This is the file we need for memory/leak reports**: the *trend over time* is what pinpoints a leak. It is **kept on disk after the server exits** (so you can grab it post-mortem) and rotates to `trajectory.ndjson.1` past ~8 MB. |
| `snapshot.json` | The latest snapshot only; handy for a quick live check. Removed on clean exit. |

The private randomized directory prevents another local account from pre-placing a link or special file at a predictable diagnostics path. Its `<pid>` component is the shared daemon's process ID, also recorded by the `daemon.start` event. Set the variable consistently in the `env` block of each agent's MCP server config, or export it before launching the first session.

### What to share

When you open a memory/performance issue, **attach the `.ndjson` trajectory**: it contains no source code or query text, only resource counters. If you'd rather not attach a file, paste it (or an agent's summary of it) into the issue: your assistant can read the NDJSON directly and report whether `rss`/`committed` grow monotonically, how fast, and relative to query count; which is exactly what we need to find the cause.

## Installation

### Pre-built Binaries

| Platform | Archive |
|----------|---------|
| macOS (Apple Silicon) | `codebase-memory-mcp-darwin-arm64.tar.gz` |
| macOS (Intel) | `codebase-memory-mcp-darwin-amd64.tar.gz` |
| Linux (x86_64) | `codebase-memory-mcp-linux-amd64.tar.gz` |
| Linux (ARM64) | `codebase-memory-mcp-linux-arm64.tar.gz` |
| Windows (x86_64) | `codebase-memory-mcp-windows-amd64.zip` |

Every release includes `checksums.txt` with SHA-256 hashes. The executable is self-contained; no adjacent data file is required. Linux `-portable` archives contain the fully static builds; ordinary platform archives use their native system ABI.

> **Windows note**: SmartScreen may show a warning for unsigned software. Click **"More info"** → **"Run anyway"**. Verify integrity with `checksums.txt`.

### Setup Scripts

<details>
<summary>Automated download + install</summary>

**macOS / Linux:**

```bash
curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/scripts/setup.sh | bash
```

**Windows (PowerShell):**

```powershell
irm https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/scripts/setup-windows.ps1 | iex
```

</details>

### AUR (Arch Linux)

```bash
yay -S codebase-memory-mcp-bin
```

```bash
paru -S codebase-memory-mcp-bin
```

The `codebase-memory-mcp-bin` package is available at: https://aur.archlinux.org/packages/codebase-memory-mcp-bin

### Nix (flake)

The flake exposes two server packages plus the standalone frontend:

| Package | Contents |
|---------|----------|
| `default` (`codebase-memory-mcp`) | Standard server, no UI |
| `codebase-memory-mcp-ui` | Server with CBM Lens embedded (`--ui=true` works) |
| `graph-ui` | Just the built CBM Lens assets (`dist/`) |

Run directly without installing:

```bash
# Standard server
nix run github:DeusData/codebase-memory-mcp

# Server with CBM Lens embedded
nix run github:DeusData/codebase-memory-mcp#codebase-memory-mcp-ui -- --ui=true --port=9749
# then open http://127.0.0.1:9749
```

Or build a binary into `./result/bin/codebase-memory-mcp`:

```bash
nix build github:DeusData/codebase-memory-mcp                          # standard
nix build github:DeusData/codebase-memory-mcp#codebase-memory-mcp-ui   # with UI
```

Working in a clone? Use `.` in place of the flake URL, e.g. `nix run .#codebase-memory-mcp-ui -- --ui=true`, or drop into a shell that puts the binary on `PATH` with `nix shell .#codebase-memory-mcp-ui`.

> **Note:** launched by hand (not from an MCP client) the server exits as soon as `stdin` closes; that's normal MCP behaviour. Keep `stdin` open while testing the UI, e.g. `sleep infinity | codebase-memory-mcp --ui=true --port=9749`. The `codebase-memory-mcp-ui` package embeds the UI at build time; `nix run`'ing the standard `default` package with `--ui=true` will refuse to start the HTTP server.

### Install via Claude Code

```
You: "Install this MCP server: https://github.com/DeusData/codebase-memory-mcp"
```

### Build from Source

<details>
<summary>Prerequisites: C/C++ compiler, zlib, and Node.js for CBM Lens</summary>

| Requirement | Check | Install |
|-------------|-------|---------|
| **C compiler** (gcc or clang) | `gcc --version` or `clang --version` | macOS: `xcode-select --install`, Linux: `apt install build-essential` |
| **C++ compiler** | `g++ --version` or `clang++ --version` | Same as above |
| **zlib** | n/a | macOS: included, Linux: `apt install zlib1g-dev` |
| **Git** | `git --version` | Pre-installed on most systems |
| **Node.js 22+ and npm** | `node --version` and `npm --version` | Required for `--with-ui` |

</details>

```bash
git clone https://github.com/DeusData/codebase-memory-mcp.git
cd codebase-memory-mcp
scripts/build.sh --with-ui          # the shipped composition (CBM Lens embedded, needs Node.js)
scripts/build.sh                    # without the UI (development only)
# Binary at: build/c/codebase-memory-mcp   (codebase-memory-mcp.exe on Windows)
```

The `--with-ui` build embeds CBM Lens and the agent integration templates into the native executable. You do not need Node.js to run that executable. Optional browser model weights are downloaded separately after opt-in.

#### Tests

Counts on `main` at `4d9592b5`, where CBM Lens arrived. Native and Go counts use the macOS configuration; platform-specific registrations differ.

| Suite | Count | Checked for this README |
|---|---:|---|
| Native C | 9,388 tests across 189 suites | 9,376 passed, 11 skipped, 1 failed on macOS (`regex_compile_refuses_overlong_pattern`) |
| UI unit tests (Vitest) | 3,970 tests in 310 files | All passed |
| Go package wrapper | 30 top-level tests | Counted in the source |
| npm package wrapper | 24 tests | Counted in the source |
| Python package wrapper | 43 tests | Counted in the source |

Run the native test suite:

```bash
scripts/test.sh                     # full: clean sanitizer build + all suites + guards
scripts/test.sh --suites <name>     # one suite, incremental, seconds
build/c/test-runner --list-suites   # what is available
```

Frontend development and validation:

```bash
cd graph-ui
npm ci
npm run test:unit
npm run build
npm run dev
```

The dev server listens on port 5173 and proxies `/api` and `/rpc` to port 9749. Start the matching CBM backend first.

Package wrappers have their own test commands: `go test ./...` in `pkg/go`, `npm test` in `pkg/npm`, and `python3 -m unittest discover -s tests -p 'test_*.py'` in `pkg/pypi`. The Go wrapper requires Go 1.26.1 or newer.

`scripts/test.sh` is the C test entry point used by CI. The canonical local artifact-flow check builds both stripped/unstripped candidates, defaults to the stripped candidate for this explicitly unscanned local run, packages those exact bytes, extracts the archive, and smokes it:

```bash
scripts/ci/smoke-artifact.sh <linux|darwin|windows> <amd64|arm64>
```

`scripts/package-release.sh` is intentionally a lower-level immutable boundary:
it accepts only an already-final `--selected-binary` plus its
`--expected-sha256`; it never builds, strips, signs, or relinks the executable.

### Manual MCP Configuration

<details>
<summary>If you prefer not to use the install command</summary>

Add to `~/.claude.json` (user scope) or project `.mcp.json`:

```json
{
  "mcpServers": {
    "codebase-memory-mcp": {
      "command": "/path/to/codebase-memory-mcp",
      "args": []
    }
  }
}
```

Restart your agent. Verify with `/mcp`; you should see `codebase-memory-mcp` with 17 tools.

</details>

## Multi-Agent Support

`install` configures **45 supported automatic/conditional client surfaces**: 39 detected automatically and 6
conditional or explicit. “Conditional” means the installer writes only when the
documented platform or an explicit, already-existing config path proves the
target is active. It never flips experimental feature flags, enables plugins,
YOLO modes, global permission bypasses, or third-party instruction trust.

Where a client has a documented custom-agent format, the installer creates three
exact-owned definitions from one canonical contract:

- **Scout (Tier 1)**: about 3 to 4 narrow calls for fast positive, provisional discovery; no absence, exhaustive-impact, or dead-code claims.
- **Verify (Tier 2, default)**: task-directed graph evidence, exact source checks, path coverage for every cited file, and scope coverage before negative claims.
- **Auditor (Tier 3)**: bounded scope, current index generation, complete relevant pagination, broader relationship checks, and explicit unresolved limitations.

Every direct tier batches `check_index_coverage` for its evidence paths and reads
flagged ranges or skipped/excluded files directly. A clean coverage result means
only “no recorded gap,” never proof of completeness. Clients without safe child
MCP access receive the same three tiers as parent-handoff agents; the parent must
supply project, generation, pagination state, graph evidence, and coverage
results. Updates migrate only byte-identical prior Verify definitions and never
overwrite user-modified agents.

| Agent | Activation | MCP config | Durable context / augmentation |
|-------|------------|------------|--------------------------------|
| Claude Code | Detected | `~/.claude.json` | Skill + three exact-tool graph agents; `SessionStart`, `SubagentStart`, non-blocking `PreToolUse` for `Grep`/`Glob`/`Bash`, and post-`Read` coverage |
| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Managed `AGENTS.md` activation pointer, skill, three read-only agents; `SessionStart` + `SubagentStart` |
| Gemini CLI | Detected | `.gemini/settings.json` | `GEMINI.md`, three explicit read/graph-tool subagents; `BeforeTool`, `AfterTool` `read_file` coverage, and `SessionStart` |
| Zed | Detected | platform `settings.json` (JSONC) | `AGENTS.md` + shared skill |
| OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents; plugin adds grep/glob graph lookup, post-`read` coverage, first-tool-result session context, and post-compaction reinjection |
| Antigravity | Detected | `.gemini/config/mcp_config.json` | `.gemini/GEMINI.md` |
| Aider | Detected | none | `CONVENTIONS.md` via `.aider.conf.yml` |
| KiloCode | Detected | `.config/kilo/kilo.jsonc` | Rule + three graph-tool subagents with deny-by-default permissions |
| VS Code | Detected | platform `Code/User/mcp.json` | `~/.copilot/skills`, three read-only agents, `sessionStart` + `subagentStart` |
| Cursor | Detected | `.cursor/mcp.json` | Skill + three read-only parent-handoff agents; context hooks withheld because session injection races and `readonly` blocks MCP |
| Windsurf | Detected | `~/.codeium/windsurf/mcp_config.json` | Always-on `global_rules.md` |
| Augment / Auggie | Detected | `~/.augment/settings.json` | Rule, three read-only handoff subagents, `SessionStart` + post-`view` coverage |
| OpenClaw | Detected | `$OPENCLAW_CONFIG_PATH` or state `openclaw.json` | Active-workspace `AGENTS.md` + `TOOLS.md`; compaction reinjection |
| Kiro | Detected | `$KIRO_HOME/settings/mcp.json` | Steering, skill, three JSON agents with isolated Scout/Analysis-profile MCP and explicit graph-tool selectors (`includeMcpJson: false`) |
| Junie | Detected | `.junie/mcp/mcp.json` | Skill + three graph subagents for EAP-capable builds; Scout and Analysis server aliases hard-limit the tier tool surfaces; no ineffective EAP `SessionStart` hook |
| Hermes | Detected | `$HERMES_HOME/config.yaml` | Skill + fail-open `pre_llm_call` context augmentation |
| OpenHands | Detected | `.openhands/mcp.json` | Shared `.agents/skills/codebase-memory/SKILL.md` |
| Cline | Detected | `~/.cline/mcp.json` + `${CLINE_DATA_DIR:-~/.cline/data}/settings/cline_mcp_settings.json` | Rule + skill; automatic file hooks withheld because they auto-activate and their output is not reliably consumed; child agents cannot use MCP |
| Warp | Detected, skill only | UI, Warp Drive, or per invocation (manual) | Shared `~/.agents/skills/codebase-memory/SKILL.md` |
| Qwen Code | Detected | `.qwen/settings.json` | `QWEN.md`, skill, three explicit read/graph-tool agents; `SessionStart`, `SubagentStart`, and post-`ReadFile` coverage |
| GitHub Copilot CLI | Detected | `$COPILOT_HOME/mcp-config.json` | Instructions, skill, three read-only agents; `sessionStart` + `subagentStart` |
| Factory Droid | Detected | `.factory/mcp.json` | `AGENTS.md`, skill, three droids with exact per-tier graph-tool lists (without additive whole-server exposure); `SessionStart` + post-`Read` coverage on macOS/Linux, withheld on Windows |
| Crush | Detected | `.config/crush/crush.json` | Managed context path with explicit parent-to-child handoff |
| Goose | Detected | `.config/goose/config.yaml` | `.goosehints` |
| Mistral Vibe | Detected | `$VIBE_HOME/config.toml` | `AGENTS.md`, skill, and three matched agent/prompt pairs with explicit read-only graph-tool allowlists |
| Grok Build | Detected | `$GROK_HOME/config.toml` | Owned `rules/codebase-memory.md`, skill, three graph agents with named-server `mcpInheritance` and exact `server__tool` dispatcher ids; context hooks withheld because its passive hook events discard stdout |
| Qoder CLI | Detected | `~/.qoder/settings.json` | Skill, three directly MCP-attached agents with named-server scoping and exact per-tier graph-tool lists; `SessionStart`, `SubagentStart`, and post-`Read` coverage, including documented PowerShell execution on Windows |
| Kimi Code CLI | Detected | `$KIMI_CODE_HOME/mcp.json` (default `~/.kimi-code`) | Same-root `AGENTS.md` + skill; fail-open `UserPromptSubmit` hook in `config.toml` |
| GitLab Duo CLI | Detected | `$GLAB_CONFIG_DIR/duo/mcp.json` or platform fallback | Fail-open user `SessionStart` on macOS/Linux; hook withheld on Windows; no experimental global skill enablement |
| Rovo Dev CLI | Detected | configured override or `~/.rovodev/mcp.json` | Global `AGENTS.md`, skill + three read-only handoff subagents; no undocumented hook |
| Amp | Detected | `~/.config/agents/skills/codebase-memory/mcp.json` | Colocated skill + `~/.config/amp/AGENTS.md`; no plugin |
| Devin CLI / Local | Detected | `~/.config/devin/config.json` (platform app-data path on Windows) | Same-root `AGENTS.md` + skill; macOS/Linux `UserPromptSubmit` + `PostCompaction`, and `SessionStart` only when Claude does not already provide it; hooks withheld on Windows |
| Tabnine | Detected | `~/.tabnine/mcp_servers.json` | MCP only; no experimental/YOLO setting |
| Continue / cn | Conditional | Existing `~/.continue/config.yaml` or `$CBM_CONTINUE_CONFIG_PATH` | MCP only |
| Visual Studio | Conditional, Windows | `~/.mcp.json` | MCP only |
| TRAE | Conditional | Existing `$CBM_TRAE_CONFIG_PATH` | MCP only |
| Roo Code | Conditional | Existing `$CBM_ROO_CONFIG_PATH` | MCP only |
| Amazon Q Developer IDE | Detected | `~/.aws/amazonq/default.json` (preserves an existing `agents/default.json` or legacy `mcp.json`) | MCP only |
| CodeBuddy Code CLI | Detected | `~/.codebuddy/.mcp.json` (preserves an active deprecated/legacy file) | `CODEBUDDY.md`, skill, three read-only graph agents; beta hooks are not auto-installed |
| IBM Bob Shell | Detected by `bob` | `~/.bob/mcp_settings.json` | Shared rule; no invented hook or agent |
| Pochi | Detected | `~/.pochi/config.jsonc` (`mcp`) | `README.pochi.md`, skill, and three `readFile`-only parent-handoff agents |
| Pi | Detected | none | `~/.pi/agent/AGENTS.md` + skill; MCP/subagents require an explicit reviewed extension |
| IBM Bob IDE | Conditional | Existing `~/.bob/mcp.json` | Shared rule + IDE skill; no invented hook or agent |
| Oh My Pi (omp) | Detected | Effective agent directory (`OMP_PROFILE` / `PI_CODING_AGENT_DIR`; default `~/.omp/agent/mcp.json`) | Skill and three direct-MCP graph-tool subagents (Scout/Verify/Auditor); preserves user `AGENTS.md` |
| Sourcegraph Cody | Explicit opt-in | Existing `$CBM_CODY_CONFIG_PATH` | MCP only |

For Codex, install keeps only a tiny managed activation pointer in global
`$CODEX_HOME/AGENTS.md`; all detailed behavior lives in the installed `codebase-memory` skill.
Fresh installs create the pointer, upgrades replace the legacy full managed block while preserving
all user-owned bytes, and uninstall removes only the managed pointer.

### Sessions, compaction, and subagents

Hooks installed by this project are fail-open and context-only. Claude Code's
`PreToolUse` observes `Grep`/`Glob`/`Bash` and injects matching graph symbols as
`additionalContext`; `PostToolUse` on `Read` adds targeted coverage context when
the graph could not fully parse or index that file. It never denies or replaces
the requested tool call.

Claude Code, Codex CLI, Qwen Code, GitHub Copilot CLI, and VS Code's Copilot
runtime receive paired session/subagent context where the vendor exposes a
documented context-output contract. Codex users must review and trust installed
hooks through `/hooks`; changing a hook definition changes its trust hash, so an
update can require re-trust. Qoder uses `SessionStart`, `SubagentStart`, and
post-`Read` coverage, including its documented PowerShell executor on Windows.
Kimi uses `UserPromptSubmit`, while Hermes uses `pre_llm_call`; both retain their
documented Windows execution paths. Devin installs
`UserPromptSubmit` and `PostCompaction` on macOS/Linux and adds `SessionStart`
only when Claude's equivalent managed hook is not present. GitLab Duo gets a
narrowly scoped macOS/Linux user `SessionStart` entry on its experimental hook
surface. GitLab Duo, Devin, and Factory hooks are withheld on Windows
because those vendors do not document a deterministic shell/executor contract
there. Gemini CLI, Factory Droid, and Augment also add documented post-read/view
coverage context but expose no equivalent documented child-start context.

For runtimes without a stable context-producing lifecycle event, durable files
carry the contract across fresh sessions and compaction: verify the graph project
and index freshness, query structural facts in the parent, then pass the project,
qualified symbols, paths, and call-chain evidence in every delegated task.
Claude, Codex, Gemini, Kiro, Qwen, Copilot, CodeBuddy, OpenCode, Kilo, Vibe,
Qoder, Junie, Factory, and Grok Build receive Scout, Verify, and Auditor graph profiles.
Kiro embeds this MCP server with `--tool-profile scout` for Scout and
`--tool-profile analysis` for Verify/Auditor. Junie registers equivalent named
server aliases because its subagent schema filters by server rather than by
individual tool. Both process profiles use positive allowlists: Scout exposes
seven fast inspection tools, Analysis exposes eleven, and future or mutating
tools remain unavailable until explicitly reviewed. If either Junie alias
collides with user configuration, the installer preserves it and installs
parent-handoff profiles instead. Qoder combines its documented named-server
selection with exact tier-specific MCP tool IDs. Factory uses exact registered
MCP tool IDs without its additive `mcpServers` field, which would expose the
whole server. Codex, Kilo, Vibe, and other capable formats likewise enumerate
the narrowest supported tool set. Rovo, Cursor, Augment, Pochi, and Cline use parent handoff where direct
child MCP is unavailable or unsafe; Pochi is limited to `readFile`, and Cline
child agents cannot use MCP.

Cline's file hooks auto-activate when present, and current Cline does not
reliably consume their context output, so automatic adapters are withheld and
older owned adapters are cleaned up. CodeBuddy's beta, version-gated hooks are
not auto-installed. Junie's EAP
`SessionStart` output is documented as ignored, so no context hook is installed.
Junie custom agents remain EAP-dependent. Qoder can resolve higher-priority
project or plugin agents before user agents with the same name; reload the
client after installation or profile changes.
Cursor context
hooks are withheld: session context injection has a known race, `subagentStart`
is control-only, and read-only subagents cannot safely receive MCP access. Grok Build's
passive hook events (`SessionStart`, `SubagentStart`, `PostToolUse`) discard
stdout and `PreToolUse` honors only deny/rewrite decisions, so its context hooks
are withheld; Grok also reads Claude and Cursor MCP, skill, and hook files
through its compat layer, and the native `config.toml` entry shadows that copy
by name. Rovo
has no documented session context-output hook, and Bob
documents neither a suitable hook nor a custom-agent surface. Those surfaces are
not approximated with invented augmentation. Kimi plugins, Amp plugins, and
GitLab experimental global skills remain opt-in.

OpenClaw reinjects the `Codebase Knowledge Graph (codebase-memory-mcp)` AGENTS
section after compaction and places the same guidance in `TOOLS.md`, the bootstrap
files inherited by its subagents. Automatic augmentation covers the active/default
workspace. Separate `agents.list[].workspace` directories require making that
workspace active for installation or copying the managed block there.

The installed Claude shim is named `cbm-code-discovery-gate` for backward
compatibility; despite the legacy name, it never gates or blocks.

### Manual or UI-managed integrations

These are intentionally not counted as automatic installs: Qodo MCP is added
through its UI and may be governed by enterprise allowlists; Warp MCP is managed
through Warp Drive/UI or per invocation (only the shared skill is automatic);
JetBrains AI Assistant / ACP is IDE-managed; GitHub Copilot coding agent, Jules,
and CodeRabbit are cloud/repository-managed; Replit exposes a remote/service
integration rather than a stable local user-global client; BLACKBOX AI does not
document a stable arbitrary user-global MCP/instruction/agent schema; Plandex has
no stable global registry safe to mutate; and SWE-agent uses explicit YAML and is
no longer a suitable automatic global target.

## CLI Mode

Every MCP tool can be invoked as a local, one-shot command. CLI tools connect to the shared coordination daemon (starting one if none is already running) to get a crash-safe exact-build admission lease for the command's lifetime; they never register with the background watcher, and a daemon a command had to start itself exits again once the command's connection closes and no other session is using it. `index_repository` is the only exception internally: it starts a temporary, exact-build supervised worker for the index, then stops that worker before the CLI command exits; the worker holds its own lease until exit.

Commands that mutate graph data use shared OS-backed, per-project locks. This serializes conflicting work from CLI and MCP sessions on the same project while allowing unrelated projects to proceed independently.

When stderr is an interactive terminal, the CLI automatically shows lifecycle and indexing progress. Pass `--progress` to force the same feedback when stderr is redirected or the command is run non-interactively. Pass `--quiet` to disable automatic terminal progress and ordinary diagnostics while retaining errors; it cannot be combined with `--progress` or outer `cli --verbose`. Routine informational logs are quiet by default; pass outer `cli --verbose` to include them. Progress and logs use stderr while stdout remains reserved for the command result. Read tools return a compact tree by default; pass a tool's `--format json` for machine-readable payload JSON, or outer `--json` for the full MCP envelope.

Large compact-tree tables may start with a response-local `<section>_refs` directory and an explicit `<section>_ref_rule`. A cell such as `@0+handler.go` reconstructs to ref `0`'s prefix plus `handler.go`. References are local to that sibling `<section>` table and expansion is non-recursive: entries inside `<section>_refs` are always literal prefixes. This is limited to declared path and qualified-name columns and activates only when the exact rendered table is at least 15% and 64 bytes smaller and a conservative model-neutral token-shape proxy also improves by at least 1%. Search and trace likewise render direct and prefix-grouped tree shapes and keep the smaller complete representation, so singleton or scattered answers do not pay directory overhead. Keys are declared once per table but never cryptically abbreviated, and `--format json` keeps stable literal strings for machine consumers. Both gates are deterministic; exact token counts still depend on the caller's tokenizer.

Lean responses truncate semantically, never by cutting arbitrary bytes from code or identifiers. Ranked graph rows are retained ahead of raw grep rows and diagnostic summaries; omitted rows/sections report totals, `has_more`, and a strictly advancing continuation offset or cursor. If even the first whole row cannot fit, CBM asks for a higher budget and emits no self-looping cursor. `max_output_tokens` is model-neutral sizing guidance: CBM enforces a deterministic ceiling of four UTF-8 bytes per requested token, so it is not a tokenizer-exact count. Detail flags such as `diagnostics`, `source_mode`, and `detail` opt into heavier fields. `search_code` pages ranked rows with `result_limit`/`result_offset` (`limit` remains a compatibility alias), raw rows with `raw_limit`/`raw_offset`, and directory summaries with `directory_limit`/`directory_offset`. Raw lines default to a UTF-8-safe match-centered preview; each row reports `content_start_byte`, returned/total byte counts, match byte bounds when known, and a content continuation offset. Pass `raw_content_offset` to page the original line without moving the raw-row cursor. `match_limit` and `source_max_lines` bound per-result details, with exact omission metadata. `detect_changes` pages changed files, impacted symbols, and module summaries independently; prefer its snapshot-bound `*_cursor` continuations, which reject changed commits, worktree bytes, graph generation, or semantic arguments instead of silently skipping or duplicating rows.

Every response is standard UTF-8. Identifiers, paths, and raw search previews preserve POSIX byte-string identities: a preserved value containing malformed UTF-8 is emitted reversibly as `@bytes:<lowercase hex of every original byte>`. A valid preserved value that literally begins with the reserved `@bytes:` or `@utf8:` prefix is emitted as `@utf8:<original value>`, so decoding is unambiguous: strip one `@utf8:` prefix for literal UTF-8, or hex-decode one `@bytes:` prefix for original bytes. Ordinary valid UTF-8 is unchanged and pays no output-token overhead. To keep code readable, source bodies replace malformed UTF-8 with U+FFFD; use the pageable raw search preview when byte-exact source inspection is required.

Use `cli <tool> --help` to see the flags generated from that tool's input schema:

```bash
codebase-memory-mcp cli index_repository --repo-path /path/to/repo
codebase-memory-mcp cli list_projects

# Use the "name" returned by list_projects as the project value.
codebase-memory-mcp cli search_graph --project my-project --name-pattern '.*Handler.*' --label Function
codebase-memory-mcp cli trace_path --project my-project --function-name Search --direction both
codebase-memory-mcp cli query_graph --project my-project --query 'MATCH (f:Function) RETURN f.name LIMIT 5'

# Force human-readable progress without contaminating stdout.
codebase-memory-mcp cli --progress index_repository --repo-path /path/to/repo
# Suppress automatic terminal progress and non-error diagnostics.
codebase-memory-mcp cli --quiet list_projects --format json
codebase-memory-mcp cli search_graph --project my-project --label Function --format json
codebase-memory-mcp cli list_projects --format json --detail stats | jq '.projects[].name'
```

Tools requiring arguments accept JSON piped on stdin. Tools with only optional arguments, such as `list_projects`, accept flags, `--args-file`, or redirected JSON files without waiting on an inherited pipe. Inline JSON remains accepted for backward compatibility but is deprecated in favor of flags, `--args-file`, or stdin.

## MCP Tools

### Indexing

| Tool | Description |
|------|-------------|
| `index_repository` | Index a repository into the graph. Auto-sync keeps it fresh only while it is the project of an open MCP session; repos indexed from the CLI or from another project's session need a re-run (`index_status` shows `watch`). Waits for the whole index by default; pass `async: true` to start it in the daemon and return at once, then poll with `status: true` (see below). |
| `list_projects` | List indexed projects with paging; use `detail: "stats"` for node/edge counts and database sizes. |
| `delete_project` | Remove a project and all its graph data. |
| `index_status` | Check indexing status of a project. |
| `check_index_coverage` | Check whether exact paths or a scope are indexed and fresh. A clean result means no recorded gap, not proof of completeness. |

**Long indexes and client call deadlines.** A synchronous `index_repository` on a large
repository can take longer than an MCP client allows one tool call (some IDE clients give up
after a fixed deadline). When a client cancels or disconnects, the daemon cancels an index that
nobody else is waiting for, so retrying the same blocking call never finishes. Use the async
mode instead:

1. `index_repository(repo_path="/abs/path", async: true)` starts the index in the daemon (or
   joins the one already running for that project) and returns immediately with
   `state` (`queued`/`running`). The job keeps running even if the client cancels, times out or
   disconnects; only stopping the daemon ends it.
2. `index_repository(repo_path="/abs/path", status: true)` reports `state`
   (`queued`, `running`, `cancelling`, `succeeded`, `failed`, `cancelled`), `started_at`,
   `finished_at` and an `error` summary. Poll it until the state is `succeeded`, `failed` or
   `cancelled`. Pass the same `repo_path` (and `name`, if the index call used one).

`async` and `status` are exclusive; `async` does not apply to `cross-repo-intelligence`. Both
need the daemon-backed server (the default `codebase-memory-mcp` entry point). A temporary
daemon (started on demand rather than by `codebase-memory-mcp daemon start`) stops, and
cancels its jobs, when its last client disconnects. A connected MCP session keeps it alive, so
async from your editor works. A one-shot `codebase-memory-mcp cli index_repository --async`
that is the daemon's only client is refused with a clear error, because the job would die the
moment the command exits: run `codebase-memory-mcp daemon start` first, keep an MCP session
open, or call without `--async`. `status` works everywhere. When a synchronous call was cut
short, the next
`index_repository` or `status` call for that project carries a `notice` suggesting the async
mode. `index_status` keeps describing the published graph and its freshness.

### Querying

| Tool | Description |
|------|-------------|
| `search_graph` | Structural, BM25, and semantic search. Page structural rows with `offset`/`limit` and ranked semantic rows independently with `semantic_offset`/`semantic_limit`. |
| `trace_path` | Trace callers, callees, data flow, or cross-service paths (alias: `trace_call_path`). Depth 1 to 15; tests are excluded by default. `mode`: `calls` (default), `data_flow`, `cross_service`, `topology` (Kubernetes objects, routes, controllers and what they deploy; start at a Resource or Route), `deploy` (what a CI job, script or Make rule runs and deploys; outbound from the job, inbound from a chart), `run` (what a Service or workload runs: workload → `Image` → Dockerfile → the program its container starts, with the environment each container is given; outbound from a Service, workload or compose service). |
| `detect_changes` | Map git diff to affected symbols + blast radius with risk classification. |
| `query_graph` | Execute Cypher-like graph queries (read-only). |
| `get_graph_schema` | Node/edge counts, relationship patterns, property definitions per label. Run this first. |
| `compare_graphs` | Compare two indexed snapshots: node/edge additions and removals between a base and a target. |
| `get_code_snippet` | Read source code for a function by qualified name. |
| `get_file_outline` | Declaration outline of one repository-relative file in source order, with optional label filter and paging. |
| `get_architecture` | Codebase overview: languages, packages, routes, hotspots, clusters, ADR. |
| `search_code` | Grep-like text search within indexed project files. |
| `manage_adr` | CRUD for Architecture Decision Records (`get` reads, `update` replaces the whole document, `set_sections` rewrites only the named sections and leaves every other byte untouched, `sections` lists headings). Query modes do not wait behind a same-project reindex; writes remain serialized. |
| `ingest_traces` | Store observed call paths or caller/callee pairs under a run label. Both endpoints must resolve to indexed symbols; unresolved names are reported. |

`manage_adr(mode='set_sections')` writes one or more sections by name and splices them into the stored document, so text outside the named sections (including a preamble, code fences and section ordering) is preserved byte-for-byte. Any `## Heading` works, not just the conventional PURPOSE / STACK / ARCHITECTURE / PATTERNS / TRADEOFFS / PHILOSOPHY set; names match exactly, including case. Writing the same section twice is a no-op, so a retry after a lost response cannot duplicate content.

`manage_adr` query modes (`get` and `sections`) use the server's cached query store so they can proceed while a same-project reindex is running. If another process publishes a replacement store during reindexing, they can return the pre-publication ADR until idle eviction refreshes that cache. Updates remain serialized through the project mutation guard.

## Graph Data Model

### Node Labels

`Project`, `Package`, `Folder`, `File`, `Module`, `Section`, `Class`, `Struct`, `Interface`, `Trait`, `Enum`, `Type`, `Function`, `Method`, `Field`, `Variable`, `Constant`, `Macro`, `Decorator`, `Route`, `Channel`, `Table`, `Process`, `EnvVar`, `Resource`, `Chart`, `Image`, `Branch`

### Edge Types

`CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `CALL_REFERENCE`, `USAGE`, `INHERITS`, `IMPLEMENTS`, `OVERRIDE`, `DECORATES`, `RAISES`, `THROWS`, `WRITES`, `DATA_FLOWS`, `HTTP_CALLS`, `GRPC_CALLS`, `ASYNC_CALLS`, `EMITS`, `LISTENS_ON`, `HANDLES`, `SPAWNS`, `CONFIGURES`, `INFRA_MAPS`, `CONNECTS_TO`, `DEPLOYS`, `DEPENDS_ON`, `REFERENCES_FILE`, `MENTIONS`, `TESTS`, `TESTS_FILE`, `SIMILAR_TO`, `SEMANTICALLY_RELATED`, `FILE_CHANGES_WITH`, `HAS_BRANCH`, and the cross-repo `CROSS_HTTP_CALLS`, `CROSS_GRPC_CALLS`, `CROSS_GRAPHQL_CALLS`, `CROSS_TRPC_CALLS`, `CROSS_ASYNC_CALLS`, `CROSS_CHANNEL`

`MENTIONS` links a documented definition to the code its doc comment references (C# `<see cref>`, `<seealso cref>`, `<exception cref>`, `<inheritdoc cref>`), one edge per pair with `via`, `syntax`, `tier` (`exact` or `unique`), first `line` and `count`. A reference is bound only when it names exactly one definition under the language's own lookup rules; the rest are counted by reason in `index_status` under `doc_links` (samples with `diagnostics='full'`).

### Qualified Names

`get_code_snippet` uses qualified names: `<project>.<path_parts>.<name>`. Use `search_graph` to discover them first.

### Supported Cypher (openCypher read subset)

`query_graph` is a read-only openCypher subset:

- **Clauses**: `MATCH`, `OPTIONAL MATCH`, multiple `MATCH`, `WHERE`, `WITH` (+ `WITH … WHERE`), `RETURN`, `ORDER BY`, `SKIP`, `LIMIT`, `DISTINCT`, `UNWIND`, `UNION` / `UNION ALL`, `CASE`.
- **Patterns**: labelled nodes, label alternation `(n:A|B)`, relationship types/direction, variable-length paths `[*1..3]`, inline property maps.
- **WHERE**: `= <> < <= > >=`, `AND/OR/XOR/NOT`, `IN`, `CONTAINS`, `STARTS WITH`, `ENDS WITH`, `IS [NOT] NULL`, regex `=~`, label test `n:Label`, and `EXISTS { (n)-[:TYPE]->() }` (single-hop existence; great for dead-code, e.g. `WHERE NOT EXISTS { (f)<-[:CALLS]-() }`).
- **Aggregates**: `count` (+`DISTINCT`), `sum`, `avg`, `min`, `max`, `collect`.
- **Functions**: `labels`, `type`, `id`, `keys`, `properties`; `toLower/toUpper/toString/toInteger/toFloat/toBoolean`; `size`, `length`, `trim/ltrim/rtrim`, `reverse`; `coalesce`, `substring`, `replace`, `left`, `right`.

Anything outside this subset (write/`MERGE`/`CALL` clauses, unsupported functions, list/map literals, comprehensions, path functions, parameters) **fails with a clear `unsupported …` error** rather than returning empty results.

## Ignoring Files

Layered: hardcoded patterns (`.git`, `node_modules`, etc.) → `.gitignore` hierarchy → `.cbmignore` (project-specific, gitignore syntax). Symlinks are always skipped.

See [docs/cbmignore.md](docs/cbmignore.md) for the full `.cbmignore` how-to: syntax, precedence across the ignore layers, and negation semantics.

## Configuration

```bash
codebase-memory-mcp config list                          # show all settings
codebase-memory-mcp config set auto_index true           # auto-index on session start
codebase-memory-mcp config set auto_index_limit 50000    # max files for auto-index
codebase-memory-mcp config set auto_watch false          # don't register background git watcher (default: true)
codebase-memory-mcp config set doc_candidates false      # skip doc<->code candidate scoring on huge repos (default: auto = on for full, off for moderate/fast)
codebase-memory-mcp config set watcher_enabled false     # stop the watcher thread entirely (default: true)
codebase-memory-mcp config set watch_non_git true        # also poll non-git project roots (default: false)
codebase-memory-mcp config set index_max_files 250000    # optional per-index source-file limit
codebase-memory-mcp config set index_max_source_mb 16384 # optional per-index source-size limit
codebase-memory-mcp config reset auto_index              # reset to default
```

The two `index_max_*` settings default to `off`. Exceeding one fails the complete
index attempt rather than publishing a partial graph; an existing serving index
is preserved. See [Index resource limits](docs/INDEX_RESOURCE_LIMITS.md).

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `CBM_ALLOWED_ROOT` | *(unset)* | Confine `index_repository` to paths within this directory. When set, a `repo_path` that resolves (after symlink / `..` resolution) outside this root is refused, and the same check now applies to the graph UI's `POST /api/index` route rather than only to the MCP tool. Unset imposes no *containment* restriction; but see the always-on limits below, which apply whether or not this is set. Useful when the server may be driven by an untrusted caller, e.g. agentic or multi-tenant deployments. |
| `CBM_CACHE_DIR` | `~/.cache/codebase-memory-mcp` | Override the database storage directory. All project indexes and config are stored here. One account can use only one canonical cache root at a time; close active CBM sessions/commands before switching it. |
| `CBM_DIAGNOSTICS` | `false` | Set to `1` or `true` to enable the shared daemon's periodic `snapshot.json` and retained `trajectory.ndjson` below a fresh owner-private directory in the system temp directory. Exact paths are logged by `diagnostics.start`. |
| `CBM_DOWNLOAD_URL` | *(GitHub releases)* | Override the download URL for updates. Used for testing or self-hosted deployments. |
| `CBM_LOG_LEVEL` | role-aware | Set the minimum log level. Thin MCP/CLI/hook frontends default to `warn`; the detached daemon and its supervised index workers default to `info` so lifecycle and liveness records remain available. Accepted values (case-insensitive): `debug`, `info`, `warn`, `error`, `none`; or their numeric equivalents `0` to `4`. A physical worker retains INFO liveness records even under a stricter override because its private log drives the supervisor's no-progress timeout. Frontend messages go to that session's stderr; detached daemon events go to `${CBM_CACHE_DIR}/logs/cbm-daemon.log`. Stdout is reserved for MCP JSON-RPC. |
| `CBM_WORKERS` | *(detected)* | Override the parallel-indexing worker count returned by `cbm_default_worker_count`. Useful inside containers where `sysconf(_SC_NPROCESSORS_ONLN)` reports host CPUs rather than the cgroup's effective quota. Range 1 to 256; invalid values are ignored with a warning. |
| `CBM_MEM_BUDGET_MB` | *(detected)* | Override the in-memory graph budget with an explicit cap in MiB, taking precedence over the derived default (`ram_fraction × total_RAM`, adjusted for available memory and daemon worker caps). Useful on bare-metal hosts without a cgroup limit, or to pin a budget *below* the cgroup limit so headroom is left for sibling processes. Must be a positive integer; it is clamped to detected total RAM (logged as `mem.budget.clamped`), and non-numeric or non-positive values are ignored with a warning (`mem.budget.env.invalid`). |
| `CBM_DUMP_VERIFY_MIN_RATIO` | `0.5` | After indexing, compare persisted SQLite node count to the in-memory dump count. When persisted nodes fall below this fraction of committed nodes (and committed > 50), `index_repository` returns `status:"degraded"` instead of silent `indexed`. Range 0 to 1; set `0` to disable. Invalid values are ignored with a warning. |

Environment used by daemon-owned components (such as diagnostics, daemon logging, and process-wide indexing resource limits) is captured from the first daemon-backed session that starts the daemon. Later sessions join that process and cannot replace those values. To change them, close all daemon-backed sessions, update the relevant agent configurations consistently, and restart a session. `CBM_ALLOWED_ROOT` remains session-specific, a conflicting `CBM_CACHE_DIR` is rejected, and a one-shot CLI command is not exempt from the rule above: it connects to the coordination daemon like any other session, starting one if none is running. Its own environment becomes the captured daemon-owned environment only when its invocation is the one that starts the daemon; joining an already-running daemon, it inherits that daemon's already-captured values instead.

```bash
# Store indexes in a custom directory
export CBM_CACHE_DIR=~/my-projects/cbm-data
```

## Custom File Extensions

The JSON config files support a single key, `extra_extensions`, which maps additional file extensions to supported languages. Useful for framework-specific extensions like `.blade.php` (Laravel) or `.mjs` (ES modules). (For other tunables, see [Environment Variables](#environment-variables) and the `config` subcommand above.)

Need the full config-file reference? See [docs/CONFIGURATION.md](docs/CONFIGURATION.md).

**Per-project** (in your repo root):
```json
// .codebase-memory.json
{"extra_extensions": {".blade.php": "php", ".mjs": "javascript"}}
```

**Global** (applies to all projects):
```json
// ~/.config/codebase-memory-mcp/config.json  (or $XDG_CONFIG_HOME/...)
{"extra_extensions": {".twig": "html", ".phtml": "php"}}
```

Each entry maps an extension (which **must** start with `.`) to a language name. Language names are matched **case-insensitively**. Accepted values (aliases in parentheses) are:

`arkts`, `asciidoc`, `bash` (`sh`), `c`, `c++` (`cpp`), `c#` (`csharp`), `chialisp`, `clojure`, `cmake`, `cobol`, `common lisp` (`commonlisp`, `lisp`), `css`, `cuda`, `dart`, `diagram`, `dockerfile`, `elixir`, `elm`, `emacs lisp` (`emacslisp`), `erlang`, `f#` (`fsharp`), `form`, `fortran`, `glsl`, `go`, `graphql`, `groovy`, `haskell`, `hcl` (`terraform`), `html`, `ini`, `java`, `javascript`, `jinja2` (`jinja`), `json`, `julia`, `kotlin`, `lean`, `lua`, `magma`, `makefile`, `markdown`, `matlab`, `meson`, `mojo`, `nix`, `objective-c` (`objc`), `ocaml`, `openapi`, `pdf`, `perl`, `php`, `plsql`, `protobuf`, `python`, `r`, `ruby`, `rust`, `scala`, `scss`, `sql`, `svelte`, `swift`, `toml`, `tsx`, `typescript`, `verilog`, `vimscript`, `vue`, `wolfram`, `xml`, `yaml`, `zig`.

Project config overrides global for conflicting extensions. An entry whose language name is unknown, or whose extension does not start with `.`, is skipped and logged as a warning. Missing config files are ignored.

## Persistence

SQLite databases stored at `~/.cache/codebase-memory-mcp/`. Persists across restarts (WAL mode, ACID-safe). To reset: `rm -rf ~/.cache/codebase-memory-mcp/`.

## Troubleshooting

| Problem | Fix |
|---------|-----|
| `/mcp` doesn't show the server | Check `.mcp.json` path is absolute. Restart agent. Test: `echo '{}' \| /path/to/binary` should output JSON. |
| `index_repository` fails | Pass absolute path: `index_repository(repo_path="/absolute/path")` |
| `index_repository` times out in the client | Start it with `async: true`, then poll with `status: true` (see [Indexing](#indexing)). |
| `trace_path` returns 0 results | Use `search_graph(name_pattern=".*PartialName.*")` first to find the exact name. |
| Queries return wrong project results | Add `project="name"` parameter. Use `list_projects` to see names. |
| Binary not found after install | Add to PATH: `export PATH="$HOME/.local/bin:$PATH"` |
| UI not loading | Ensure you ran `--ui=true`. Check `http://localhost:9749`. |
| CBM Lens shows "server unreachable" or "Architecture is not available yet." | Check that `?project=` names an indexed project exactly; pick it from the project menu, or see `list_projects`. Then look at **System**, tab **Logs**, or `${CBM_CACHE_DIR}/logs/ui.log`. |

## Hybrid LSP

**Semantic type resolution beyond tree-sitter.**

Tree-sitter alone gives a syntactic AST. That handles naming, structure, and call sites well, but it can't tell you that `user.profile.display_name()` resolves to `Profile.display_name` declared three modules away; tree-sitter doesn't track imports, generics, inheritance, or stdlib types.

codebase-memory-mcp ships a **lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers** (tsserver / typescript-go, pyright, gopls, Roslyn, Eclipse JDT, rust-analyzer), embedded directly into the native executable. No language server process, no per-project setup, no API key. We call this layer **Hybrid LSP**: it runs alongside tree-sitter on every parse and refines invocation resolution (`CALLS` / `RESOLVED_CALLS`) and callable-value resolution (`CALL_REFERENCE`, with ambiguous values retained as `USAGE`) using type information, so the resulting graph mirrors what an IDE "Go to Definition" would resolve.

**Languages with full Hybrid LSP:**

| Language | What it handles |
|----------|-----------------|
| **Python** *(new in v0.7.0)* | imports + dotted submodule walks, dataclasses, `Self` return types, generics, `@property`, `match/case` class patterns, SQLAlchemy 2.0 `Mapped[T]`, Pydantic `BaseModel`, `typing.Annotated` / `ClassVar` / `Final` / `InitVar`, async/await, classmethod/staticmethod, narrowing (`isinstance` / `is not None` / walrus), `typing.cast` / `assert_type`, common stdlib (logging, pathlib, json, functools). Target ~95% resolution on idiomatic code. |
| **TypeScript / JavaScript / JSX / TSX** | generics, JSX component dispatch, JSDoc inference for plain JS, `.d.ts` declarations, module re-exports, method chaining via return-type propagation, per-file overlay chained to a shared cross-file registry |
| **PHP** *(new in v0.7.0)* | namespaces, traits, late-static-binding, PHPDoc inference, parameter binding, return-type inference |
| **C#** *(new in v0.7.0)* | global usings, file-scoped namespaces, records (incl. C# 12 primary constructors), LINQ method syntax, `async Task<T>` / `ValueTask<T>` unwrap, generic methods, `this` / `base` dispatch, `var` inference, common BCL stdlib |
| **Go** *(sharpened in v0.7.0)* | pre-built per-package cross-file registry, generics, embedded structs, interface satisfaction, package-aware import resolution |
| **C / C++** *(sharpened in v0.7.0)* | pre-built per-language cross-file registry shared across C and C++; C side handles macros + `typedef` chains + header-vs-source linking; C++ side handles templates, namespaces, `auto` inference, and method resolution via class hierarchy |
| **Java** *(new in v0.8.0)* | imports (single-type, on-demand, static), class hierarchies with `this` / `super` dispatch, generics, annotations, overload matching by arity and parameter types, lambdas / method references bound to functional interfaces, field-type inference, common JDK stdlib |
| **Kotlin** *(new in v0.8.0)* | imports + same-package resolution, classes / objects / companion objects, extension functions, data classes, nullable-type unwrapping, scope functions (`let` / `apply` / `run` / `also` / `with`), infix calls, common stdlib |
| **Rust** *(new in v0.8.0)* | `use` declarations + module paths, `impl` blocks and trait methods, struct fields, generics with trait bounds, operator-trait desugaring, derive-macro method synthesis, UFCS static paths, common std prelude |
| **Perl** | packages + `@ISA` / `use parent` / `use base` inheritance with method-resolution-order dispatch, `SUPER::` calls, Exporter (`use Foo qw(...)`) import maps, `bless` / `ref($class)\|\|$class` self-type inference, qualified `Pkg::sub` static calls, curated perlfunc + CPAN OOP stdlib; unresolved receivers emit no edge (zero-edge guarantee) |

**Two-layer architecture:**

1. **Tree-sitter pass**: fast, syntactic, runs for every one of the 162 languages. Extracts definitions, calls, imports.
2. **Hybrid LSP pass**: type-aware, runs above the tree-sitter pass per-language. Refines call edges using the import graph plus a per-file or pre-built cross-file definition registry. Languages without a Hybrid LSP pass yet fall back to textual resolution, with coverage varying by language and construct.

The result is a knowledge graph accurate enough to drive `trace_path` across packages, inheritance hierarchies, and stdlib calls; without paying for a language server process per project.

## Language Support

162 languages, counted by the vendored grammar directories in [the parser manifest](internal/cbm/vendored/grammars/MANIFEST.md). Benchmarked against 64 real open-source repositories (78 to 49K nodes):

| Tier | Score | Languages |
|------|-------|-----------|
| **Excellent** (>= 90%) | | Lua, Kotlin, C++, Perl, Objective-C, Groovy, C, Bash, Zig, Swift, CSS, YAML, TOML, HTML, SCSS, HCL, Dockerfile |
| **Good** (75-89%) | | Python, TypeScript, TSX, Go, Rust, Java, R, Dart, JavaScript, Erlang, Elixir, Scala, Ruby, PHP, C#, SQL |
| **Functional** (< 75%) | | OCaml, Haskell |

Also supported (not yet benchmarked): Ada, Agda, Apex, ArkTS, Assembly (NASM), Astro, AWK, Beancount, BibTeX, Bicep, Bitbake, Blade, Cairo, Cap'n Proto, CFML, CFScript, Chialisp, Clojure, CMake, COBOL, Common Lisp, Crystal, CSV, CUDA, D, Devicetree, Diff, .env, Elm, Emacs Lisp, F#, Fennel, Fish, FORM, Fortran, FunC, GDScript, .gitattributes, .gitignore, Gleam, GLSL, GN, Go module, Go template, GraphQL, Hare, HLSL, Hyprlang, INI, ISPC, Janet, Jinja2, JSDoc, JSON, JSON5, Jsonnet, Julia, Just, Kconfig, KDL, Lean 4, Linker Script, Liquid, LLVM IR, Luau, Magma, Makefile, Markdown, MATLAB, Mermaid, Meson, Mojo, Move, Nickel, Nix, ObjectScript (UDL and routines), Odin, Pascal, Pine Script, Pkl, PL/SQL, PO (gettext), Pony, PowerShell, Prisma, .properties, Protobuf, Puppet, PureScript, QML, Racket, Regex, requirements.txt, ReScript, RON, reStructuredText, Scheme, Slang, Smali, Smithy, Solidity, SOQL, SOSL, Squirrel, SSH config, Starlark, Svelte, Sway, SystemVerilog, TableGen, Tcl, Teal, Templ, Thrift, TLA+, Typst, Verilog, VHDL, Vim script, Vue, WGSL, WIT, Wolfram, XML, Zsh.

## Architecture

```
src/
  main.c              Entry point (MCP stdio server + CLI + install/update/config)
  daemon/             Per-account session coordination, IPC, lifecycle, shared jobs/watchers
  mcp/                MCP server (17 tools, JSON-RPC 2.0, session detection, auto-index)
  cli/                Install/uninstall/update/config (45 client surfaces, hooks, instructions)
  store/              SQLite graph storage (nodes, edges, traversal, search, Louvain)
  pipeline/           Multi-pass indexing (structure → definitions → calls → HTTP links → config → tests)
  cypher/             Cypher query lexer, parser, planner, executor
  discover/           File discovery (.gitignore, .cbmignore, symlink handling)
  watcher/            Background auto-sync (git polling, adaptive intervals)
  traces/             Runtime trace ingestion
  ui/                 Local HTTP server + embedded CBM Lens frontend
  foundation/         Platform abstractions (threads, filesystem, logging, memory)
graph-ui/             CodeAtlasWeb: React, Monaco, and Three.js browser frontend
internal/cbm/         Vendored tree-sitter grammars (162 languages) + AST extraction engine
```

## Security

Every release is verified through a multi-layer pipeline before publication:

- **VirusTotal**: all 24 executable candidates (unstripped, debug-stripped and stripped) across the eight release products are scanned before smoke/soak (clean is preferred and only the single-Microsoft `!ml` tolerance documented in [SECURITY.md](SECURITY.md#our-release-policy) may pass; the number of engines that returned a decisive result is recorded as evidence but is VirusTotal fleet availability, not a pass condition). The selected executable is packaged without changing its SHA-256, release notes link the verdict for the exact bytes shipped, and the full per-candidate evidence is published alongside the release as TSVs for anyone auditing the selection. Every distinct object extracted from the shipped containers; `install.sh`, `install.ps1`, `LICENSE`, `THIRD_PARTY_NOTICES.md`, the MCPB `manifest.json` and the unpacked UI assets; is then scanned as well, so the full published surface is covered, not just the executables.
- **SLSA Level 3**: cryptographic build provenance generated by the trusted GitHub Actions build workflow; verify with `gh attestation verify <file> --repo DeusData/codebase-memory-mcp --signer-workflow DeusData/codebase-memory-mcp/.github/workflows/_build.yml`
- **Sigstore cosign**: keyless signatures on all artifacts; bundles included in every release
- **SHA-256 checksums**: `checksums.txt` published with every release; verified by both install scripts before extraction
- **CodeQL SAST**: blocks release pipeline if any open alerts remain
- **No language-runtime dependency chain**: the native backend needs no language-runtime environment; UI builds embed the frontend

### VirusTotal release scans

The [v0.11.0 release notes](https://github.com/DeusData/codebase-memory-mcp/releases/tag/v0.11.0#security-verification) report clean scans for all eight shipped binaries. Each result is tied to the exact executable hash. The links below show the reports for those binaries; the displayed hashes are shortened for readability.

| Binary | SHA-256 prefix | Reported verdict |
|--------|----------------|------------------|
| `linux-amd64` | `a831cdcafaed7b1f7c23...` | [clean](https://www.virustotal.com/gui/file/a831cdcafaed7b1f7c23ce996a72a035f2188d7e20e9a18f39ec4b2d4e3302a6/detection) |
| `linux-arm64` | `7812062c19030de15ae2...` | [clean](https://www.virustotal.com/gui/file/7812062c19030de15ae21ec2d30746447bb2c27bc72f3bf21924ba0e2dacd7e0/detection) |
| `linux-amd64-portable` | `ce11c141431aeadd7885...` | [clean](https://www.virustotal.com/gui/file/ce11c141431aeadd788506c3a7e6942db8fd438dec369d0707a39ec9fd8c6510/detection) |
| `linux-arm64-portable` | `403d0fab6204e7129167...` | [clean](https://www.virustotal.com/gui/file/403d0fab6204e712916701936a3229dd472bad05080c757ea5177318a80fdbfe/detection) |
| `darwin-amd64` | `69ca71b4b62fe2336778...` | [clean](https://www.virustotal.com/gui/file/69ca71b4b62fe233677851bdf54953e2a6660cf5d687b4d64226b2591b14ed39/detection) |
| `darwin-arm64` | `a67b7ccead5d2ca85205...` | [clean](https://www.virustotal.com/gui/file/a67b7ccead5d2ca852051f8619458ab96af41393257b56fb36e523a110265d48/detection) |
| `windows-amd64` | `7edcd3807ebcfd85ec19...` | [clean](https://www.virustotal.com/gui/file/7edcd3807ebcfd85ec1968985964080f2589748da2fc3c7ce9261eebab31ff04/detection) |
| `windows-arm64` | `5615aa31e3cdbe6155e7...` | [clean](https://www.virustotal.com/gui/file/5615aa31e3cdbe6155e7096c43d07e62e335fe34d28e489b4cc3ce113e81b4e6/detection) |

The release also publishes [candidate scan results](https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.11.0/virustotal-candidate-results.tsv), [binary provenance](https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.11.0/release-candidates.tsv), and [selection records](https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.11.0/release-selection.tsv). For subsequent versions, use the scan evidence linked from the [latest release](https://github.com/DeusData/codebase-memory-mcp/releases/latest).

## Help shape what comes next

The next idea could be yours. We invite the OSS community to bring ideas, tricky integration questions, and feedback to [our Discord community](https://discord.gg/KguGXxg2X). When a direct conversation works better, reach out to **DeusData** or the project's co-developers there. Let's build what comes next together.

## License

MIT

---

### Images README (`docs/images/README.md`)

# CBM Lens Beta README captures

These are screenshots and browser recordings of the real application using public source from this repository. The application data and text were not retouched. The GIFs were refreshed on 20 October 2026; the still images preserve the earlier captures from 12–13 October.

## High-resolution recordings: 20 October

- Repository: [DeusData/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp)
- Source commit: `4cf44147e639ae070ee44d9230244bfb9d9676e0`
- Build: `make -f Makefile.cbm cbm-with-ui`, with the standard frontend and experimental agent activity disabled
- Capture: built-in browser screenshots on macOS, English locale, native viewport **1600 × 1000**, **12 frames per second**
- Encoding: JPEG screenshot frames converted to GIF with one 256-color palette per recording, no dithering, infinite looping; no upscaling, sharpening or pointer overlays

The `v0.0.1` label is the frontend package version, not the backend release version. Standard native releases include Lens; using a source build for these recordings is a capture-provenance detail, not an installation requirement.

### Data and interaction

The project `codebase-memory-mcp` is a clean `git archive` export of the source commit, indexed in `full` mode into an isolated local cache. No private repository was used. Because the export has no `.git` folder, the interface reports "Git unavailable" where applicable.

The index reported 81,190 nodes and 411,072 edges. These counts describe this source scope, not a benchmark or proof of complete repository coverage. The interface retains its coverage and display-limit notices. The clips join recorded interaction segments with pauses shortened between them; their displayed application states and answers are unchanged. Directional pulses represent indexed relationships, not observed execution.

| Recording | Duration | What it shows |
|-----------|----------|---------------|
| [lens-overview.gif](lens-overview.gif) | 12.5 seconds | Architecture overview, opening `src/foundation`, browsing its files, selecting `arena.c` and reading its source beside the map. |
| [lens-galaxy.gif](lens-galaxy.gif) | 12 seconds | Expanding outgoing relationships around `cbm_arena_alloc`, switching to the hierarchy and tracing the two-hop call path `cbm_arena_alloc → arena_grow → cbm_alloc`. |
| [lens-chat.gif](lens-chat.gif) | 11.5 seconds | `arena_grow` selected in Galaxy, with caller and callee questions answered directly from the indexed graph. |

All three GIFs are 1600 × 1000. Their frames and playback duration were decoded and checked after export. README and website use these same files.

### Local chat check

The optional Qwen2.5 Coder 0.5B model was downloaded and loaded through the application's own setup. Automatic explanations were disabled for this recording. Both displayed answers are deterministic graph answers, labelled "Listed from the indexed graph; not generated by the model." The clip shows one incoming `CALLS` relationship from `cbm_arena_alloc` and two outgoing `CALLS` relationships to `cbm_alloc` and `cbm_free`, alongside the other indexed relationship types. It does not demonstrate model-generated prose.

## Original still images: 12 October

Bernhard Jackiewicz captured the PNG stills on 12 October 2026, from commit `4d9592b5` on `main` after [PR #2068](https://github.com/DeusData/codebase-memory-mcp/pull/2068). The build used `scripts/build.sh --with-ui`; headless Chromium on macOS used English locale, a 1440 × 900 viewport and device scale factor 1. Experimental agent activity was disabled.

The indexed project was a public `git archive` export of that commit in `full` mode, with 75,037 nodes and 361,349 edges. The PNG stills are 1440 × 900, except the chat still at 1440 × 1200 so the explanation and answer fit together. These are historical application views, also used as reduced-motion alternatives on the website; they are not extracted from the new GIFs.

| Still | What it shows |
|-------|---------------|
| [lens-architecture.png](lens-architecture.png) | The whole repository on the architecture map. |
| [lens-chat.png](lens-chat.png) | A local model explanation of `cbm_pipeline_pass_doclinks` and a caller answer listed from the graph. |
| [lens-system-structure.png](lens-system-structure.png) | Component groups and their dependencies in System structure, with `src` selected. |
| [lens-behavior.png](lens-behavior.png) | Possible direct calls of `compileCards`, with its declaration. |

## Refreshed still images: 13 October

`lens-explore.jpg` and `lens-galaxy.jpg` are unedited browser captures at 1280 × 720 from the locally served CBM Lens Beta UI. The running binary reports `dev`; its exact build commit is unavailable, so these captures do not certify an exact source revision. The local project is named `cbm-triage-main-20260919`; the screenshots show public CBM source and indexed relationships.

- **[Explore](lens-explore.jpg):** `src/foundation/limits.c`, its compact file impact summary and direct dependencies (24 nodes, 26 edges). The UI's freshness, sampling and coverage notices remain visible.
- **[Galaxy](lens-galaxy.jpg):** `cbm_max_file_bytes`, both directions, all edge types, two layers (160 nodes, 218 edges).

These counts describe the displayed index snapshot, not a benchmark or proof of complete repository coverage.

---

### BENCHMARK (`docs/BENCHMARK.md`)

# Codebase Memory MCP -- v0.3.0 Language Benchmark

## Methodology

- **63 languages** (27 programming + 8 config/markup), 12 questions each (4 for config languages)
- **Up to 5 attempts** per question with escalating retry strategies
- **Real open-source repos** (medium to large: 78--49K nodes)
- **Grading**: PASS (1.0) / PARTIAL (0.5) / FAIL (0.0), N/A excluded from denominator
- **Platform**: Apple M3 Pro, macOS Darwin 25.3.0
- **Date**: 2026-03-01

### The 12 Questions

| # | Category | Question | Primary MCP Tool |
|---|----------|----------|------------------|
| Q1 | Indexing | Verify index stats (nodes, edges, schema) | `get_graph_schema` |
| Q2 | Discovery | Find functions/methods | `search_graph(label="Function")` |
| Q3 | Discovery | Find classes/structs | `search_graph(label="Class")` |
| Q4 | Pattern | Find by name pattern | `search_graph(name_pattern="...")` |
| Q5 | Code | Get code snippet | `get_code_snippet` |
| Q6 | Search | Text search in code | `search_code` |
| Q7 | Trace | Outbound call trace | `trace_call_path(direction="outbound")` |
| Q8 | Trace | Inbound call trace | `trace_call_path(direction="inbound")` |
| Q9 | Graph | Cypher CALLS query | `query_graph` |
| Q10 | Enrich | Params/returns/properties | `query_graph` |
| Q11 | OOP | Inheritance/implements | `query_graph` |
| Q12 | Files | List files & directories | `list_directory` |

Config/markup languages (HTML, CSS, SCSS, YAML, TOML, HCL, SQL, Dockerfile) run Q1, Q4, Q6, Q12. Other questions marked N/A.

---

## Summary Table

| # | Language | Repo | Nodes | Edges | Score | Pct | Tier |
|---|----------|------|------:|------:|------:|----:|------|
| 1 | Lua | neovim/neovim | 23,955 | 90,247 | 12/12 | 100% | 1 |
| 2 | Kotlin | ktorio/ktor | 25,297 | 71,498 | 12/12 | 100% | 1 |
| 3 | C++ | nlohmann/json | 5,262 | 8,681 | 12/12 | 100% | 1 |
| 4 | Perl | mojolicious/mojo | 3,287 | 4,182 | 12/12 | 100% | 1 |
| 5 | Objective-C | AFNetworking | 1,087 | 1,348 | 12/12 | 100% | 1 |
| 6 | Groovy | spockframework/spock | 14,081 | 34,557 | 12/12 | 100% | 1 |
| 7 | C | jqlang/jq | 1,330 | 3,923 | 11/11 | 100% | 1 |
| 8 | Bash | bats-core/bats-core | 436 | 479 | 10/10 | 100% | 1 |
| 9 | Zig | zigtools/zls | 2,824 | 10,230 | 10/10 | 100% | 1 |
| 10 | Swift | Alamofire/Alamofire | 3,631 | 7,639 | 10.5/11 | 95% | 1 |
| 11 | CSS | animate-css/animate.css | 295 | 214 | 4/4 | 100% | 1 |
| 12 | YAML | kubernetes/examples | 2,110 | 1,844 | 4/4 | 100% | 1 |
| 13 | TOML | rust-lang/cargo | 16,773 | 51,403 | 4/4 | 100% | 1 |
| 14 | HTML | twbs/bootstrap | 2,726 | 4,135 | 4/4 | 100% | 1 |
| 15 | SCSS | twbs/bootstrap | 2,726 | 4,135 | 4/4 | 100% | 1 |
| 16 | HCL | hashicorp/terraform | 78 | 76 | 4/4 | 100% | 1 |
| 17 | Dockerfile | docker-library/official-images | 1,481 | 1,588 | 4/4 | 100% | 1 |
| 18 | Python | django/django | 49,398 | 196,022 | 10.5/12 | 87% | 2 |
| 19 | TypeScript | nestjs/nest | 9,063 | 15,772 | 10.5/12 | 87% | 2 |
| 20 | TSX | shadcn-ui/ui | 29,755 | 41,883 | 10.5/12 | 87% | 2 |
| 21 | Go | codebase-memory-mcp (self) | 2,259 | 6,561 | 10.5/12 | 87% | 2 |
| 22 | Rust | BurntSushi/ripgrep | 4,118 | 6,971 | 10.5/12 | 87% | 2 |
| 23 | Java | spring-projects/spring-petclinic | 660 | 1,080 | 10.5/12 | 87% | 2 |
| 24 | R | tidyverse/dplyr | 1,618 | 2,409 | 10.5/12 | 87% | 2 |
| 25 | Dart | felangel/bloc | 5,089 | 6,430 | 10.5/12 | 87% | 2 |
| 26 | JavaScript | lodash/lodash | 244 | 405 | 9.5/11 | 86% | 2 |
| 27 | Erlang | ninenines/cowboy | 3,270 | 9,815 | 9.5/11 | 86% | 2 |
| 28 | Elixir | elixir-plug/plug | 870 | 865 | 9.5/11 | 86% | 2 |
| 29 | Scala | playframework/playframework | 19,627 | 43,764 | 9/12 | 75% | 2 |
| 30 | Ruby | sinatra/sinatra | 1,377 | 1,893 | 9/12 | 75% | 2 |
| 31 | PHP | laravel/framework | 38,644 | 161,242 | 9/12 | 75% | 2 |
| 32 | C# | jasontaylordev/CleanArchitecture | 1,043 | 1,632 | 9/12 | 75% | 2 |
| 33 | SQL | flyway/flyway | 4.5/6 | 75% | 4.5/6 | 75% | 2 |
| 34 | OCaml | ocaml/dune | 11,691 | 10,447 | 8/11 | 72% | 3 |
| 35 | Haskell | PostgREST/postgrest | 2,066 | 2,463 | 7.5/12 | 62% | 3 |

---

## Tier Classification

| Tier | Range | Count | Languages |
|------|-------|------:|-----------|
| **Tier 1** -- Excellent | >= 90% | 17 | Lua, Kotlin, C++, Perl, Objective-C, Groovy, C, Bash, Zig, Swift, CSS, YAML, TOML, HTML, SCSS, HCL, Dockerfile |
| **Tier 2** -- Good | 75--89% | 16 | Python, TypeScript, TSX, Go, Rust, Java, R, Dart, JavaScript, Erlang, Elixir, Scala, Ruby, PHP, C#, SQL |
| **Tier 3** -- Functional | < 75% | 2 | OCaml (72%), Haskell (62%) |

---

## Per-Language Results

### Python (django/django)

**Project**: `django-python` | **Repo**: `/tmp/lang-bench/django-python`
**Nodes**: 49,398 | **Edges**: 196,022

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 49,398 nodes, 196,022 edges, 12 labels, 20 rel types |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | render_to_string (1458 in), skipUnlessDBFeature, call_command. 1005 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | ModelAdmin (511 in), ValidationError (415 in), ImproperlyConfigured. 1005 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | SimpleTestCase, TestCase, SchemaTests. 22,135 total |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | render_to_string: 11 lines from django/template/loader.py:52-62 |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO\|FIXME) | 5 matches: FIXME in admin/checks.py, TODO in admin/options.py |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | 21 edges, 3 hops: render_to_string -> render, get_template, select_template |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 3 callers: test_include, test_include_state, test_include_cache |
| Q9 | Cypher CALLS | PASS | 3/5 | query_graph with project param | First 2 attempts needed project param adjustment |
| Q10 | Properties | PARTIAL | 3/5 | query_graph(properties) | Functions returned but all properties null |
| Q11 | Inheritance | PASS | 3/5 | query_graph(INHERITS) | 200 rows (capped): LazySettings, SimpleAdminConfig, ModelAdmin |
| Q12 | List Directory | PASS | 1/5 | list_directory | 21 entries: django/, docs/, tests/, extras/, pyproject.toml |

**Score: 10.5/12 (87%)**

### JavaScript (lodash/lodash)

**Project**: `lodash-js` | **Repo**: `/tmp/lang-bench/lodash-js`
**Nodes**: 244 | **Edges**: 405

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 244 nodes, 405 edges, 7 labels |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | build (7 variants), baseConvert, Hash. 36 total |
| Q3 | Find Classes | PASS | 2/5 | search_graph(Module) | No Class nodes (functional lib). Module fallback: 39 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | browser-testing.yml, markdown-doctest-setup.js. 10 total |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | build: 57 lines from lib/main/build-site.js |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 1 match: TODO in .github/workflows/ci-bun.yml |
| Q7 | Outbound Trace | PASS | 2/5 | trace_call_path(baseConvert) | 2 edges: baseAry, self-recursive |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 1 caller: build-dist.js module |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | 5 rows: minify->minify, build-dist.js->build |
| Q10 | Properties | PARTIAL | 1/5 | query_graph(properties) | Functions returned but properties null |
| Q11 | Inheritance | N/A | -- | -- | No classes/inheritance (pure functional library) |
| Q12 | List Directory | PASS | 1/5 | list_directory | 19 entries: dist/, fp/, lib/, test/, lodash.js |

**Score: 9.5/11 (86%)**

### TypeScript (nestjs/nest)

**Project**: `nestjs-ts` | **Repo**: `/tmp/lang-bench/nestjs-ts`
**Nodes**: 9,063 | **Edges**: 15,772

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 9,063 nodes, 15,772 edges; 14 labels |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | Injectable (in=242), Module (in=209), Controller (in=117). 292 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Post (in=128), FastifyAdapter, Module, InstanceWrapper. 1005+ total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | TestingModuleBuilder, TestController, test folders. 142 results |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | Injectable: 6 lines from common/decorators/core/injectable.decorator.ts |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 2 matches in express.spec.ts, gulp/tasks/samples.ts |
| Q7 | Outbound Trace | PASS | 2/5 | trace_call_path(Module) | First try Injectable had 0 outbound; Module: 4 edges |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(Injectable) | 242 callers across services, interceptors, pipes, guards |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | 5 rows: gulpfile.js->register, bar.service->Injectable |
| Q10 | Properties | PARTIAL | 1/5 | query_graph(properties) | Properties null |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | 200 rows: AuthGuard, BadGatewayException, ClientGrpcProxy |
| Q12 | List Directory | PASS | 1/5 | list_directory | 22 entries: packages/, integration/, sample/, tools/ |

**Score: 10.5/12 (87%)**

### TSX (shadcn-ui/ui)

**Project**: `shadcn-tsx` | **Repo**: `/tmp/lang-bench/shadcn-tsx`
**Nodes**: 29,755 | **Edges**: 41,883

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 29,755 nodes, 41,883 edges; 13 labels |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | cn (in=558), IconPlaceholder (in=362). 7,424 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | 18 classes: RegistryError, ComponentErrorBoundary |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 146 results: search.test.ts, registries.test.ts |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet(cn) | 3 lines from apps/v4/examples/base/lib/utils.ts:4-6 |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 5 matches in registry.ts, source.config.ts |
| Q7 | Outbound Trace | PASS | 2/5 | trace_call_path(Button) | Button->cn (1 edge) |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(cn) | 558 callers (result saved to file) |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | BlocksPage->getAllBlockIds, ChartPage->getActiveStyle |
| Q10 | Properties | PARTIAL | 1/5 | query_graph(properties) | Properties null |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | 14 rows: RegistryNotFoundError, ConfigMissingError |
| Q12 | List Directory | PASS | 1/5 | list_directory | 17 entries: apps/, packages/, templates/ |

**Score: 10.5/12 (87%)**

### Go (codebase-memory-mcp)

**Project**: `codebase-memory-mcp` | **Repo**: self
**Nodes**: 2,259 | **Edges**: 6,561

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 2,259 nodes, 6,561 edges; 13 labels |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | OpenMemory (in=136), NodeText (in=79), Parse (in=35). 918 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Node (in=243), LanguageSpec, FileInfo, Edge. 91 structs |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 413 results: cypher_test.go, langparity_test.go |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | OpenMemory: 16 lines from internal/store/store.go:113-128 |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 4 matches in tools.go, langparity_test.go |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | 11 edges: Open, Close, initSchema, cacheDir |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 68 callers: test files across store/pipeline/httplink |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | printAST->printAST, main->Parse, TestLexBasicQuery->Lex |
| Q10 | Properties | PARTIAL | 1/5 | query_graph(properties) | Properties null |
| Q11 | Inheritance | PASS | 1/5 | query_graph(IMPLEMENTS) | 3 rows: RelPattern, FilterWhere, fusedExpandMarker |
| Q12 | List Directory | PASS | 1/5 | list_directory | 15 entries: cmd/, internal/, scripts/, go.mod |

**Score: 10.5/12 (87%)**

### Rust (BurntSushi/ripgrep)

**Project**: `ripgrep-rust` | **Repo**: `/tmp/lang-bench/ripgrep-rust`
**Nodes**: 4,118 | **Edges**: 6,971

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 689 Functions, 2039 Methods, 294 Classes, 70 Enums |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | matcher (in=133), args (119), printer_contents (72). 689 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | HiArgs (out=93), LowArgs (out=74), Debug (in=54). 294 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | test method (in=524), SearcherTester, TestCommand. 134 results |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | matcher() from test_matcher.rs:8-10 |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3 TODOs in globset, gitignore, overrides |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(search) | 12 callees: quit_after_match, sort, haystack_builder, search_worker |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(search) | Called by run, main, search_parallel, files_parallel |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | main->set_git_revision_hash, main->set_windows_exe_options |
| Q10 | Properties | PASS | 1/5 | query_graph(properties) | 5 functions returned |
| Q11 | Inheritance | PARTIAL | 1/5 | query_graph(INHERITS) | 1 result: NoCaptures. Rust traits not modeled as inheritance |
| Q12 | List Directory | PASS | 1/5 | list_directory | 20 entries: crates/, tests/, benchsuite/, Cargo.toml |

**Score: 10.5/12 (87%)**

### Java (spring-projects/spring-petclinic)

**Project**: `spring-petclinic-java` | **Repo**: `/tmp/lang-bench/spring-petclinic-java`
**Nodes**: 660 | **Edges**: 1,080

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 167 Methods, 39 Classes, 30 Routes, 3 Interfaces |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Method) | getPet (in=12,out=6), testValidate (out=16). 167 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | OwnerControllerTests, PetController, Owner. 39 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 97 test-related nodes |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | getPet() from Owner.java:135-145 |
| Q6 | Text Search | PARTIAL | 2/5 | search_code(error) | TODO/FIXME returned 0; "error" found 3 matches |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | getPet calls getId, isNew, getName, getPets |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | Called by loadPetWithVisit, findPet, processCreationForm, test methods |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | toString->getName, addPet->isNew, getPet->getPets |
| Q10 | Properties | PASS | 1/5 | query_graph(properties) | main, registerHints, getId, setId, isNew |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | 8 results: NamedEntity, Person, Owner, Pet, Vet |
| Q12 | List Directory | PASS | 1/5 | list_directory | 13 entries: src/, gradle/, pom.xml, docker-compose.yml |

**Score: 10.5/12 (87%)**

### C++ (nlohmann/json)

**Project**: `nlohmann-json-cpp` | **Repo**: `/tmp/lang-bench/nlohmann-json-cpp`
**Nodes**: 5,262 | **Edges**: 8,681

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 2,456 Functions, 649 Methods, 603 Macros, 153 Classes |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | result (in=99), push_back (in=42), CAPTURE (in=35) |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Fuzzer (out=54), MutationDispatcher (out=36). 153 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 234 test-related nodes |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | result from unit-bjdata.cpp:2935-2936 |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3 TODOs in binary_reader.hpp, json.hpp |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(push_back) | Recursive call through json_pointer chain |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(push_back) | 23 callers: main, sax_event_consumer, lexer, binary_reader |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | main->at, main->back |
| Q10 | Properties | PASS | 1/5 | query_graph(properties) | 5 functions returned |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | 21 results: parse_error, invalid_iterator, type_error, out_of_range |
| Q12 | List Directory | PASS | 1/5 | list_directory | 20 entries: include/, tests/, docs/, CMakeLists.txt |

**Score: 12/12 (100%)**

### C# (jasontaylordev/CleanArchitecture)

**Project**: `clean-architecture-csharp` | **Repo**: `/tmp/lang-bench/clean-architecture-csharp`
**Nodes**: 1,043 | **Edges**: 1,632

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 303 Methods, 123 Classes, 5 Interfaces |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Method) | SendAsync (in=17), PaginatedList (in=11). 303 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | TodoItems (in=18), TodoLists (in=18), BaseTestFixture. 123 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 85 test-related nodes |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | SendAsync() from Testing.cs:38-45 |
| Q6 | Text Search | PARTIAL | 2/5 | search_code(error) | TODO/FIXME=0; "error" found 3 matches |
| Q7 | Outbound Trace | PARTIAL | 1/5 | trace_call_path(outbound) | SendAsync has 0 outbound (calls external ISender.Send) |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 16 callers: test methods |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | NotEqualOperator->EqualOperator, GetHashCode->GetEqualityComponents |
| Q10 | Properties | PASS | 1/5 | query_graph(properties) | AuthorizationBehaviour, Handle, LoggingBehaviour |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | 29 results: BaseAuditableEntity, TodoItem, TodoList |
| Q12 | List Directory | PASS | 1/5 | list_directory | 16 entries: src/, tests/, infra/, templates/ |

**Score: 9/12 (75%)**

### PHP (laravel/framework)

**Project**: `laravel-framework-php` | **Repo**: `/tmp/lang-bench/laravel-framework-php`
**Nodes**: 39,767 | **Edges**: 148,440 (post-Phase-4–5 PHP-LSP; was 196,979 baseline, –25%)
**CALLS edges**: 47,341 (was ~83k baseline — 43% reduction in name-fallback misroutes)
**Tests**: 278 PHP-LSP unit tests, all passing (total project: 3,091 / 0 failed)
**LSP module size**: ~3,700 lines C resolver + ~700 lines stdlib + ~5,500 lines tests = ~9,900 LoC

PHP runs through a Light Semantic Pass (`internal/cbm/lsp/php_lsp.c`,
~3,500 lines) that approaches phpactor-grade type resolution while
staying in-process and PHP-runtime-free. Phase 4 capabilities:

- Receiver-type tracking with full ancestor chain walk (cycle-detected,
  bounded to 32 hops)
- Namespace + `use`-clause resolution (class, function, const), incl.
  `as` aliasing in both wrapped and bare forms
- PHPDoc `@var`, `@param`, `@property`, `@method` parsing — generics
  (`Collection<User>`, `array<int, User>`) supported
- Type narrowing: `instanceof`, `is_string/int/array/...`, `assert(...)`
  sequential narrowing, negative narrowing on early return / throw
- Property type tracking: typed declarations, constructor property
  promotion, constructor-body inference (`$this->bar = $bar`)
- Trait flattening with `as` aliasing
- Late static binding chain depth, self/parent/static distinction
- Match / ternary / clone / cast result-type evaluation
- Foreach element-type propagation through `array<T>` and Iterator
- Magic methods (`__call`/`__callStatic` → facade dispatch)
- Stdlib coverage: SPL, PSR, DateTime, Throwable, Closure, plus
  Eloquent Builder/Model/Collection chains, Symfony HttpFoundation,
  Carbon date methods, PSR-7 with-builders

**Attribution correctness fix** (the primary metric, see
`docs/PHP_LSP_PRE_FLIGHT.md` §6):
- `ConfiguresPrompts.configurePrompts → helpers.value` misroute (3
  call sites): **fixed** — those typed-receiver `$prompt->value()` calls
  no longer route to the global helper. Verified: 0 misroute edges.
- Roughly 27 500 fewer total edges, primarily from blocking
  name-fallback edges where the receiver was a vendor (un-indexed) type
  — better precision at the cost of some recall, see notes below.

| Q# | Question | Grade | Approach | Notes |
|----|----------|-------|----------|-------|
| Q1 | Index Stats | PASS | get_graph_schema | unchanged |
| Q2 | Find Functions | PASS | search_graph(Function) | unchanged |
| Q3 | Find Classes | PASS | search_graph(Class) | unchanged |
| Q4 | Pattern Search | PASS | search_graph(name_pattern=test) | unchanged |
| Q5 | Code Snippet | PASS | get_code_snippet | unchanged |
| Q6 | Text Search | PARTIAL | search_code | search-backend issue, separate ticket — see PHP_LSP_PRE_FLIGHT §3 |
| Q7 | Outbound Trace | PASS | trace_call_path(outbound) | unchanged |
| Q8 | Inbound Trace | PARTIAL | trace_call_path(inbound) | tool-side disambiguation, separate ticket — see PHP_LSP_PRE_FLIGHT §4.1 |
| Q9 | Cypher CALLS | PASS | query_graph | unchanged |
| Q10 | Properties | PASS | query_graph(properties) | unchanged |
| Q11 | Inheritance | PASS | query_graph(INHERITS) | unchanged |
| Q12 | List Directory | PASS | list_directory | unchanged |

**Score: 10/12 (83%)** — the two remaining PARTIALs are non-LSP issues
tracked separately. PHP-LSP delivered the underlying graph-correctness
win (collide-set attribution); benchmark Tier 1 (≥ 90 %) requires the
search-recall and trace-disambiguation tickets to ship as well.

**Note on edge-count drop**: when the LSP knows the receiver is typed
but the receiver class is not indexed (e.g., Composer vendor types like
`Laravel\\Prompts\\Prompt`), it emits a synthetic
`php_method_typed_unindexed` resolution that the bridge uses to
**suppress** the unified extractor's name-based fallback. This trades
recall (no edge instead of a guess) for precision (no wrong edge). The
pre-flight argued this is the right trade for graph correctness.

**Test coverage**: 100 unit tests in `tests/test_php_lsp.c`, all
passing. Total project tests: 2913 / 0 failed.

### Lua (neovim/neovim)

**Project**: `neovim-lua` | **Repo**: `/tmp/lang-bench/neovim-lua`
**Nodes**: 23,955 | **Edges**: 90,247

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 12,311 Functions, 7,587 Variables, 1,157 Files, 399 Methods, 196 Classes |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | assert (in=825), vim.fn.bufnr (in=390), vim.fn.count (in=228) |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | cmdmod (in=84), file_buffer (out=80), TUIData (out=59). 196 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 198 test-related nodes |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | vim.validate: 21 lines from runtime/lua/vim/_core/shared.lua |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | Found across build.zig, test.yml |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | 5 callees: vim.deprecate, is_valid, validate_spec. Depth 2: 15 more |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 129K chars result (saved to file). Massive inbound |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | f_rpcstart->check_secure, f_termopen->f_jobstart |
| Q10 | Properties | PASS | 1/5 | search_graph(include_connected) | assert: in=825/out=2, connected to M.error, main |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | UGridPrinter inherits object |
| Q12 | List Directory | PASS | 1/5 | list_directory | 22 entries: src/, runtime/, test/, cmake/, deps/ |

**Score: 12/12 (100%)**

### Scala (playframework/playframework)

**Project**: `playframework-scala` | **Repo**: `/tmp/lang-bench/playframework-scala`
**Nodes**: 19,627 | **Edges**: 43,764

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 8,018 Methods, 2,924 Variables, 2,007 Classes, 309 Interfaces |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | app (in=48), queryString (in=14), json (in=9). 71 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Http (out=135), AbstractController (in=81), Controller (in=68) |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 636 test-related nodes |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | app: 1-line abstract def from play/api/test/package.scala |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | FIXME in CachedSpec, TODO in CryptoComponents |
| Q7 | Outbound Trace | PARTIAL | 1/5 | trace_call_path(outbound) | queryString: abstract method, no outbound calls |
| Q8 | Inbound Trace | PARTIAL | 1/5 | trace_call_path(inbound) | queryString: no inbound calls detected |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | fromContent->fromString, waitForCancel->waitForKey |
| Q10 | Properties | PASS | 1/5 | search_graph(include_connected) | ok: in=133/out=6, data: in=131/out=1 |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | 200 rows (capped): DefaultApplication, Controller, many *Spec |
| Q12 | List Directory | PASS | 1/5 | list_directory | 14 entries: core/, dev-mode/, documentation/, testkit/ |

**Score: 9/12 (75%)**

### Kotlin (ktorio/ktor)

**Project**: `ktor-kotlin` | **Repo**: `/tmp/lang-bench/ktor-kotlin`
**Nodes**: 25,297 | **Edges**: 71,498

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 8,485 Methods, 2,865 Functions, 2,572 Classes, 1,921 Routes |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | request (in=216), content (in=186), http (in=171). 1010+ total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | ContentType (in=224), HttpRequestBuilder (in=208). 1010+ total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | testApplication (in=1504), runTest (in=359). 5,015 total |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | request: 2 lines from ktor-client-core |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | "TODO: Remove when PR fixing this file will be merged" |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(HttpClient) | 4 callees: create, apply, invokeOnCompletion |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(HttpClient) | 3 callers: config, testPluginInstalledTwice |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | getNative->error, matches->toLowerCasePreservingASCIIRules |
| Q10 | Properties | PASS | 1/5 | search_graph(include_connected) | request: in=216/out=10, content: in=186/out=6 |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | 200 rows (capped): HttpClientEngineBase, many *Engine |
| Q12 | List Directory | PASS | 1/5 | list_directory | 39 entries: ktor-client/, ktor-server/, ktor-http/ |

**Score: 12/12 (100%)**

### Ruby (sinatra/sinatra)

**Project**: `sinatra-ruby` | **Repo**: `/tmp/lang-bench/sinatra-ruby`
**Nodes**: 1,377 | **Edges**: 1,893

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 786 Methods, 127 Classes, 37 Functions |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | setup (in=11), route_def, write_app_file. 37 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Sinatra (out=172), OkJson (out=31). 127 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 183 test-related nodes |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | setup: 4 lines from test/markdown_test.rb |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 2 matches: base.rb, content_for_spec.rb |
| Q7 | Outbound Trace | PARTIAL | 1/5 | trace_call_path(outbound) | setup in Minitest: no outbound calls (block-based) |
| Q8 | Inbound Trace | PARTIAL | 1/5 | trace_call_path(inbound) | No inbound calls for this method |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | status_app->mock_app, mock_app->new, setup_blocks->each |
| Q10 | Properties | PASS | 1/5 | search_graph(include_connected) | it: in=38/out=3, get: in=23/out=1 |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | 2 results: BError->AError, ErubiTest->ERBTest |
| Q12 | List Directory | PASS | 1/5 | list_directory | 18 entries: lib/, test/, examples/ |

**Score: 9/12 (75%)**

### C (jqlang/jq)

**Project**: `jq-c` | **Repo**: `/tmp/lang-bench/jq-c`
**Nodes**: 1,330 | **Edges**: 3,923

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 764 Functions, 159 Fields, 75 Variables, 45 Classes (structs) |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | jv_free (in=163), jv_get_kind (in=97), yyparse (out=51). 764 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | jq_state (out=25), yyguts_t (out=24), jv_parser (out=21). 45 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 25 test-related nodes: run_jq_tests, jv_test, LLVMFuzzerTestOneInput |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | jv_free: 1-line declaration from src/jv.h:55 |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 7 matches: build_manpage.py, execute.c, jv_parse.c |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | 5 callees: jvp_invalid/number/array/string/object_free. Depth 2: jvp_refcnt_dec |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 87K chars (saved to file). Massive caller tree |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | f_negate->type_error, f_json_parse->type_error, f_length->type_error |
| Q10 | Properties | PASS | 1/5 | search_graph(include_connected) | jv_free: in=163/out=6, jv_get_kind: in=97/out=1 |
| Q11 | Inheritance | N/A | -- | -- | C has no class inheritance |
| Q12 | List Directory | PASS | 1/5 | list_directory | 23 entries: src/, tests/, docs/, build/ |

**Score: 11/11 (100%)**

### Bash (bats-core/bats-core)

**Project**: `bats-bash` | **Repo**: `/tmp/lang-bench/bats-bash`
**Nodes**: 436 | **Edges**: 479

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 156 Functions, 65 Variables, 66 Modules |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | bats_print_stack_trace, main, run, bats_generate_warning. 156 total |
| Q3 | Find Classes | N/A | -- | -- | Bash has no classes |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=run) | 6 matches: run, bats_semaphore_run, reentrant_run |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | run: 142-line function source (310-451) |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 2 TODOs in semaphore.bash, tracing.bash |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | 6 callees: bats_quote_code, bats_format_file_line_reference |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 3 callers across 2 hops |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | 10 rows of function call pairs |
| Q10 | Properties | PASS | 1/5 | search_graph(Variable) | 65 variables |
| Q11 | Inheritance | N/A | -- | -- | Bash has no class hierarchy |
| Q12 | List Directory | PASS | 1/5 | list_directory | 66 modules found |

**Score: 10/10 (100%)**

### Zig (zigtools/zls)

**Project**: `zls-zig` | **Repo**: `/tmp/lang-bench/zls-zig`
**Nodes**: 2,824 | **Edges**: 10,230

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 1,010+ Functions, 1,005 Variables, 109 Modules |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | std.ArrayList, trace, empty, resolveTypeOfNodeUncached. 1010+ total |
| Q3 | Find Classes | N/A | -- | -- | Zig uses structs, not classes |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=resolve) | 57 matches: resolve, resolveTypeOfNodeUncached |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | trace: 19-line function showing tracy integration |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3+ TODOs in DiagnosticsCollection.zig, DocumentStore.zig |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | 2 callees: ___tracy_emit_zone_begin, _callstack |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 94 callers: processMessage, create, handleConfiguration |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | main->createRequestBody, build->getVersion |
| Q10 | Properties | PASS | 1/5 | search_graph(Variable) | 1,005 variables |
| Q11 | Inheritance | N/A | -- | -- | Zig has no inheritance |
| Q12 | List Directory | PASS | 1/5 | list_directory | 109 modules |

**Score: 10/10 (100%)**

### Elixir (elixir-plug/plug)

**Project**: `plug-elixir` | **Repo**: `/tmp/lang-bench/plug-elixir`
**Nodes**: 870 | **Edges**: 865

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 554 Functions, 129 Classes, 80 Modules |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | parse_before_param, parse_hd_before_value. 554 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Plug.Conn, Plug.Debugger, Plug.CSRFProtection. 129 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=parse) | 29 matches |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | match: 3-line macro source |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3 TODOs in adapters/test/conn.ex, ssl.ex |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | 6 edges across 2 hops |
| Q8 | Inbound Trace | PARTIAL | 1/5 | trace_call_path(inbound) | 0 callers (entry point function) |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | parse_headers->match, skip_preamble->before_parse_headers |
| Q10 | Properties | PASS | 1/5 | search_graph(Variable) | 6 variables |
| Q11 | Inheritance | N/A | -- | -- | Elixir uses protocols/behaviours |
| Q12 | List Directory | PASS | 1/5 | list_directory | 80 modules |

**Score: 9.5/11 (86%)**

### Haskell (PostgREST/postgrest)

**Project**: `postgrest-haskell` | **Repo**: `/tmp/lang-bench/postgrest-haskell`
**Nodes**: 2,066 | **Edges**: 2,463

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 746 Functions, 148 Classes, 655 Variables |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | defaultenv, jwtauthheader, headers. 746 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Thread, PostgrestTimedOut, ErrorBody. 148 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=action) | 4 matches: actionResult, actionPlan, actionResponse |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | actionPlan: 7-line Haskell source with pattern matching |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3+ TODOs in workflow files |
| Q7 | Outbound Trace | PARTIAL | 1/5 | trace_call_path(outbound) | 0 callees (function composition not traced as CALLS) |
| Q8 | Inbound Trace | PARTIAL | 1/5 | trace_call_path(inbound) | 0 callers (Haskell call patterns not fully traced) |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | generate_jwt->encode, run_command->run, main->generate_jwt |
| Q10 | Properties | PASS | 1/5 | search_graph(Variable) | 655 variables |
| Q11 | Inheritance | PARTIAL | 1/5 | query_graph(INHERITS) | 1 result: Thread (Python test helper). Typeclasses not modeled |
| Q12 | List Directory | PASS | 1/5 | list_directory | 184 modules |

**Score: 7.5/12 (62%)**

### OCaml (ocaml/dune)

**Project**: `dune-ocaml` | **Repo**: `/tmp/lang-bench/dune-ocaml`
**Nodes**: 11,691 | **Edges**: 10,447

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 1,010+ Functions, 12 Classes, 1,005 Modules |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | value, unit, append_files_in_dir_if_not_empty. 1010+ total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | fsenv, watch, CramLexer, DuneLexer. 12 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=build) | 111 matches |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | value: 4-line OCaml source with match expression |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3+ TODOs in workflow.yml, client.ml |
| Q7 | Outbound Trace | PARTIAL | 1/5 | trace_call_path(outbound) | 0 callees (pure function) |
| Q8 | Inbound Trace | PARTIAL | 1/5 | trace_call_path(inbound) | 0 callers (OCaml call analysis limited) |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | dune_trace_write->write, add_watch->isprefix |
| Q10 | Properties | PASS | 1/5 | search_graph(Variable) | 103 variables |
| Q11 | Inheritance | N/A | -- | -- | OCaml uses modules/functors |
| Q12 | List Directory | PASS | 1/5 | list_directory | 1,005 modules |

**Score: 8/11 (72%)**

### Erlang (ninenines/cowboy)

**Project**: `cowboy-erlang` | **Repo**: `/tmp/lang-bench/cowboy-erlang`
**Nodes**: 3,270 | **Edges**: 9,815

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 3,270 nodes, 9,815 edges; 8 labels |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | send (in=379), gun_open (in=278), do_get (in=103). 2,739 total |
| Q3 | Find Classes | PASS | 2/5 | search_graph(Module) | No Class nodes; Module fallback: 193 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 29 results |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | send: 2 lines from src/cowboy_http3.erl |
| Q6 | Text Search | PASS | 2/5 | search_code(error) | TODO/FIXME=0; "error" found 5 matches |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | send->send (self-recursive) |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 379 callers (saved to file) |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | start->compile, start->start_clear, stop->stop_listener |
| Q10 | Properties | PARTIAL | 1/5 | query_graph(properties) | Properties null |
| Q11 | Inheritance | N/A | -- | -- | Erlang has no class inheritance |
| Q12 | List Directory | PASS | 1/5 | list_directory | 12 entries: src/, test/, examples/ |

**Score: 9.5/11 (86%)**

### R (tidyverse/dplyr)

**Project**: `dplyr-r` | **Repo**: `/tmp/lang-bench/dplyr-r`
**Nodes**: 1,618 | **Edges**: 2,409

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 1,027 Functions, 13 Classes, 168 test nodes |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | arrange.data.frame, group_by, filter, mutate, select. 1,027 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | rlang_api_ptrs_t, Expander, FactorExpander. 13 total (C++ structs) |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 168 results: testthat.R, test-across.R |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | arrange.data.frame: 10 lines from R/arrange.R:88-97 |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 5 matches in R/across.R about deprecation |
| Q7 | Outbound Trace | PASS | 3/5 | trace_call_path(outbound) | C++ func found 2 edges (grouped/ungrouped) |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 3 callers: test files |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | context.R->new_environment, starwars.R->get_all/lookup |
| Q10 | Properties | PARTIAL | 1/5 | query_graph(properties) | Properties null |
| Q11 | Inheritance | PASS | 1/5 | query_graph(INHERITS) | 3 rows: FactorExpander, VectorExpander, LeafExpander inherit Expander |
| Q12 | List Directory | PASS | 1/5 | list_directory | 21 entries: R/, src/, tests/, vignettes/ |

**Score: 10.5/12 (87%)**

### Objective-C (AFNetworking/AFNetworking)

**Project**: `afnetworking-objc` | **Repo**: `/tmp/lang-bench/afnetworking-objc`
**Nodes**: 1,087 | **Edges**: 1,348

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | search_graph(File) | 89 files indexed |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | AFNetworkReachabilityCallback, data, destination. 114 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | AFSecurityPolicyTests, AFHTTPRequestSerializer. 80 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=Session) | 26 results |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | AFNetworkReachabilityCallback: 3-line function |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 1 match in AFImageDownloader.m |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(outbound) | 2 edges: Callback->PostReachabilityStatusChange->StatusForFlags |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | Callers traced |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | Function->Function edges present |
| Q10 | Properties | PASS | 1/5 | search_graph(Variable) | 32 variables |
| Q11 | Inheritance | PASS | 1/5 | search_graph(Method) | 632 methods extracted |
| Q12 | List Directory | PASS | 1/5 | list_directory | 17 entries: AFNetworking/, Tests/, UIKit+AFNetworking/ |

**Score: 12/12 (100%)**

### Swift (Alamofire/Alamofire)

**Project**: `alamofire-swift` | **Repo**: `/tmp/lang-bench/alamofire-swift`
**Nodes**: 3,631 | **Edges**: 7,639

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | search_graph(File) | 470 files indexed |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | write, read, responseStream, download. 117 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Session, Result, URLEncodedFormEncoder, AFError. 371 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=Request) | 480 results |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | write: 3-line function |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3 matches in Session.swift, SessionDelegate.swift |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(write) | 70KB+ result: 58+ callers |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | Rich caller chain |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | didCreateURLRequest->task, finish->finish |
| Q10 | Properties | PARTIAL | 1/5 | search_graph(Variable) | 0 Variables, 817 Fields (Swift properties as Fields) |
| Q11 | Inheritance | PASS | 1/5 | search_graph(Module) | 449 modules |
| Q12 | List Directory | PASS | 1/5 | list_directory | 19 entries: Source/, Tests/, Documentation/ |

**Score: 10.5/11 (95%)**

### Dart (felangel/bloc)

**Project**: `bloc-dart` | **Repo**: `/tmp/lang-bench/bloc-dart`
**Nodes**: 5,089 | **Edges**: 6,430

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | search_graph(File) | 900 files indexed |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | identical, downloadFile, analyzeDependencies. 479 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Bloc, Cubit, BlocObserver, HydratedCubit, MockBloc. 846 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=Bloc) | 268 results |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet(Bloc) | 278-line class source |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3 matches in gradle, kotlin, dart |
| Q7 | Outbound Trace | PARTIAL | 1/5 | trace_call_path(identical) | 0 edges (built-in function reference, leaf node) |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | Callers traced |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | generateCubitCode->get, getCubitTemplate->getFreezedCubitTemplate |
| Q10 | Properties | PASS | 1/5 | search_graph(Variable) | 592 variables |
| Q11 | Inheritance | PASS | 1/5 | search_graph(Method) | 873 methods, 14 INHERITS edges |
| Q12 | List Directory | PASS | 1/5 | list_directory | 11 entries: packages/, examples/, extensions/ |

**Score: 10.5/12 (87%)**

### Perl (mojolicious/mojo)

**Project**: `mojo-perl` | **Repo**: `/tmp/lang-bench/mojo-perl`
**Nodes**: 3,287 | **Edges**: 4,182

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | search_graph(File) | 192 files indexed |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | join, split, warn, decode, encode, render. 1,005 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | 0 classes (expected -- Perl uses packages) |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=render) | 26 results |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet(join) | 3-line sub returned |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 1 match in bundled highlight.js |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(join) | 114 results, rich 2-hop graph |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | 40+ callers: websocket, camelize, tablify |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | 491 CALLS edges: run->getopt, remote_address->split |
| Q10 | Properties | PASS | 1/5 | search_graph(Variable) | 1,003+ variables |
| Q11 | Inheritance | PASS | 1/5 | search_graph(Method) | 0 methods (expected -- Perl subs are Functions) |
| Q12 | List Directory | PASS | 1/5 | list_directory | 9 entries: lib/, t/, examples/, script/ |

**Score: 12/12 (100%)**

### Groovy (spockframework/spock)

**Project**: `spock-groovy` | **Repo**: `/tmp/lang-bench/spock-groovy`
**Nodes**: 14,081 | **Edges**: 34,557

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | search_graph(File) | 1,001 files (+ 284 unlisted) |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | cleanup, setup, println. 185 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | Specification, Issue, FailsWith. 1,005 total |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=Spec) | 802 results |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | isSatisfiedBy: interface method signature |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3+ matches in BuilderHelper.java, PojoBuilder.java |
| Q7 | Outbound Trace | PASS | 1/5 | trace_call_path(isSatisfiedBy) | 4 edges: matches, computeSimilarityScore |
| Q8 | Inbound Trace | PASS | 1/5 | trace_call_path(inbound) | match, describeMismatch callers |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | 5,876 Method->Method CALLS |
| Q10 | Properties | PASS | 1/5 | search_graph(Variable) | 1,003+ variables (1,278 total) |
| Q11 | Inheritance | PASS | 1/5 | search_graph(Method) | 1,003+ methods (6,788 total) |
| Q12 | List Directory | PASS | 1/5 | list_directory | 27 entries: spock-core/, spock-specs/, build-logic/ |

**Score: 12/12 (100%)**

### SQL (flyway/flyway)

**Project**: `flyway-sql` | **Repo**: `/tmp/lang-bench/flyway-sql`
**Nodes**: 10,149 | **Edges**: 23,222

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | search_graph(File) | 811 files indexed |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | 47 functions (Python/Bash helpers, not SQL functions) |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | 576 classes (Java: ClassicConfiguration, ConfigUtils) |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=Migration) | 456 results |
| Q6 | Text Search | PARTIAL | 1/5 | search_code(CREATE TABLE) | Found in docs but no actual SQL DDL indexed |
| Q12 | List Directory | PASS | 1/5 | list_directory | 22 entries: flyway-core/, flyway-docker/ |

**Score: 4.5/6 (75%)**

### Dockerfile (docker-library/official-images)

**Project**: `official-images-dockerfile` | **Repo**: `/tmp/lang-bench/official-images-dockerfile`
**Nodes**: 1,481 | **Edges**: 1,588

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | search_graph(File) | 206 files indexed |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=docker) | 12 results |
| Q6 | Text Search | PASS | 1/5 | search_code(FROM) | 3+ matches in workflow scripts |
| Q12 | List Directory | PASS | 1/5 | list_directory | 17 entries: library/, test/, Dockerfile |

**Score: 4/4 (100%)**

### CSS (animate-css/animate.css)

**Project**: `animate-css` | **Repo**: `/tmp/lang-bench/animate-css`
**Nodes**: 295 | **Edges**: 214

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 117 Files, 115 Modules, 37 Variables, 3 Functions |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 0 results (no tests in CSS lib) |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 0 matches (clean codebase) |
| Q12 | List Directory | PASS | 1/5 | list_directory | 14 entries: source/, docs/ |

**Score: 4/4 (100%)**

### YAML (kubernetes/examples)

**Project**: `k8s-yaml` | **Repo**: `/tmp/lang-bench/k8s-yaml`
**Nodes**: 2,110 | **Edges**: 1,844

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 1,235 Variables, 335 Files, 309 Modules |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 12 test-related nodes |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 9 matches across demo scripts |
| Q12 | List Directory | PASS | 1/5 | list_directory | 12 entries: AI/, databases/, web/ |

**Score: 4/4 (100%)**

### TOML (rust-lang/cargo)

**Project**: `cargo-toml` | **Repo**: `/tmp/lang-bench/cargo-toml`
**Nodes**: 16,773 | **Edges**: 51,403

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | search_graph(Variable) | 305 variables |
| Q4 | Pattern Search | PASS | 1/5 | search_code(TODO) | 3+ TODOs in workflows, Cargo.toml |
| Q6 | Text Search | PASS | 1/5 | list_directory(src/cargo) | 8 entries: core/, lib.rs, lints/, ops/ |
| Q12 | List Directory | PASS | 1/5 | search_graph(Module) | 1,005+ modules |

**Score: 4/4 (100%)**

### HCL (hashicorp/terraform)

**Project**: `terraform-hcl` | **Repo**: `/tmp/lang-bench/terraform-hcl`
**Nodes**: 78 | **Edges**: 76

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | search_graph(Variable) | 50 variables (version, constraints, hashes, region) |
| Q4 | Pattern Search | PASS | 1/5 | search_code(TODO) | 0 TODOs (clean config) |
| Q6 | Text Search | PASS | 1/5 | list_directory | 6 entries: main.tf, outputs.tf, variables.tf |
| Q12 | List Directory | PASS | 1/5 | search_graph(Module) | 5 modules: main.tf, .terraform.lock.hcl |

**Score: 4/4 (100%)**

### HTML (twbs/bootstrap)

**Project**: `bootstrap-scss` (shared) | **Repo**: `/tmp/lang-bench/bootstrap-scss`
**Nodes**: 2,726 | **Edges**: 4,135

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 1,525 Variables, 267 Methods, 195 Functions, 22 Classes |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 18 test-related nodes |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3 TODO/FIXME matches in js/src/carousel.js |
| Q12 | List Directory | PASS | 1/5 | list_directory | 15 entries: scss/, js/, dist/, build/ |

**Score: 4/4 (100%)**

### SCSS (twbs/bootstrap)

**Project**: `bootstrap-scss` (shared) | **Repo**: `/tmp/lang-bench/bootstrap-scss`
**Nodes**: 2,726 | **Edges**: 4,135

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 1,525 Variables, 267 Methods, 195 Functions, 22 Classes |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=test) | 18 SCSS test files |
| Q6 | Text Search | PASS | 1/5 | search_code(TODO) | 3 matches |
| Q12 | List Directory | PASS | 1/5 | list_directory | 15 entries: scss/, js/, dist/ |

**Score: 4/4 (100%)**

---

## Linux Kernel Stress Test

**Project**: `linux-kernel` | **Subset**: `drivers/net/ethernet/intel/`
**Files**: 387 C/H files | **Nodes**: 19,993 | **Edges**: 67,305

### Index Breakdown

| Label | Count |
|-------|------:|
| Function | 11,546 |
| Class (structs) | 1,851 |
| Method | 1,803 |
| Macro | 1,346 |
| Field | 1,095 |
| Community | 734 |
| Enum | 480 |
| Variable | 461 |

### Results

| Q# | Question | Grade | Attempts | Approach | Notes |
|----|----------|-------|----------|----------|-------|
| Q1 | Index Stats | PASS | 1/5 | get_graph_schema | 14 node labels, 11 edge types, 20K nodes, 67K edges |
| Q2 | Find Functions | PASS | 1/5 | search_graph(Function) | e1e_rphy (in=64), e1e_wphy (in=53). 11,546 total |
| Q3 | Find Classes | PASS | 1/5 | search_graph(Class) | nic (in=86), params (in=59). 1,851 C structs |
| Q4 | Pattern Search | PASS | 1/5 | search_graph(name_pattern=init\|probe) | 715 matches: i40e_probe (out=59), ixgbe_probe (out=43) |
| Q5 | Code Snippet | PASS | 1/5 | get_code_snippet | e1e_rphy: 4 lines from e1000.h:543-546 |
| Q6 | Text Search | PASS | 1/5 | search_code(BUG_ON\|WARN_ON\|pr_err) | Found across amt.c, e100.c, e1000_ethtool.c |
| Q7 | Outbound Trace (depth=5) | PASS | 1/5 | trace_call_path(outbound) | i40e_probe: 129K chars result. Massive call tree, no timeout |
| Q8 | Inbound Trace (depth=5) | PASS | 1/5 | trace_call_path(inbound) | e1e_rphy: 67.6K chars. 64 direct callers, deep chains |
| Q9 | Cypher CALLS | PASS | 1/5 | query_graph | 10 CALLS edges: netdev_boot_setup, amt functions |
| Q10 | Properties | PARTIAL | 2/5 | query_graph(Cypher) | `IS NOT NULL` not supported; retried with adjusted query |
| Q11 | Inheritance | N/A | -- | -- | C language |
| Q12 | List Directory | PASS | 1/5 | list_directory | 17 entries: e100.c, i40e/, ice/, ixgbe/, igb/ |

**Score: 11/11 (100%)** (Q11 N/A)

### Stress Metrics

| Test | Result | Size | Timeout? |
|------|--------|-----:|----------|
| Deep trace (depth=5, outbound, i40e_probe) | PASS | 129,026 chars | No |
| Deep trace (depth=5, inbound, e1e_rphy) | PASS | 67,600 chars | No |
| Deep trace (depth=5, both, e1000e_reset) | PASS | moderate | No |
| Multi-hop Cypher (3-hop CALLS chain) | PASS | 10 valid chains | No |

The tool handles 20K-node kernel subsystems without timeouts. Deep traces on well-connected probe functions (out_degree=59) produce massive but complete results. Community detection identified 734 clusters -- useful for understanding driver module boundaries.

---

## Cross-Cutting Findings

### Strengths
1. **Zero indexing failures** across 63 repos of all sizes (78 to 49K nodes)
2. **100% on 17 languages** -- half the languages achieved perfect scores
3. **No language below 62%** -- even the weakest performs core operations
4. **Massive codebases handled**: Django (49K nodes), Laravel (38K nodes), neovim (24K nodes) -- no performance issues
5. **Kernel-scale stress test passed**: 20K nodes, 67K edges, 129K-char traces, zero timeouts
6. **"Removed" languages still strong**: Obj-C (100%), Perl (100%), Groovy (100%), Swift (95%), Dart (87%)

### Common PARTIAL Patterns
- **Q10 (Properties)**: Most languages return functions with `properties=null`. Parameter/return extraction is incomplete -- this is the single most common deduction
- **Q7/Q8 (Trace)**: Abstract methods, entry-point functions, and built-in references correctly return 0 edges (no false positives), but are graded PARTIAL when no alternative function has edges
- **Q11 (Inheritance)**: Languages using alternative paradigms (traits, protocols, typeclasses, modules) have limited INHERITS edges

### Known Limitations
1. **Function properties**: Parameter types and return types are not extracted for most languages, resulting in `properties=null`
2. **Haskell call tracing**: Function composition (`f . g . h`) is not modeled as CALLS edges. Only explicit function applications are traced
3. **OCaml call analysis**: Limited call resolution due to module functor indirection
4. **Cypher `IS NOT NULL`**: Not supported in the Cypher parser -- use alternative query patterns
5. **Scala/Ruby trace**: Abstract methods and block-based patterns can have 0 trace edges
6. **query_graph 200-row cap**: Aggregation queries (COUNT) silently undercount on large codebases

---

## Aggregate Statistics

| Metric | Value |
|--------|------:|
| Languages tested | 35 |
| Total questions asked | 370 |
| PASS | 327 (88%) |
| PARTIAL | 25 (7%) |
| FAIL | 18 (5%) |
| N/A (excluded) | 41 |
| Weighted score | 339.5 / 370 |
| Overall percentage | **91.8%** |
| Perfect scores (100%) | 17 languages |
| Tier 1 (>=90%) | 17 languages |
| Tier 2 (75-89%) | 16 languages |
| Tier 3 (<75%) | 2 languages |

---

### CONFIGURATION (`docs/CONFIGURATION.md`)

# Configuration Reference

This page documents the configuration files that `codebase-memory-mcp` reads or writes today.

## At a Glance

| Purpose | Path | Format | Notes |
|---|---|---|---|
| Global custom extension mapping | `$XDG_CONFIG_HOME/codebase-memory-mcp/config.json` | JSON | Falls back to `~/.config/codebase-memory-mcp/config.json` when `XDG_CONFIG_HOME` is unset. |
| Per-project custom extension mapping | `{repo_root}/.codebase-memory.json` | JSON | Overrides conflicting global `extra_extensions` entries. |
| CLI-managed runtime settings | `${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/_config.db` | SQLite | Written by `codebase-memory-mcp config set/reset`. |
| UI settings | `${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/config.json` | JSON | Stores `ui_enabled` and `ui_port`. |
| Daemon operation log | `${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/logs/cbm-daemon.log` | Structured log | Durable daemon lifecycle, watcher/indexing, UI, resource, and error events. |
| Admission conflict log | `${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/logs/daemon-conflicts.ndjson` | NDJSON | Exact-build, ABI, and canonical-cache conflicts. |
| Activation log | `${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/logs/activation-events.ndjson` | NDJSON | Install/update/uninstall activation progress and outcomes. |

CBM resolves `CBM_CACHE_DIR` to a canonical per-account path before using any of these locations. The log directory and files are private to the account.

## 1. Custom File Extension Mapping

Two optional JSON files let you map additional file extensions to built-in languages.

### Global config

Default path:

```text
$XDG_CONFIG_HOME/codebase-memory-mcp/config.json
```

Fallback when `XDG_CONFIG_HOME` is unset:

```text
~/.config/codebase-memory-mcp/config.json
```

### Per-project config

Place this file in the repository root:

```text
.codebase-memory.json
```

### Format

```json
{
  "extra_extensions": {
    ".blade.php": "php",
    ".mjs": "javascript",
    ".twig": "html"
  }
}
```

Notes:

- Extension keys must include the leading dot.
- Language names are case-insensitive.
- Unknown language names are skipped.
- Missing files are ignored.
- If the same extension appears in both files, the per-project file wins.

## 2. CLI-Managed Runtime Settings

The `config` subcommand stores runtime settings in a small SQLite database:

```text
${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/_config.db
```

Inspect or change values with the CLI:

```bash
codebase-memory-mcp config list
codebase-memory-mcp config get auto_index
codebase-memory-mcp config set auto_index true
codebase-memory-mcp config set auto_index_limit 50000
codebase-memory-mcp config set watcher_enabled false
codebase-memory-mcp config reset auto_index
```

Current keys:

| Key | Default | Meaning |
|---|---|---|
| `auto_index` | `false` | Automatically index new projects when an MCP session starts. |
| `auto_index_limit` | `50000` | Maximum file count allowed for automatic indexing of a new project. |
| `auto_watch` | `true` | Register the session's project with the background git watcher on connect. Set `false` to keep a session from registering its project (the watcher still runs for other projects). |
| `doc_candidates` | `auto` | Score documentation sections against code to store doc↔code candidates (the possible targets of a section, and the sections that may describe a function or file, each with a judged probability). Unset (`auto`), the index mode decides: on for `full` indexes (the default mode), off for `moderate` and `fast`. Set `true` or `false` to choose for every mode; `config reset doc_candidates` returns to `auto`. The scoring grows with sections × functions: under a second up to ~100k functions, ~15 s at ~400k, ~3 minutes on the Linux kernel. Off stores no candidates and leaves the rest of the graph unchanged; on in `fast` mode stores candidates without adding `fast`'s skipped `SIMILAR_TO` / `SEMANTICALLY_RELATED` edges. `index_repository` also takes `doc_candidates` for a single index, which wins over this setting. Applies from the next index. |
| `watcher_enabled` | `true` | Master switch for the background watcher subsystem. Set `false` to stop the watcher from starting at all — no poll thread and no project registration. Reindex manually with `index_repository` when disabled. Only an open MCP session's own project is ever watched; `index_status` reports the current state in its `watch` object. |
| `watch_non_git` | `false` | Also poll project roots that are **not git repositories**. By default the watcher only follows git projects, so a project indexed from a plain directory is never refreshed after its first index — reindex it manually with `index_repository`. Set `true` to poll such roots on the same adaptive cadence with a file-tree scan: the indexer's own discovery walk (same skip lists, `.gitignore` and `.cbmignore` rules) hashed over each file's path, size and mtime. Any change reindexes once; paths the indexer skips (including cbm's own `.codebase-memory/` output) never trigger. The first poll after the daemon starts reindexes each such project once, since nothing records which tree state the index holds. The scan walks the whole tree every poll, so it costs more than git polling on very large trees. Read once when the daemon starts, like `watcher_enabled`. |
| `index_max_files` | `off` | Optional maximum number of accepted source files in one discovery run. |
| `index_max_source_mb` | `off` | Optional maximum accepted source size in MiB in one discovery run. |

For a watched project, `index_status` names the polling method in `watch.strategy`:
`git` (HEAD and dirty-state polling), `tree` (a non-git root polled by the
`watch_non_git` file-tree scan), `none` (a non-git root that is registered but never
polled, the default for a plain directory) or `pending` (registered; the first poll
has not run yet).

> **`watcher_enabled` vs `auto_watch`.** `watcher_enabled` controls whether the
> watcher *subsystem* starts at all (the background poll thread). `auto_watch` is
> narrower: it only controls whether a connecting session registers *its own*
> project with an already-running watcher. When `watcher_enabled=false`,
> `auto_watch` has no effect — there is no watcher to register with.
>
> They also differ in **when they are read**, which matters because the watcher
> lives in the background daemon, not in your MCP client:
>
> - `auto_watch` is consulted each time a session would register its project, so
>   a change applies to sessions that connect afterwards.
> - `watcher_enabled` is read **once, when the daemon starts**, because it decides
>   whether the watcher is built at all. The daemon is long-lived and outlives
>   individual MCP sessions, so **reconnecting your client is not enough** — retire
>   the daemon so the next one picks the new value up:
>
> ```bash
> codebase-memory-mcp config set watcher_enabled false
> codebase-memory-mcp daemon stop     # next session starts a daemon without the watcher
> codebase-memory-mcp daemon status   # confirm
> ```
>
> Disabling the watcher does not disable anything else: the daemon still starts,
> `auto_index` still runs, and `index_repository` stays available for manual
> reindexing.

The two `index_max_*` settings are independent and disabled by default. They
apply to explicit indexing, automatic indexing, and watcher re-indexing, but not
to `cross-repo-intelligence`, which does not scan repository source files.
Equality is allowed; exceeding either setting fails the complete index request
and preserves any previously serving database. See
[Index resource limits](INDEX_RESOURCE_LIMITS.md) for counting, validation, and
error-response details.

## 3. UI Settings

The optional built-in graph UI stores its settings in:

```text
${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/config.json
```

Current format:

```json
{
  "ui_enabled": false,
  "ui_port": 9749
}
```

Notes:

- If a UI-enabled binary finds its verified external asset pack and no UI config file exists yet, the UI auto-enables on first run. Missing or invalid assets leave the MCP/daemon service available and keep the UI disabled.
- `CBM_CACHE_DIR` changes both the UI config location and the runtime settings database location.
- CBM resolves `CBM_CACHE_DIR` to one canonical per-account cache root. A process configured with a different root fails while any CBM session or command is active; close them before switching roots.

## 4. Environment Variables

These environment variables affect runtime behavior:

| Variable | Default | Description |
|---|---|---|
| `CBM_ALLOWED_ROOT` | *(unset)* | Confine `index_repository` to paths within this directory. When set, a `repo_path` that resolves (after symlink / `..` resolution) outside this root is refused, and the same check now applies to the graph UI's `POST /api/index` route rather than only to the MCP tool. Unset imposes no *containment* restriction — but see the always-on limits below, which apply whether or not this is set. Useful when the server may be driven by an untrusted caller, e.g. agentic or multi-tenant deployments. |
| `CBM_CACHE_DIR` | `~/.cache/codebase-memory-mcp` | Override the cache directory used for indexes, `_config.db`, and UI `config.json`. |
| `CBM_DIAGNOSTICS` | `false` | Enable periodic `snapshot.json` and retained `trajectory.ndjson` below a fresh owner-private directory in the system temp directory. The daemon records the randomized paths in the `diagnostics.start` discovery record (a single JSON line) in `${CBM_CACHE_DIR}/logs/cbm-daemon.log`; that one record is emitted even when `CBM_LOG_LEVEL` suppresses ordinary logging, so the paths always remain discoverable. |
| `CBM_DOWNLOAD_URL` | GitHub releases | Override the update download URL. |
| `CBM_LOG_LEVEL` | role-aware | Set the log level to `debug`, `info`, `warn`, `error`, or `none` (or `0`-`4`). Thin MCP/CLI/hook frontends default to `warn`; the detached daemon and supervised index workers default to `info`. Physical workers retain INFO liveness records because their private logs drive the supervisor's no-progress timeout. Frontend messages use that session's stderr; detached daemon events use `${CBM_CACHE_DIR}/logs/cbm-daemon.log`. |
| `CBM_RUNTIME_DIR` | `%LOCALAPPDATA%` (Windows), `/private/tmp` (macOS), `/tmp` (other) | Parent directory for the daemon/CLI rendezvous directory, which CBM creates inside it as `cbm-daemon-<uid>` (`cbm-daemon-<key>` on Windows). Set it when the default ancestry cannot pass the private-directory check — see below. `CBM_CACHE_DIR` does **not** move the rendezvous. |
| `CBM_WORKERS` | auto-detected | Override the indexing worker count. |

### Relocating the daemon rendezvous directory

Before it is used, the rendezvous directory and every ancestor of it are checked:
each ancestor must be owned by you or by root, must not be world-writable (unless
it is the standard root-owned sticky directory such as `/tmp`), and must carry no
allow-ACL — on Windows, no ACE granting mutation rights to another identity. The
rendezvous directory itself is then forced to owner-only (`0700`, no extended ACL
/ an owner-only DACL).

That ancestry is not always acceptable in the default location. A Windows profile
that has acquired a capability-SID ACE with `WRITE_DAC` / `WRITE_OWNER` / `DELETE`
on `%LOCALAPPDATA%` — something an installed packaged app can add — fails the walk,
and so can an unusual `/tmp` or home directory on POSIX. When that happens *every*
command fails, `config list` included, so the settings surface cannot be reached
either:

```text
codebase-memory-mcp: secure daemon endpoint could not be created
```

`CBM_RUNTIME_DIR` points the rendezvous at an ancestry you choose:

```bash
export CBM_RUNTIME_DIR="$HOME/cbm-runtime"   # any directory you own
```

```powershell
$env:CBM_RUNTIME_DIR = "D:\cbm-runtime"
```

The check is not relaxed for the directory you name: it goes through exactly the
same validation as the default, and a value that fails it is refused rather than
silently ignored. Because the rendezvous is how sessions find each other, every
process that should share one daemon must see the same value — set it in the
environment of your MCP client and your shell alike, or a CLI invocation without
it will coordinate through the default location instead.

**WSL2 and Windows drives (`/mnt/c`, `/mnt/e`, ...).** `CBM_CACHE_DIR` goes through
the same private-directory check. With WSL's default automount options, DrvFs
reports every directory as `0777` and ignores `chmod`, so a cache under
`/mnt/<drive>` is refused, and the refusal names this remedy. Either turn on
permission metadata in `/etc/wsl.conf`, then run `wsl --shutdown` and reopen WSL:

```ini
[automount]
options = "metadata,umask=22,fmask=11"
```

or keep the cache on the Linux filesystem (the default `~/.cache/codebase-memory-mcp`),
which is also much faster than a 9p-mounted Windows drive.

Environment used by daemon-owned components—such as diagnostics, daemon logging, and process-wide indexing resource limits—is captured from the first daemon-backed session that starts the daemon. Later sessions join the existing process and cannot replace those values. To change them, close every daemon-backed session, update the relevant agent configurations consistently, and restart a session. `CBM_ALLOWED_ROOT` remains session-specific, a conflicting `CBM_CACHE_DIR` is rejected, and a one-shot CLI command is not exempt from the rule above: it connects to the coordination daemon like any other session, starting one if none is running, so its own environment becomes the captured daemon-owned environment only when its invocation is the one that starts the daemon — joining an already-running daemon, it inherits that daemon's already-captured values instead.


### Roots that are always refused

Independently of `CBM_ALLOWED_ROOT`, some directories are refused as an indexing
root because they are too broad or too sensitive to index as a unit:

- a filesystem root, a Windows drive root, or a UNC share root;
- a top-level system tree — `/etc`, `/var`, `/usr`, `/home`, `/Users`, and on
  Windows `C:\Windows`, `C:\Users`, `C:\ProgramData`, `C:\Program Files`;
- your home directory itself (directories *below* it are fine);
- a credential directory at any depth — `.ssh`, `.aws`, `.gnupg`, `.kube`,
  `.docker`, `.netrc`, `.git-credentials`, `.password-store`, macOS `Keychains`.

Two limits are worth stating plainly. This constrains *scope*, not
*sensitivity*: inside a root that is allowed, every file the process can read may
be indexed and later returned. And the credential list is a denylist, so it
raises the cost of a mistake rather than closing the class — a directory it does
not name is permitted.

### Shared / HPC filesystems

On clusters and other shared machines, home directories often live on a network
filesystem (NFS, Lustre, GPFS, SMB/CIFS), and `~/.cache` is sometimes a symlink
into a shared tree. Three rules keep CBM working there:

- **Put the cache and the rendezvous on local storage.** Set `CBM_CACHE_DIR` (indexes,
  `_config.db`) and, if needed, `CBM_RUNTIME_DIR` (the daemon rendezvous) to a
  local disk you own, for example `/local/scratch/$USER/cbm` or `/tmp/$USER-cbm`.
  Set the same values in your MCP client's environment and your shell.
- **Do not route the cache into another account's tree.** A symlinked `~/.cache` is
  resolved and allowed, but every directory on the *resolved* path must be owned
  by you or by root. If a parent such as `/shared/fs` belongs to another account,
  that account can rename or replace anything below it, so CBM refuses the path
  and names the directory and its owner:

  ```text
  ... (cache-private) - '/shared/fs' is owned by uid 1234 (alice), not by you (uid 1000) or root; ...
  ```

  This check has no override. Choose a location with a safe ancestry instead.
- **Avoid network filesystems for the cache.** The indexes are SQLite databases in
  WAL mode, which needs shared-memory locking that network filesystems do not
  provide reliably. On Linux, CBM logs one `daemon.cache_root_network_fs` warning
  at startup when the cache root is on NFS, Lustre, GPFS, CIFS or SMB2. The cache
  still works there, but it can be slow, and a database can be corrupted.

## 5. Agent and Editor Integration Files

The `install` command can also write MCP entries and instruction blocks into agent/editor config files such as Claude Code, Codex, Gemini, VS Code, Cursor, Zed, and others.

Those target paths vary by tool and platform, so the easiest way to inspect the exact files for your machine is:

```bash
codebase-memory-mcp install --dry-run
```

That prints the specific config files the installer would modify without writing anything.

---

### INDEX RESOURCE LIMITS (`docs/INDEX_RESOURCE_LIMITS.md`)

# Index resource limits

Index resource limits are optional operator controls for repositories whose
discovery breadth is not known in advance. They are disabled by default so
existing large-repository workloads retain their current behavior.

## Discovery settings

| Key | Default | Accepted value | Protects |
|---|---:|---:|---|
| `index_max_files` | `off` | `off` or `1..10000000` | Accepted source-file count |
| `index_max_source_mb` | `off` | `off` or `1..1048576` | Accepted source-file bytes |

Set or reset them with the normal configuration command:

```bash
codebase-memory-mcp config set index_max_files 250000
codebase-memory-mcp config set index_max_source_mb 16384
codebase-memory-mcp config reset index_max_files
```

Values use base-10 integers. MiB means 1,048,576 bytes. Empty values, zero,
negative values, suffixes, trailing characters, and values outside the stated
ranges are rejected without changing the stored value.

## Counting and failure semantics

`index_max_files` counts a file only after it passes directory pruning, ignore
rules, filename and suffix filters, language detection, and the existing
per-file size rule. `index_max_source_mb` sums the filesystem sizes of that same
accepted set.

Equality is allowed. The first file or byte that makes an observed value greater
than its limit stops discovery. CBM discards the partial file list and does not
publish a partial graph as a complete index.

For an explicit MCP request the error payload contains:

```json
{
  "status": "error",
  "code": "resource_limit_exceeded",
  "stage": "discovery",
  "resource": "files",
  "observed": 250001,
  "limit": 250000,
  "unit": "files",
  "retryable": true,
  "serving_index_preserved": true,
  "message": "Index discovery exceeded index_max_files"
}
```

The previous database remains available because publication occurs only after a
complete discovery and successful staged build. If no previous database exists,
`serving_index_preserved` is false.

## Trust and compatibility

Limits are read from the CLI-managed `_config.db`; they are not MCP request
arguments. A supervised parent replaces any caller-supplied internal policy
before spawning its worker, and the worker rejects a missing or incomplete
parent policy.

These settings do not replace or increase `auto_index_limit`, change the 512 MiB
single-file cap, alter workspace-root authorization, or affect
`cross-repo-intelligence`. With both settings `off`, discovery follows the
existing unbounded path.

---

### MEASURING SAVINGS (`docs/MEASURING_SAVINGS.md`)

# Measuring quality, latency, and agent savings

This guide describes how to measure codebase-memory-mcp (CBM) on your own
repository without mixing three different questions:

1. Does the agent give a better answer?
2. Is CBM fast and stable under the workload?
3. Does graph-assisted exploration use fewer model tokens or tool calls than
   file-by-file exploration?

Measure and report those classes separately. A fast graph query does not prove
that the final answer is correct, and CBM's query counters do not reveal the
agent's model-token or total tool-call consumption. The README's published
[performance figures](../README.md#performance), the
[language benchmark](BENCHMARK.md) and the more detailed
[evaluation plan](EVALUATION_PLAN.md) provide the grading concepts and the
larger comparative methodology behind this smaller recipe.

## Freeze the experiment

The shell examples in this guide require a POSIX-compatible shell on macOS or
Linux, or Git Bash on Windows. They are not native PowerShell syntax.
Run the command blocks in one dedicated shell: the setup and preflight blocks
enable fail-fast handling, and they capture Git command output before testing it
so a failed inner command cannot be mistaken for a clean worktree.

Before either condition runs, record:

- the repository URL or local identity and exact Git commit SHA;
- the question set and expected scope of each answer;
- the CBM version, index mode, operating system, and machine details;
- the agent model/version, system prompt, tool instructions, context limits,
  and per-question budget;
- which condition runs first, plus any warm-up policy;
- the client or evaluation harness used to record tokens and tool calls.

Use the same repository SHA, questions, model, prompt, budget, and clean-session
policy for both conditions. Do not let the second condition see the first
condition's answers or tool results. If runs are repeated, choose the repeat
count and aggregation method before looking at results and retain every run,
including failures and zero-result queries.

Do not run either condition in a merely clean-looking working copy. A frozen
experiment includes untracked files, so create separate clean detached
worktrees for the two conditions:

```bash
set -eu
SOURCE_REPO=/absolute/path/to/source-repository
COMMITISH=main
SHA=$(git -C "$SOURCE_REPO" rev-parse "$COMMITISH^{commit}") || exit 1
MODE=full
RUN_ROOT=$(mktemp -d) || exit 1
GRAPH_REPO="$RUN_ROOT/graph"
BASELINE_REPO="$RUN_ROOT/baseline"
ARTIFACT_ROOT="$RUN_ROOT/artifacts"

git -C "$SOURCE_REPO" worktree add --detach "$GRAPH_REPO" "$SHA" || exit 1
git -C "$SOURCE_REPO" worktree add --detach "$BASELINE_REPO" "$SHA" || exit 1
mkdir "$ARTIFACT_ROOT" || exit 1

GRAPH_SHA=$(git -C "$GRAPH_REPO" rev-parse HEAD) || exit 1
GRAPH_STATUS=$(git -C "$GRAPH_REPO" status --porcelain=v1 --untracked-files=all) || exit 1
BASELINE_SHA=$(git -C "$BASELINE_REPO" rev-parse HEAD) || exit 1
BASELINE_STATUS=$(git -C "$BASELINE_REPO" status --porcelain=v1 --untracked-files=all) || exit 1
test "$GRAPH_SHA" = "$SHA" || exit 1
test -z "$GRAPH_STATUS" || exit 1
test "$BASELINE_SHA" = "$SHA" || exit 1
test -z "$BASELINE_STATUS" || exit 1
```

Repeat the matching SHA and cleanliness assertions immediately before indexing
and immediately before each condition. If either assertion fails, stop and
create a new detached worktree; do not erase unknown files to make a reused
checkout appear clean. Write run artifacts under `ARTIFACT_ROOT`, outside both
condition worktrees.

## Preflight the graph

Immediately before every Graph-condition measurement, repeat its clean-worktree
assertions and require a successful fresh index of that exact worktree and mode:

```bash
set -eu
GRAPH_SHA=$(git -C "$GRAPH_REPO" rev-parse HEAD) || exit 1
GRAPH_STATUS=$(git -C "$GRAPH_REPO" status --porcelain=v1 --untracked-files=all) || exit 1
test "$GRAPH_SHA" = "$SHA" || exit 1
test -z "$GRAPH_STATUS" || exit 1
codebase-memory-mcp cli index_repository \
  --repo-path "$GRAPH_REPO" \
  --mode "$MODE" || exit 1
GRAPH_SHA_AFTER=$(git -C "$GRAPH_REPO" rev-parse HEAD) || exit 1
GRAPH_STATUS_AFTER=$(git -C "$GRAPH_REPO" status --porcelain=v1 --untracked-files=all) || exit 1
test "$GRAPH_SHA_AFTER" = "$SHA" || exit 1
test -z "$GRAPH_STATUS_AFTER" || exit 1
```

Take `PROJECT_NAME` from the successful indexing response. A verbose status call
is useful only as a root/current-HEAD cross-check:

```bash
codebase-memory-mcp cli list_projects
codebase-memory-mcp cli index_status --project PROJECT_NAME --verbose
```

Confirm that `root_path` is `GRAPH_REPO`, `git.head_sha` is `SHA`, and the Git
context is detached. `index_status` describes the project root and its current
filesystem Git context; it does **not** by itself prove that indexed records
were produced from that revision. The immediately preceding successful fresh
`index_repository` call is the freshness requirement.

Finally, run one or more representative queries whose expected symbols you
have verified directly at the recorded SHA:

```bash
codebase-memory-mcp cli search_graph \
  --project PROJECT_NAME \
  --name-pattern 'KNOWN_SYMBOL_PATTERN' \
  --limit 10
```

Do not start measurement until the fresh index succeeds, the status cross-check
names the intended detached checkout and current SHA, and the representative
queries return the expected symbols. Save the indexing response, status, and
query outputs with the run artifacts. Immediately before the file-by-file
condition, repeat the corresponding SHA and cleanliness assertions for
`BASELINE_REPO`.

## 1. Measure answer quality

Create a fixed set of real developer questions. Include a mix of definition
discovery, relationships or call paths, targeted source retrieval,
architecture, and cross-cutting questions where those dimensions apply. Record
the expected scope or an independently derived ground truth for each question.

Run two isolated conditions:

| Condition | Allowed exploration tools |
|-----------|---------------------------|
| Graph | CBM graph tools such as `search_graph`, `trace_path`, `query_graph`, `get_code_snippet`, `get_architecture`, and `search_code` |
| File-by-file baseline | File listing, text search, and targeted file reads only |

Grade the final answers against the source at the frozen SHA, not against how
plausible they sound. The compact rubric in [BENCHMARK.md](BENCHMARK.md) uses
PASS (1.0), PARTIAL (0.5), and FAIL (0.0), excluding truly inapplicable
questions from the denominator. For a finer comparison, follow
[EVALUATION_PLAN.md](EVALUATION_PLAN.md): score correctness, completeness, and
specificity separately, blind the grader to which condition produced each
answer, randomize A/B order, and require source evidence for the grade.

Keep quality scores beside, but separate from, efficiency metrics. A token
reduction is only useful when the answer still meets the chosen quality bar.

## 2. Measure latency and stability

Record indexing time separately from query latency, and classify the index run
as full-source, artifact-assisted, or incremental. When no local project
database exists, `index_repository` can import a compatible
`.codebase-memory/graph.db.zst` and then take the incremental-manifest route.
Before timing, record whether that artifact exists in `GRAPH_REPO`, retain the
index log, and require the later successful `artifact.import` record containing
`db` and `size_mb` before classifying the run as artifact-assisted. A bootstrap
attempt alone, or an `artifact.import` record containing `skip` or `err`, does
not prove that the artifact was used. For queries, use the same fixed workload
in the same order, identify warm-up calls in advance, and retain per-call
durations and exit status rather than only a single average. Report the machine,
OS, CBM version, repository SHA, index mode, question set, index-run class, and
whether query results are cold or warm.

### Built-in diagnostics

For a daemon-backed run, enable diagnostics before the first session starts.
The daemon captures its environment at startup; if it is already running,
close all daemon-backed sessions before changing the setting. See
[Configuration](CONFIGURATION.md#4-environment-variables) for the complete
environment contract.

```bash
export CBM_DIAGNOSTICS=1
```

CBM creates a fresh randomized, owner-private diagnostics directory below the
system temporary directory. Do not assume or construct an old predictable
`/tmp` filename. Discover the exact `snapshot` and `trajectory` paths from the
`diagnostics.start` JSON record in
`${CBM_CACHE_DIR}/logs/cbm-daemon.log` (the default cache directory is
`~/.cache/codebase-memory-mcp`). The record is emitted even when the configured
log level suppresses ordinary logging. The README's
[diagnostics section](../README.md#troubleshooting--diagnostics) explains the
files and retention behavior.

The live `snapshot.json` includes CBM-side `query_count`, `query_errors`,
`query_total_us`, `query_avg_us`, and `query_max_us`, along with process resource
counters. The retained `trajectory.ndjson` provides the resource and query-count
trend over time. These are useful for CBM latency and stability analysis, but
they are not a record of an agent's model usage or of non-CBM tools.
Because a daemon can be shared by multiple sessions, use an otherwise idle
daemon and record before/after values (or start a dedicated run) when attributing
its counters to one workload.

### Canonical soak workload

From a source checkout, the canonical endurance entry point is:

```bash
scripts/soak-legs.sh build/c/codebase-memory-mcp 10
```

It runs the quick mixed workload and the read-only query-leak workload, checks
for a valid completion summary, and writes per-call latency plus resource
results. The quick leg writes `soak-results/`; the query-leak leg writes
`soak-results-query-leak/`. Each directory contains `latency.csv`, `metrics.csv`,
and `summary.txt`. The two CSV files begin with these exact headers:

```text
timestamp,tool,duration_ms,exit_code
timestamp,uptime_s,rss_bytes,heap_committed,fd_count,query_count,query_max_us
```

Do not invoke `scripts/soak-test.sh` directly; `soak-legs.sh` owns the
release-gating sequence. Treat these artifacts as stability and CBM latency
evidence, not as answer-quality or model-token evidence.

## 3. Measure token and tool-call savings

Run the Graph and file-by-file conditions on the same frozen inputs described
above. The MCP client or evaluation harness must capture agent usage because
CBM cannot know the final model input/output token count or the agent's total
tool-call consumption.

Define each `run_id` as one paired experimental replicate. Each
`(run_id, condition)` is exactly one isolated client session that answers one
question. Capture one row for every usage window the client directly measures:

```text
run_id,condition,repo_sha,question_id,window,input_tokens,output_tokens,total_tokens,tool_calls,wall_time_ms,answer_artifact,quality_score
```

In each row, `tool_calls` is the count of every client tool invocation inside
that same window, including orchestration calls and the graph or file tools
permitted for that condition, plus retries, errors, and zero-result calls. Do
not count only CBM calls. Candidate windows are:

- **Answering tokens:** input plus output tokens between fixed markers around
  the question-answering phase.
- **Full-session tokens:** the entire isolated session, including orientation,
  initial probes, dead ends, and answer formatting.

The full-session value best represents an adopter's total cost; the answering
window helps explain where a difference arose. Do not substitute CBM's
`query_count` for either `tool_calls` value: it cannot see file searches, file
reads, or other client tools.

For each Graph/baseline pair, compare only a window that both clients report
directly, with at most one row per `(run_id, condition, window)`. If either
client cannot expose a window, omit that window or record its usage fields as
N/A and exclude it from the comparison. Never infer a missing window, partition
a session total across questions, or duplicate one session total into multiple
question rows.

Select one window `W` and calculate from the Graph and baseline rows for that
same window only:

```text
token reduction (%)     = 100 * (baseline tokens - Graph tokens) / baseline tokens
tool-call reduction (%) = 100 * (baseline calls  - Graph calls)  / baseline calls
token ratio              = baseline tokens / Graph tokens
tool-call ratio          = baseline calls  / Graph calls
```

Calculate and label every common measured window separately; never mix their
numerators or denominators. If baseline tokens or calls are zero, the
corresponding reduction percentage is N/A. If Graph tokens or calls are zero,
the corresponding ratio is N/A. Do not add pseudocounts. Publish the raw paired
counts alongside ratios and percentages. Also publish the quality result for
each pair, the number of runs, aggregation method, failures, and experimental
controls. Never turn a result from one repository, question set, model, or
machine into a universal savings claim.

## Reproducibility checklist

- Repository identity and exact SHA are recorded; each condition uses a clean,
  detached worktree with no tracked or untracked changes.
- CBM version, index mode, project name, and preflight outputs are retained.
- Every Graph run follows a successful fresh index of the frozen worktree and
  mode; verbose status is only a root/current-HEAD cross-check.
- Questions, ground truth, prompts, budgets, and condition order are frozen.
- Graph and file-by-file runs use isolated sessions and the same controls.
- Quality, CBM latency/stability, and agent savings are reported separately.
- Usage counts come directly from the client or evaluation harness; each
  condition run is one isolated question/session, common windows use separate
  rows, and unsupported windows are omitted or N/A rather than inferred.
- Diagnostics paths come from `diagnostics.start`, not a guessed temp path.
- Raw outputs, errors, zero results, and calculation inputs are retained.
- Claims identify their repository, SHA, question set, model, machine, and run
  count, with no invented or extrapolated benchmark numbers.

---

### RUNTIME TRACE MODEL (`docs/RUNTIME_TRACE_MODEL.md`)

# Runtime Trace Observation Model

## Status and recommendation

This document is an architecture contract, not an implementation. It recommends a
durable, versioned runtime-observation sidecar whose publications can be read alongside
a pinned static index generation. Runtime observations MUST NOT patch static `CALLS`
edges, fabricate static nodes, or appear as persistent edges in the default graph.

The model intentionally separates facts observed at runtime from facts derived from
source. A caller may explicitly request a `RUNTIME_CALL` overlay; existing static
search, architecture, and default trace behavior remain unchanged.

This document-only change does not alter the runtime wire version, runtime schema
version, runtime semantic version, runtime artifact version, static semantic index
version, or artifact schema version. In particular, the static semantic index version
remains 3.

## Scope

The model defines:

- stable identities for spans, producer contributions, observations, endpoints, and
  composite publications;
- deterministic ingestion, aggregation, publication, and replay semantics;
- generation-scoped resolution from runtime endpoints to static symbols;
- an opt-in runtime call overlay without changing the stored static graph;
- concurrency, retry, conflict, recovery, privacy, and cardinality boundaries; and
- six bounded implementation packages that can be reviewed and shipped separately.

## Non-goals

This design does not:

- infer new static `CALLS` edges or change confidence on existing ones;
- create placeholder nodes in the static graph for runtime-only identities;
- persist runtime edges in the generic graph edge table;
- make runtime data visible to existing graph tools unless an overlay is requested;
- define distributed tracing transport, sampling policy, or collector deployment;
- promise complete traces, causal proof from timing alone, or source-line attribution;
- store arbitrary span payloads, headers, request bodies, query strings, or secrets;
- average percentile summaries or treat a producer's p99 as a mergeable percentile;
- provide a compatibility promise for a future mutation API before its versioned
  contract exists; or
- change any runtime or static version merely by adding this document.

## Current surface

The current `ingest_traces` surface requires `project` and `traces[]`. Each trace item
permits `caller`, `callee`, and `count`, but those item fields are not currently
individually required. Its handler counts the supplied records and returns
`status="accepted"`, `traces_received`, and an unimplemented note; this is not an
accepted durable mutation, and the handler does not open or mutate a store.

The existing trace helper surface can extract `service.name`, HTTP method, HTTP path,
HTTP status, span kind, duration, URL path, and p99. Verified span-kind values are:

- `1`: internal;
- `2`: server; and
- `3`: client.

Generic graph edges are unique by source, target, type, and the local-name
discriminator. Generic edge upsert merges properties with `json_patch`. Those rules are
appropriate for the static graph but are not the contribution or aggregation rules in
this document. The store already exposes `BEGIN IMMEDIATE`, `COMMIT`, and `ROLLBACK`.
Static publication already uses opaque generations and request-scoped reads.

## Normative terms

`MUST`, `MUST NOT`, `SHOULD`, and `MAY` are normative. "Canonical JSON" means UTF-8
JSON with a fixed schema, lexicographically sorted object keys, arrays in their
specified order, no insignificant whitespace, and integers rather than locale- or
platform-dependent numeric strings. Hashes are computed over those canonical bytes.

## Identity layers

The following identities are different and MUST NOT be substituted for one another.

### Trace and span identity

A span is identified within one producer namespace by:

```text
(project, producer_id, producer_epoch, trace_id, span_id)
```

`trace_id` and `span_id` are opaque byte strings rendered in one canonical lowercase
hex form at the boundary. `producer_epoch` is part of span identity so a producer can
reuse trace or span identifiers in a later epoch without collision. The tuple identifies
an input span for diagnostics and deduplication; it does not identify a static node or
an aggregate.

### Producer contribution identity

Every accepted batch has a producer-stable identifier:

```text
contribution_id = H(canonical JSON {
  producer_id,
  producer_epoch,
  source_batch_id
})
```

`producer_id` names the authenticated logical producer, not a transient process.
`producer_epoch` changes only when that producer intentionally starts a new identity
domain. `source_batch_id` is stable across delivery retries. The resulting
`contribution_id` is scoped by project.

An accepted contribution is immutable. Retrying the same `(project, producer_id,
contribution_id)` with the same canonical payload hash is an idempotent success and
returns the original disposition. Retrying it with different canonical bytes is a
conflict and MUST be rejected without mutation. A producer MUST NOT reuse another
producer's namespace, and the service MUST NOT silently mint a replacement ID to turn
a conflict into a second contribution.

The payload hash covers the canonical normalized allowlisted contribution map together
with its pinned wire and runtime semantic version fields. It does not cover original
envelope ordering, unsupported fields, discarded fields, or raw payload bytes.

### Endpoint identity

Every endpoint has an explicit tag and a canonical key.

1. `symbol`: a source-derived symbol reference `(project, qualified_name)`. This stable
   endpoint identity deliberately excludes a static generation. A separate resolution
   record maps it within one named static generation and resolver version. A later
   generation MUST resolve again and MUST NOT inherit a node identifier from an older
   generation.
2. `runtime`: a runtime-only endpoint derived from an allowlisted protocol identity,
   such as canonical service name plus normalized HTTP route and method. It is stored
   only in the sidecar and never creates a static node.
3. `unknown`: an opaque, scoped endpoint used when the allowlisted evidence cannot
   identify either a symbol or a runtime service endpoint. Unknown keys MUST be stable
   only inside their documented scope (for example, producer plus trace) and MUST NOT
   collapse unrelated missing values into one global endpoint.

Endpoint canonicalization is versioned by `runtime_semantic_version`. All strings are
Unicode-normalized, methods and protocol tokens use their specified canonical case,
and absent values are distinct from empty values. Raw URL queries, fragments, and
credentials are excluded.

### Observation identity

An observation describes one directed runtime perspective, not one delivery. Its
canonical key is:

```text
observation_key = H(canonical JSON {
  runtime_semantic_version,
  project,
  protocol,
  perspective,
  source_endpoint_key,
  target_endpoint_key,
  operation_key
})
```

The key excludes counters, timestamps, duration summaries, status summaries,
contribution IDs, and static database row IDs. `operation_key` contains only the
versioned allowlisted operation identity, such as canonical HTTP method and normalized
route template. Consequently, identical observations from multiple producers converge
while semantically different directions or operations remain distinct.

### Composite publication identity

A reader-visible snapshot is identified by the atomic pair:

```text
{ static_generation, runtime_generation }
```

The pair is the head identity, but its halves are independent immutable publications.
A runtime generation identifies runtime contributions and aggregates without embedding
a static generation. Generation-scoped symbol resolutions and overlay projections are
keyed by the pair plus resolver version. Neither half may be silently advanced while a
request is using the pair.

## Protocol and perspective mapping

Ingestion parses and normalizes only versioned, allowlisted span fields into the
contribution, then derives a perspective from that evidence. The raw span payload is
not durably preserved. The HTTP mapper uses the existing service-name, HTTP method,
HTTP path or URL path, status, span-kind, and duration helpers. The route field is
normalized as a route template when the source provides one; a raw high-cardinality URL
path MUST NOT be promoted to a template by guesswork.

For the verified span kinds:

- `client` (`3`) represents an outbound attempt. The source is the resolved local
  symbol when available, otherwise a tagged local runtime or scoped unknown endpoint.
  The target is the allowlisted remote service/HTTP endpoint or a scoped unknown
  endpoint. The perspective token is `outbound`.
- `server` (`2`) represents an inbound handling attempt. The source is an allowlisted
  peer/runtime endpoint or a scoped unknown endpoint. The target is the resolved local
  handler when available, otherwise the local service/HTTP runtime endpoint. The
  perspective token is `inbound`.
- `internal` (`1`) represents local work. A directed call projection is produced only
  when explicit parent/child evidence supplies two endpoints. Otherwise the
  observation remains queryable as internal activity but does not become a
  `RUNTIME_CALL`. The perspective token is `internal`.

Unsupported or absent span kinds are retained only as normalized, allowlisted non-call
observation metadata if policy allows; unsupported fields and raw payload bytes are
discarded. They MUST NOT be guessed into one of the three perspectives. Status and
duration contribute to summaries but never to observation identity.

Symbol resolution is best-effort and generation-scoped. Ambiguous matches remain
tagged runtime or unknown endpoints. Resolution MUST fail closed: it must not choose a
symbol by filename substring, short-name coincidence, timing, or insertion order.

## Sidecar model

The durable sidecar is conceptually separate from the static graph store even if an
implementation initially shares the same database file. It contains these logical
relations:

### `runtime_endpoints`

Stores the endpoint tag, canonical endpoint key, and canonical allowlisted identity.
For a `symbol` endpoint, the durable identity contains project plus qualified name but
no static generation. Generation and static database row IDs belong only to resolution
records or caches, not durable endpoint identity.

### `runtime_contributions`

Stores project, producer ID, contribution ID, canonical payload hash, ingestion state,
and immutable canonical contribution metadata. States include at least `staged` and
`finalized`; an aborted transaction leaves neither state visible.

### `runtime_observation_contributions`

Maps `(contribution_id, observation_key)` to that contribution's canonical summary.
The map entry is immutable after finalization. Replacing it requires a new contribution
identity.

### `runtime_aggregates`

Stores the deterministic fold of finalized contribution-map entries for each
observation key and runtime generation. It is derived state and can be rebuilt from
finalized contributions.

### `runtime_resolutions`

Maps a stable runtime or unknown endpoint key to an optional stable SymbolEndpoint
`(project, qualified_name)` plus its current static node/cache result, unresolved
result, or ambiguity result. Each record is keyed by project, runtime generation,
static generation, stable endpoint key, and resolver version. Resolution records are
derived caches, not endpoint or observation identity, and a record from one static
generation MUST NOT be reused for another.

### `runtime_publications`

Stores immutable runtime generations independently of static generations. A separate
atomic per-project composite head pairs one current runtime generation with one current
static generation. Publication metadata includes the relevant runtime versions; the
composite head records the static index/artifact versions used for compatibility
checks.

No table above is a static graph node or edge table. The implementation MUST NOT reuse
generic `json_patch` edge upsert for contribution merging.

## Deterministic aggregation

Before immutable summaries are stored, one contribution is normalized deterministically:

1. canonicalize only allowlisted fields and compute the exact span identity
   `(project, producer_id, producer_epoch, trace_id, span_id)`;
2. deduplicate records with the same span identity and identical normalized bytes;
3. reject the entire contribution if one span identity has different normalized bytes;
4. group the remaining unique normalized records by `observation_key`;
5. after keyed exact-span deduplication/set union supplies idempotence, fold integer
   counts and duration sums, fixed status buckets, and fixed-schema histogram bins over
   the unique normalized span map using associative and commutative exact addition; and
6. sort keys and canonical-serialize the contribution map before hashing or storage.

The normalized contribution map and its immutable summaries MUST be byte-identical for
every input ordering and worker partitioning of the same unique normalized span set.
Discarded or unsupported fields cannot change the payload hash.

For each observation, aggregation is a map union keyed by contribution identity:

```text
aggregate(observation) = fold(sorted union {
  contribution_id -> canonical contribution summary
})
```

Union of identical key/value entries is idempotent. A differing value under the same
key is a conflict, not a winner selection. Map union is associative and commutative;
sorting by contribution ID before serialization makes the resulting bytes independent
of producer, retry, transaction, or worker order.

Canonical contribution summaries MAY contain integer count, integer duration sum in a
fixed unit, fixed-schema status buckets, and a mergeable duration distribution whose
encoding is pinned by `runtime_semantic_version`. Aggregate totals are exact integer
sums over the contribution map. Derived display values are computed after the fold.

Percentiles are not additive. A batch p99 MUST NOT be averaged, weighted-averaged, or
selected as the aggregate p99. The existing p99 helper may summarize a single raw batch
for diagnostics, but a published cross-contribution p99 requires a versioned mergeable
distribution (for example fixed histogram boundaries). Without such a distribution,
the aggregate p99 is unavailable.

Canonical aggregate JSON MUST be byte-identical for every permutation and
parenthesization of the same finalized contribution set.

## Ingestion and publication transaction

Full wire parsing, canonicalization, payload hashing, and every validation that does
not depend on mutable project state happen before acquiring the publication lock.
Publication is then serialized by one shared per-project publication lock used by both
static and runtime publishers. An implementation MUST NOT introduce an independent
runtime lock that permits lost updates to either half of the composite head.

Within that lock, a runtime publisher:

1. begins a write transaction with `BEGIN IMMEDIATE`;
2. reads and pins the current composite head and validates compatibility versions;
3. performs state-dependent authorization, quota, producer identity, deduplication,
   contribution conflict, and expected-head checks;
4. stages all endpoints, immutable contribution-map entries, and rebuilt aggregates;
5. allocates an opaque runtime generation independent of the pinned static generation;
6. durably finalizes every contribution in the publication;
7. writes the immutable runtime publication and atomically changes only the runtime
   half of the composite head, preserving its pinned static generation;
8. commits all changes with `COMMIT`; and
9. releases the per-project publication lock.

Every validation or write failure before commit uses `ROLLBACK`. There is no visible
partial contribution, aggregate, generation, or head. Finalization and head movement
are in the same transaction, so a success response is sent only after durable commit.

Static publication follows the same lock discipline and atomically changes only the
static half of the composite head, preserving its pinned runtime generation. It does
not create a new runtime generation merely because source was reindexed. Resolution
records for the new `{static_generation, runtime_generation}` pair are recomputed or
materialized under their pair-and-resolver-version key; records from the old static
generation are never treated as resolutions for the new one.

## Reader contract

A request that opts into runtime data pins one composite head at request start. Every
page, endpoint resolution, aggregate, and overlay edge in that request is read from
that pair. Default requests pin only their existing static request scope and do not pay
for or expose the overlay.

Pagination cursors encode the project, composite head, query shape, overlay mode, and
runtime semantic version. If the current head differs, a subsequent request with that
cursor returns a stale-cursor error; it never resumes against mixed generations.
Readers MAY explicitly request a still-retained historical composite head. Missing or
garbage-collected heads fail explicitly.

## `RUNTIME_CALL` overlay

Runtime call projection is an explicit read option. It presents directed overlay edges
derived from one pinned runtime generation. These edges:

- use the distinct type `RUNTIME_CALL`;
- carry endpoint tags so callers can distinguish symbol, runtime, and unknown ends;
- include deterministic aggregate summaries and the composite head identity;
- never mutate, replace, increase confidence on, or suppress a static `CALLS` edge;
- are omitted from existing search, architecture, and default trace responses unless
  the caller requests the overlay; and
- are regenerated when either half of the composite publication changes.

If a projected runtime call has symbol endpoints, it references their qualified-name
identity within the pinned static generation. Runtime-only and unknown endpoints remain
sidecar objects and are not fabricated as graph nodes.

## Version boundaries

Compatibility is divided deliberately:

- `wire_version`: request and response envelope, field encodings, and retry result;
- `runtime_schema_version`: sidecar tables, indexes, and migration compatibility;
- `runtime_semantic_version`: canonical endpoint/observation keys, mappings, and
  aggregation meaning;
- `runtime_artifact_version`: exported/imported sidecar artifact layout;
- `static_semantic_index_version`: source-derived graph semantics; and
- `static_artifact_schema_version`: static artifact representation.

A change bumps only the boundary it changes. A runtime semantic change can invalidate
runtime generations without changing static semantic index version. A storage-only
migration can change runtime schema version without changing observation identity.
Import rejects incompatible artifact versions before mutation. This document changes
none of these versions.

## Security, privacy, and cardinality

Ingestion is project-authorized and producer-authenticated. Producer IDs, contribution
IDs, and project scope are server-validated; caller-provided project or producer fields
do not grant access. Reads apply the same project authorization to runtime and static
data.

Only allowlisted attributes enter canonical identities or summaries. Credentials,
authorization data, cookies, request/response bodies, query strings, fragments,
arbitrary baggage, and raw exception text are rejected or discarded before durable
storage. Logs and errors use opaque IDs and counts, not rejected payloads.

The service enforces bounded request bytes, records, endpoints, observations, attribute
lengths, route-template segments, status buckets, and contribution-map growth. Raw URL
paths are not accepted as unbounded route labels. Unknown endpoints are scoped to avoid
both accidental global collapse and attacker-controlled global cardinality. Quotas and
retention apply per project and producer. Limit failure is all-or-nothing and does not
publish a truncated contribution unless a future wire version explicitly defines that
behavior.

## Failure and recovery matrix

| Condition | Required disposition | Visible state |
| --- | --- | --- |
| Same contribution ID, same payload hash | Idempotent success; return prior result | Unchanged |
| Same contribution ID, different payload hash | Conflict; reject entire request | Unchanged |
| Unauthorized project or producer | Reject before staging | Unchanged |
| Invalid/unsupported wire or semantic version | Reject before staging | Unchanged |
| Invalid endpoint, attribute, or cardinality limit | Reject entire contribution | Unchanged |
| Ambiguous symbol resolution | Keep runtime/unknown tag; do not guess | Valid sidecar observation only |
| Client-supplied expected composite head differs from the head read under lock | Reject as stale; retry from the newly read head | Unchanged |
| Failure while staging or aggregating | `ROLLBACK` | No partial state |
| Crash before commit | Database recovery rolls back the transaction | Prior head remains |
| Commit succeeds but response is lost | Producer retry resolves idempotently | One finalized contribution |
| Derived aggregate missing or corrupt | Rebuild from finalized contribution map before publish | Prior valid head remains |
| Cursor references a different or removed head | Return stale/missing cursor error | No mixed-generation read |
| Unsupported span kind | Retain only normalized, allowlisted non-call observation metadata; discard unsupported fields and raw payload bytes | No guessed call overlay |
| No mergeable duration distribution | Publish counts/sums; report p99 unavailable | No averaged p99 |

## Alternatives considered

### Patch static `CALLS` edges

Benefit: existing graph queries would see runtime evidence with no overlay option.

Trade-offs: it conflates observed and source-derived semantics, lets sampled or hostile
telemetry alter the static graph, cannot represent runtime-only endpoints honestly,
couples runtime retention to static indexing, and makes rollback and provenance
ambiguous. It is rejected.

### Fabricate static nodes for runtime services and unknown peers

Benefit: all endpoints would fit the existing graph node/edge shape.

Trade-offs: fabricated nodes look source-derived, create unstable identities and high
cardinality, collide with future symbol resolution, and pollute architecture/search
results. It is rejected.

### Store persistent runtime edges in the default graph

Benefit: generic edge uniqueness and `json_patch` upsert are already available.

Trade-offs: generic uniqueness has no immutable producer-contribution dimension;
`json_patch` is neither the required commutative map union nor a conflict detector;
default readers would unexpectedly mix generations and data classes. It is rejected.

### Keep runtime observations entirely ephemeral

Benefit: minimal schema, retention, and migration burden; no durable sensitive data.

Trade-offs: retries cannot be made idempotent across restarts, results are not
reproducible or pageable, multi-producer aggregation is unstable, and a reader cannot
pin a coherent composite publication. It is rejected as the primary model, though
ephemeral pre-validation remains useful.

### Durable versioned sidecar with an opt-in overlay (selected)

Benefits: preserves static semantics, gives runtime-only endpoints an honest identity,
supports deterministic replay and aggregation, isolates retention and authorization,
and provides atomic cross-generation reads.

Trade-offs: adds schema and version boundaries, a shared publication lock, endpoint
resolution, aggregate rebuilds, overlay-aware APIs, and explicit operational limits.
These costs are accepted because they make provenance, concurrency, failure recovery,
and compatibility testable rather than implicit.

## Bounded implementation packages

Implementation remains deferred. The following packages are intentionally ordered and
bounded; each requires its own reproduce-first tests and review.

1. **Versioned wire validation and canonicalization.** Define authenticated producer
   identity and epoch, contribution ID, exact span-identity deduplication, conflicting
   duplicate rejection, canonical contribution maps and payload hashes, allowlisted
   HTTP/span mapping, limits, and dry-run validation. No store mutation.
2. **Sidecar schema and immutable contributions.** Add runtime schema migrations,
   endpoints, contribution records, observation-contribution maps, idempotent retry,
   conflict rejection, and transactional rollback. No publication head or overlay.
3. **Deterministic aggregation.** Add canonical intra-contribution grouping, keyed
   exact-span idempotent set union/deduplication, associative and commutative exact
   integer/status/histogram addition, cross-contribution map-union folds, fixed
   mergeable duration distributions, input/worker/merge-order permutation tests,
   rebuild verification, and explicit unavailable p99 behavior.
4. **Generation-scoped resolution and publication.** Add the shared per-project lock,
   independent runtime generation allocation, durable finalization, pair-and-version
   resolution records, atomic composite head updates that preserve the other half, and
   crash/retry recovery tests.
5. **Pinned read and `RUNTIME_CALL` overlay.** Add opt-in queries, tagged endpoints,
   request-scoped composite pins, stale cursors, historical-head handling, and proofs
   that default static tools remain byte-for-byte unchanged.
6. **Artifact, retention, and operational hardening.** Add versioned runtime artifact
   import/export, quota/retention policy, authorization audits, corruption rebuild,
   migration/rollback tests, metrics, and multi-producer concurrency tests.

No package may silently absorb a later package's public surface or persistence model.

## Acceptance invariants

An implementation of this contract is acceptable only if all of these hold:

1. Static nodes and `CALLS` edges are byte-identical with runtime ingestion enabled or
   disabled for the same source input.
2. Existing search, architecture, and default trace responses exclude runtime data.
3. Runtime data appears only through an explicit, versioned overlay request.
4. Same producer contribution plus same payload is idempotent across retries and
   restarts; a different payload under that identity is a no-mutation conflict.
5. Normalized contribution and aggregate bytes are identical for every input, delivery,
   worker, and merge order of the same unique normalized span set, including identical
   duplicate replay; a conflicting duplicate span identity rejects the whole
   contribution without mutation.
6. Aggregate p99 is derived only from a pinned mergeable distribution and is otherwise
   unavailable; producer p99 values are never averaged.
7. Symbol resolution is qualified-name based, generation-scoped, and fail-closed on
   ambiguity; runtime and unknown endpoints never fabricate static nodes.
8. Runtime generation state contains all of its finalized contributions, endpoints,
   and aggregates, and reader visibility requires the separate atomic composite-head
   update; the transaction exposes both the complete generation and head update, or
   neither.
9. Every overlay read and page uses one immutable
   `{static_generation, runtime_generation}` pair; stale cursors fail explicitly.
10. Static and runtime publication serialize through the same per-project lock; each
    publisher changes only its own half of the composite head and preserves the other,
    and symbol-resolution records are never reused across static generations or
    resolver versions.
11. Unsupported span kinds, missing identities, and limit violations never become
    guessed calls or partial publications.
12. Authorization, privacy filtering, quotas, and retention are enforced before data
    becomes durable or reader-visible.
13. Wire, runtime schema, runtime semantic, runtime artifact, static semantic, and
    static artifact versions can change independently according to their boundaries.
14. Documentation-only adoption leaves all six version values and all runtime behavior
    unchanged.

---

### SECURITY DISCLOSURE (`docs/SECURITY-DISCLOSURE.md`)

# Security Disclosure & Handling Process

This document explains **how security reports are handled** for
codebase-memory-mcp — what happens after you report a vulnerability, what you
can expect from us, and how disclosure and credit work.

For **how to report** a vulnerability and **what is in scope**, see
[`SECURITY.md`](../SECURITY.md). This document covers the process *after* a
report arrives.

> **This is a solo, volunteer-maintained project.** Everything below is handled
> on a good-faith, best-effort basis. The timeframes are honest targets we aim
> to beat — not contractual guarantees. If something will take longer, we will
> tell you and keep you updated rather than go silent.

## Principles

We follow **coordinated disclosure**:

1. **Fix privately, disclose publicly.** Details of an unfixed vulnerability are
   never discussed in the open. We develop and validate the fix in private, ship
   a release, and only then disclose.
2. **Patch before publicity.** A fixed release is always available *before* the
   vulnerability is described publicly, so users can update immediately.
3. **Credit the researcher.** Public credit by default; anonymity on request.
4. **A bug fixed once should stay fixed.** Every fix ships with a regression
   test or guard so the same class of issue cannot silently return.

## What happens after you report

| Step | What we do | Target (best-effort) |
|------|------------|----------------------|
| 1. **Acknowledge** | Confirm we received your report and are looking at it. | within **7 days** (usually much sooner) |
| 2. **Triage & severity** | Reproduce the issue and assign a severity (CVSS). | within **14 days** |
| 3. **Fix privately** | Develop the fix in a private environment, with a regression guard, and validate it across all supported platforms (Linux, macOS, Windows) under full CI. | severity-dependent |
| 4. **You verify** | We invite you (read-only) to confirm the fix resolves the issue and that the guard prevents regression. Your sign-off is welcomed; an unresponsive reporter will not indefinitely block a release. | — |
| 5. **Release** | Merge the fix and cut a patched release promptly. | as fast as severity warrants |
| 6. **Disclose** | Publish a [GitHub Security Advisory](https://github.com/DeusData/codebase-memory-mcp/security/advisories), request a **CVE** (GitHub is a CNA), and credit you. | after a short upgrade window |

**Overall fix timeline:** we aim to resolve and release a fix within **90 days**
of triage, and much faster for high-severity issues. Critical, actively
exploitable issues are handled with the highest priority.

## Severity

We assess severity using **CVSS** and prioritise accordingly. Roughly:

- **Critical / High** — remote code execution, sandbox/scope escape, supply-chain
  compromise. Prioritised; expedited release.
- **Medium** — issues requiring local access, non-default configuration, or
  significant user interaction.
- **Low** — defense-in-depth gaps, hardening, information exposure with limited
  impact.

## Credit & CVE

- You are **credited by name/handle** in the published advisory unless you ask to
  remain anonymous.
- A **CVE identifier** is requested for each distinct vulnerability via the
  GitHub Security Advisory (one CVE per vulnerability, not per report — a single
  report may yield several).
- The advisory lists the **affected and patched version ranges** so downstream
  tooling (e.g. Dependabot) can alert users automatically.

## Safe harbor

We will not pursue or support legal action against researchers who act in
**good faith**, meaning you:

- only access, modify, or store data in **your own test environment**;
- avoid privacy violations, data destruction, and degradation of service for
  others;
- give us a **reasonable opportunity to fix** the issue before disclosing it
  publicly;
- do not exploit the issue beyond the minimum necessary to demonstrate it.

Good-faith research conducted under this policy is considered authorised, and we
will work with you, not against you.

## What we ask of you

- Report **privately** (see [`SECURITY.md`](../SECURITY.md)) — not as a public
  issue, PR, or social-media post.
- Give us **reasonable time** to fix before any public write-up.
- Provide enough detail to **reproduce** (affected version, steps, impact).

Thank you for helping keep a tool used by developers worldwide safe. 🙏

---

### SMART CI (`docs/SMART_CI.md`)

# Smart CI: pull-request test selection on GitHub Actions

Smart CI runs, for each pull request, only the CI jobs and tests that the
change can reach. This page explains how this repository does it and how
another repository on GitHub Actions could do the same with the released
`codebase-memory-mcp` binary.

Status: experimental. Every answer carries `"experimental": true`. The
selection narrows real test runs only while the repository variable
`TEST_IMPACT_MODE` is `gate`; otherwise it runs in report-only (shadow) mode.

> **Read the scope section before you start.** The lane layer is a pattern
> any repository can copy. The test layer narrows only C/C++ code whose tests
> follow this repository's test-framework conventions (or declared
> equivalents), and only with a per-test coverage map that you must produce
> yourself. In any other repository the engine answers "run everything". That
> is safe, but it saves nothing.

## The guarantee

Smart CI never runs fewer tests unless it can show that the omitted tests
cannot be affected. When it is unsure, it runs everything:

- a path that no rule knows, an empty or truncated file list, or a change to
  the selection's own configuration, tooling or workflows;
- a shallow clone, a diff that cannot be read in full, or an invalid config;
- no coverage map, or a coverage map that is not admitted (wrong commit, more
  than 7 days old, made for another platform or harness);
- a change that reaches the test runner's `main`, or a test-runner role
  (below) that no test names;
- a role table entry the engine cannot read, or a macro naming a role's
  literal that it cannot follow;
- any failure of the selection job itself.

Each of these makes the run cost more CI minutes. None of them loses coverage.

## Two layers

```
changed files ──► Layer 1: lane selector ──► which CI jobs run
                  (scripts/ci/select-lanes.sh, your own rules)
                                   │
                                   ▼  (the Linux x86_64 test leg only, in this repo)
merge base + PR ─► Layer 2: test-impact engine ─► which suites / tests run
                  (codebase-memory-mcp test-impact select
                   + team-artifact bundle from main)
```

| | Layer 1: lanes | Layer 2: tests |
|---|---|---|
| Question | Which jobs (lint, test legs, smoke, sanitizers) does this PR need? | Inside a test job, which suites and tests? |
| Input | The PR's changed-file list | Git history, the code graph, a per-test coverage map |
| Implementation | `scripts/ci/select-lanes.sh` (repository-specific rules) | The binary's `test-impact select` subcommand |
| Unknown input | Every lane runs | Every test runs (`run_all` with reasons) |
| Portable? | As a pattern: you write your own rules | C/C++ with supported test conventions only (see below) |

## Can your repository use it?

| Piece | General? | What you provide |
|---|---|---|
| Lane selection (Layer 1) | Pattern only | Your own path → category → lanes rules. `select-lanes.sh` encodes this repository's paths. |
| Engine (`test-impact select`) | Ships in the binary | Nothing to build. |
| Source languages it can narrow | **C family only**: `.c .h .cc .cpp .hpp .m .mm .cxx .hh` | Every other changed file must match a config rule (`ignore`, `referencing_tests`, ...). Otherwise the answer is `run_all` (`UNMAPPED_FILE`). |
| Test model | **C family only** | The `c-cbm` preset (this repository's `TEST`/`SUITE`/`RUN_TEST`/`RUN_SELECTED_SUITE` macros, see `tests/test_framework.h`), or custom `tests.conventions` records for equivalent C macros. |
| Coverage map | **Required to narrow at all** | Format-1 files (`functions.tsv`, `tests.tsv`, `meta.json`). This repository produces them with an LLVM-instrumented runner and `scripts/test-impact/coverage-map.py`, which are both specific to its harness. |
| Runner filter | Pattern | A test runner that can run a `suite` / `suite:test` list and fails when a listed test does not exist. |

The other presets (`pytest`, `go`, `junit`, `gtest`, `rust`, `jest`) are
accepted switches, but the engine does not model them. An enabled `gtest`
blocks narrowing for C++ files. Custom `tests.conventions` are covered by the
model's unit tests. The engine's end-to-end tests use `c-cbm` only. Treat
custom conventions as unproven until you have shadow evidence.

Without admitted coverage the engine marks every suite `whole`, and the lane
reports `run_all`. Static reachability alone never omits a suite.

## Prerequisites

1. **The binary, pinned.** Use the same release in the producer and the PR
   jobs. Download that release's Linux archive and verify it against the
   release's `checksums.txt`, as this repository's shadow job does
   (`scripts/ci/test-impact-shadow.sh --fetch-release`):

   ```bash
   CBM_VERSION=vX.Y.Z   # a release that contains the features you use (see below)
   asset=codebase-memory-mcp-linux-amd64.tar.gz
   gh release download "$CBM_VERSION" --repo DeusData/codebase-memory-mcp \
     --pattern "$asset" --pattern checksums.txt --dir "$RUNNER_TEMP/cbm"
   (cd "$RUNNER_TEMP/cbm" && grep " $asset\$" checksums.txt | sha256sum -c - &&
     tar -xzf "$asset" codebase-memory-mcp)
   "$RUNNER_TEMP/cbm/codebase-memory-mcp" --version
   ```

   For a quick start, `install.sh` installs the latest release with the same
   checks (HTTPS only, SHA-256 against `checksums.txt`, the archive's member
   list, `--version`); `--skip-config` skips agent configuration, which a CI
   runner does not need. It has no version flag, so it is not a pin.

   **Version note.** `v0.12.0` and `v0.12.1` have `test-impact select`/`publish`.
   They do not have `key: "path"`, `unreferenced`, `--vetoed-out` or
   `run_all_rules`; those ship in a later release. With either version, a config
   that uses those keys is invalid, and every answer is `run_all`. Until that
   release, build the binary from source (`make -f Makefile.cbm cbm`), as this
   repository's producer and `select-tests` jobs do.

2. **Full git history.** Check out with `fetch-depth: 0` in every job that
   runs the engine. The engine verifies the history it diffs and refuses a
   shallow clone (`GIT_UNAVAILABLE`, everything runs).

3. **A runner that runs a named subset.** See "The test job" below.

4. **A coverage producer on the default branch** that writes the format-1 map
   for every test, child processes included. A test whose coverage is
   uncertain must be marked `incomplete`. An incomplete test is always
   selected.

5. **`gh`, `python3`, and `GH_TOKEN` with `actions: read`** in the PR job, to
   fetch the bundle. GitHub-hosted runners include `gh` and `python3`.

## Recipe

Copy these files from this repository and adapt the marked lines:

| File | Adapt |
|---|---|
| `scripts/ci/test-impact-bundle.sh` | `WORKFLOW=` (producer file name) and the hard-coded `main` branch filter. |
| `scripts/ci/test-impact-select.sh` | `--lane unit` (your lane name) and the `PLATFORM` default (must equal the producer's). |
| `scripts/test-impact/selection.py` | Nothing, if your runner accepts `suite` / `suite:test` lines. |

### (a) Producer: publish a bundle for every commit on main

The bundle holds the commit's frozen graph, its coverage map and a
`receipt.json` that binds both by SHA-256. It is uploaded as the Actions
artifact `test-impact-<sha>`. `test-impact-bundle.sh` accepts a bundle only
from a successful `push`, `schedule` or `workflow_dispatch` run of the
producer workflow, in the same repository, on `main`, whose head is exactly
the requested commit.

```
/* Detailed source-code truncated for AI context efficiency. */
```

This repository's producer (`.github/workflows/test-impact-artifact.yml` with
`scripts/ci/test-impact-publish.sh`) adds an incremental mode. A push re-runs
coverage only for the suites that its own selection picks and merges them into
the previous map (`coverage-merge.py`). The graph is built incrementally from
the previous bundle (`--previous`). The weekly run rebuilds everything and
fails if the incremental graph topology differs from the full one. The
incremental mode is an optimisation. A full coverage run on every push is
simpler and just as correct.

The receipt keeps the time of the oldest coverage observation. An incremental
publish carries it forward unchanged. A map whose oldest observation is more
than 7 days old is refused (`TOO_OLD`), so a full refresh has to run more often
than that.

### (b) PR job, shadow mode (report only)

```yaml
  test-impact-shadow:
    runs-on: ubuntu-latest
    continue-on-error: true          # can never block or pass a merge
    permissions:
      contents: read
      actions: read                  # the merge base's bundle
    steps:
      - uses: actions/checkout@<pinned-sha>
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Install codebase-memory-mcp
        run: <the pinned install from Prerequisites>
      - name: Select with the team artifact (shadow)
        env:
          GH_TOKEN: ${{ github.token }}
        run: scripts/ci/test-impact-select.sh --binary "$RUNNER_TEMP/cbm/codebase-memory-mcp" --out "$RUNNER_TEMP/test-impact"
      - uses: actions/upload-artifact@<pinned-sha>
        if: ${{ always() }}
        with:
          name: test-impact-prediction
          path: |
            ${{ runner.temp }}/test-impact/selection-engine.json
            ${{ runner.temp }}/test-impact/summary-engine.md
```

Leave this job out of your aggregate required check. What
`test-impact-select.sh` does:

1. Takes the base side of the PR merge commit (`HEAD^1`) and fetches that
   commit's bundle.
2. If the producer skipped that commit (merge bursts: GitHub keeps one pending
   run per concurrency group), it uses the nearest first-parent ancestor that
   has a bundle, up to 20 commits back, and diffs against that ancestor. If the
   PR reverts a change that main made after the ancestor, the ancestor's map
   cannot speak for that file, so the script selects without coverage (the
   revert guard).
3. Passes `--artifact-verified` only for a bundle that it fetched and checked
   itself. Nothing inside a bundle can assert where the bundle came from.
4. Runs `test-impact select` and writes `selection-engine.json`. It also
   writes a Markdown summary to the job's step summary.

The engine reads `.codebase-memory.json` from the **selection base** (the merge
base, or the ancestor whose bundle was admitted), not from the PR. A PR cannot
loosen its own selection. A PR that changes the config runs
everything (`builtin:test-impact-config`).

### (c) Gate job and the test job

```yaml
  select-tests:
    if: ${{ vars.TEST_IMPACT_MODE == 'gate' }}
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      actions: read
    outputs:
      selection: ${{ steps.select.outputs.selection }}
    steps:
      - uses: actions/checkout@<pinned-sha>
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Install codebase-memory-mcp
        run: <the pinned install from Prerequisites>
      - id: select
        env:
          GH_TOKEN: ${{ github.token }}
        run: scripts/ci/test-impact-select.sh --binary "$RUNNER_TEMP/cbm/codebase-memory-mcp" --out "$RUNNER_TEMP/test-impact" --gate "$RUNNER_TEMP/test-selection"
      - if: ${{ steps.select.outputs.selection == 'narrowed' }}
        uses: actions/upload-artifact@<pinned-sha>
        with:
          name: test-selection
          path: ${{ runner.temp }}/test-selection
          if-no-files-found: error

  test:
    needs: [select-tests]
    if: ${{ !cancelled() }}          # a skipped or failed select-tests must NOT skip the tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<pinned-sha>
      - if: ${{ needs.select-tests.result == 'success' && needs.select-tests.outputs.selection == 'narrowed' }}
        uses: actions/download-artifact@<pinned-sha>
        with:
          name: test-selection
          path: ${{ runner.temp }}/test-selection
      - run: ./ci/run-tests.sh          # YOURS: narrows only if test-only.txt is present
```

With `--gate DIR`, the script runs `selection.py filter … --lane unit` and
writes `selection=narrowed` or `selection=all` to `$GITHUB_OUTPUT`. A narrowed
answer writes:

- `DIR/test-only.txt`: one token per line, either `suite` (run the whole
  suite) or `suite:test`;
- `DIR/optional-suites.txt`: selected suites that are registered only under a
  preprocessor condition. A build may lack them.

Everything else means `all`: a `run_all` answer, an unknown schema or filter
encoding, a filter that disagrees with the lane's suite list, a crash, and a
lane that selects nothing. The test job then gets no selection and runs every
test.

**The test job's runner** has to keep the same contract as this repository's
runner (`scripts/test.sh` → `scripts/run-tests-parallel.sh` →
`CBM_TEST_ONLY_FILE`, read by `tests/test_main.c`):

- launch only the suites that the tokens name, and only the named tests inside
  a `suite:test` suite;
- **fail** when the selection names a suite the build does not register,
  unless `optional-suites.txt` lists it;
- **fail** when a selected test cannot be found;
- **fail** when a selection was promised but the file is missing or empty.

A selection that is silently ignored, or partly ignored, would turn a narrowing
into missed coverage.

**A runner that starts itself again.** Tests often spawn the test binary as a
child (`<runner> __probe_x ...`) and its `main` hands over to a probe. A change
that reaches `main` reaches every test, so the engine runs everything
(`RUNNER_REACHED`). List the probes instead in a table in the runner's source,
one entry per line, as `tests/test_main.c` does:

```c
TF_ROLE(TF_PHASE_SEAMED, "__cbm_sockprobe", tf_maybe_run_socket_probe),
TF_ROLE(TF_PHASE_WORKER, "--index-worker", tf_maybe_run_index_worker),
TF_ROLE(TF_PHASE_SPAWNER, "CBM_INDEX_SUPERVISOR", tf_maybe_run_index_worker),
```

The engine reads the table: a change that only a probe's handler reaches
selects the tests whose source names one of that handler's literals (the argv
the child gets, or a switch that lets production code start it), also through
a `#define` that names the literal. The runner's own use of a literal names no
spawner. A reached handler that no test names, a table entry the engine cannot
read, and a change reaching `main` any other way still run everything; the
engine note names the call chain into `main`.

**Only the legs the coverage speaks for should narrow.** Here the coverage
comes from one Linux x86_64 clang-21 producer. Only the Linux x86_64 test leg
downloads the selection (`runner.os == 'Linux' && runner.arch == 'X64'` in
`_test.yml`). The arm64, macOS and Windows legs always run everything,
because code under architecture or platform `#ifdef`s has static evidence
only there. That leg builds with gcc, not clang: code under a compiler `#if`
has static evidence only too, and `--platform` names the producer's platform,
so the engine's platform check admits the map by construction.

### (d) Flip the variable

Shadow is the default: the variable is unset, `select-tests` is skipped, and
every test runs. When the evidence is in, set the repository variable:

```bash
gh variable set TEST_IMPACT_MODE --body gate      # gate
gh variable delete TEST_IMPACT_MODE               # back to shadow
```

Do not add `select-tests` to your required checks or to the aggregate gate.
When it breaks, PRs cost more minutes. They do not lose coverage.

### Optional: Layer 1 (lanes)

`scripts/ci/select-lanes.sh` is this repository's lane rule set. A `changes`
job lists the PR's files (the `pulls/{n}/files` API, with a git fallback),
runs the selector, and writes `tier`, `full`, `lanes` (a JSON list) and
`reasons` to `$GITHUB_OUTPUT`. Each lane job then runs
`if: contains(fromJSON(needs.changes.outputs.lanes), '<lane>')`. A single
aggregate job (`ci-ok`, `scripts/ci/require-all-green.sh`) is the only required
check. It fails when a *selected* lane was skipped, cancelled or missing.

For your own version, keep these properties:

- An unknown path, an empty list, workflow and harness changes, and a list
  truncated at the API's 3000-file cap all select every lane.
- The selector runs from a checkout without credentials, after the step that
  holds the token.
- Replay your PR history through the selector and check that every real
  failure had its catching lane selected. This repository's equivalent is
  `scripts/test-impact/replay-selector.sh`.

## Configuration: `.codebase-memory.json` → `test_impact`

| Key | Values | Default |
|---|---|---|
| `version` | `1` (required) | — |
| `tests.presets` | `{"c-cbm": true, ...}`. Unknown names are an error. | `c-cbm` off, the six others on |
| `tests.conventions` | C-family macro records: `case`, `suite`, `registration`, `suite_registration` | none |
| `rules` | ordered list, see below | none (built-ins only) |
| `lanes` | list of `{name, suites, exclude_suites?, default: "run"\|"skip", narrow?}` | one lane `unit`: all suites except `@perf`, `narrow: true` |
| `unmapped_files` | `"run_all"` (the only accepted value) | `run_all` |
| `coverage.compatibility_paths` | repo-relative files that the coverage map depends on (harness, runner, build flags) | none |

**Rules.** Each rule is `{id, paths, action, ...}`. `paths` are gitignore-style
globs with `{a,b}` alternatives. A pattern with a `/` is anchored at the
repository root. Without a `/`, it matches at any depth. `**` matches across
directories. Project rules come first, then the built-ins. For each changed
path, **the first matching rule wins**, so order matters.

| `action` | Effect |
|---|---|
| `run_all` | Any match runs everything. The summary names the rule (`run_all_rules`). |
| `ignore` | The path selects nothing. |
| `referencing_tests` | Selects the tests whose source contains the path as text. The engine tries the path itself, then its parent directories down to the `key`, each with and without its first segment. `key: "path_segment:N"` stops at segment N (zero-based). `key: "path"` tries the exact path only. If no test names the path, everything runs, unless `unreferenced: "ignore"` is set. |
| `suites` | Runs the named suites whole. |
| `lanes` | Activates the named lanes (for lanes with `default: "skip"`). |

Built-in rules, applied after yours:

| id | paths | action |
|---|---|---|
| `builtin:test-impact-config` | `.codebase-memory.json` | `run_all` |
| `builtin:build` | `Makefile*`, `CMakeLists.txt`, `*.cmake`, `meson.build`, `BUILD*`, `WORKSPACE`, `package*.json`, `go.mod`, `go.sum`, `Cargo.*`, `pom.xml`, `build.gradle*`, `pyproject.toml`, `setup.*`, `requirements*.txt` | `run_all` |
| `builtin:ci` | `.github/**`, `.gitlab-ci.yml`, `.circleci/**`, `Jenkinsfile*`, `.buildkite/**`, `azure-pipelines*.yml` | `run_all` |
| `builtin:vendor` | `vendor*/**`, `third_party/**` | `run_all` |
| `builtin:conftest` | `conftest.py` | `run_all` |
| `builtin:documentation` | `docs/**`, `**/*.md`, `LICENSE*` | `ignore` |

**Lanes.** A lane that you declare yourself does **not** narrow unless you set
`"narrow": true`. `signals` is accepted but has no effect yet.

**This repository's file, explained:**

| Rule | Why |
|---|---|
| `harness` → `run_all` | The test framework and the runner's `main` touch every suite. |
| `engine` → `run_all` | The selection's own code and config. A change to them could loosen what is selected. |
| `build` → `run_all` | Build and runner scripts: the file list cannot tell a flag change from a harmless one. `/.gitattributes` is anchored at the root. |
| `grammars` → `run_all` | Vendored parsers are not modelled per test. |
| `fixtures` → `referencing_tests`, `path_segment:2` | `tests/fixtures/<name>/...` selects the tests that mention the fixture, down to `tests/fixtures/<name>`. |
| `contracts` → `ignore` | Shell, Python and data contract tests run in every leg's contract step, which is never narrowed. |
| `notices` → `ignore` | Release notices. |
| `repo-data` → `referencing_tests`, `path`, `unreferenced: ignore` | Scripts, docs and packaging. A C test whose source names one of these files (its path, as text) is selected. A file that no test names selects nothing. |

`coverage.compatibility_paths` lists the harness and coverage tools. Changing
the bytes of any of them, or the `--platform` label, makes the existing coverage
map incompatible until the producer publishes a new one.

## Rollout and evidence

1. **Shadow.** Run (a) and (b). Leave `TEST_IMPACT_MODE` unset.
2. **Measure.** For each PR run, compare `selection-engine.json` with what
   failed in that run's full test legs.
   - A **miss** is a test that failed in the full run but was not selected.
     One unexplained miss blocks the flip.
   - **Savings** are the suites and tests that were not selected.

   Nothing in this repository computes misses for Layer 2 yet. Layer 1's
   history replay (`replay-selector.sh`) is the model to follow.
3. **Gate.** Set `TEST_IMPACT_MODE=gate`. Keep the shadow job running, so that
   the evidence keeps coming.

**Reading the step summary** (`### test-impact engine`):

| Line | Meaning |
|---|---|
| `decision: \`selected\`` / `\`run_all\`` / `\`nothing\``, then `run-all reasons [...]` | The top-level answer and why everything runs. `decided by rule(s) [...]` names the matched `run_all` rule ids. |
| `team artifact: fetched (ancestor …)` / `none`; `coverage \`admitted\`` / `\`rejected\` [reasons]` / `\`unavailable\`` | Whether a bundle was used, and whether its coverage was admitted. |
| `engine note: …` | The engine's first diagnostic line, such as the path that caused `UNMAPPED_FILE`, or the call chain into the runner's `main` for `RUNNER_REACHED`. |
| `lane \`unit\` (selected): N whole [REASON n, …], M narrowed (K tests)` | Per lane: suites that run whole (with a histogram of reasons), and suites narrowed to K tests. |
| `the runner-main veto (RUNNER_REACHED) discarded this selection:` | The change reached the runner's `main`, so everything runs. The lines below show what would have been selected otherwise. |
| `PR CI runs N selection tokens` / `PR CI runs every test (selection exit N)` | Gate mode only: what the test legs received. |

Common run-all reasons:

| Reason | Cause |
|---|---|
| `RULE_RUN_ALL` | A `run_all` rule matched (see `run_all_rules`). |
| `UNMAPPED_FILE` | A changed file matched no rule and is not C-family source. |
| `RULE_TARGET_UNKNOWN` | A `referencing_tests` path that no test names (without `unreferenced: "ignore"`). |
| `RUNNER_REACHED` | The change reaches the test runner's `main` (or a child role none of whose literals a test names). |
| `GIT_UNAVAILABLE` | No git, a shallow clone, or a diff that cannot be read in full. |
| `CONFIG_INVALID` / `POLICY_UNAVAILABLE` | The merge base's config or rules do not parse. |

Reasons per suite: `ARTIFACT_REJECTED` (no admitted coverage), `SETUP_HIT` /
`SETUP_UNKNOWN` (the suite's between-test setup ran changed code, or its
coverage is incomplete), `INVENTORY_UNKNOWN` (the suite cannot be read with
certainty), `CONDITIONAL` (`#if`-registered), `STATIC` / `CHANGED` / `COVERAGE`
/ `RULE` (the evidence that selected it).

## Failure modes

Every row below costs CI minutes, not coverage.

| What happens | Result |
|---|---|
| No bundle for the merge base | Nearest bundled ancestor, up to 20 first-parent commits back. Otherwise no coverage, and everything runs. |
| Bundle fetch fails (token, API) | Warning, no coverage, and everything runs. |
| The PR reverts a change that main made after the ancestor | Revert guard: no coverage, and everything runs. |
| Coverage older than 7 days, or a platform/compatibility mismatch | Map refused: every suite runs whole. |
| Shallow checkout | `GIT_UNAVAILABLE`: everything runs. |
| Engine deadline (default 1800 s) or crash | The job fails. The test job gets no selection, so everything runs. |
| `select-tests` skipped (shadow) or failed | Everything runs (`!cancelled()` on the test job). |
| The selection names a suite or test that the build lacks | The test run **fails**: a plumbing fault, reported, never skipped. |

## Known limits

- Narrowing covers C-family source and C test conventions only. Everything
  else is decided by rules or runs everything.
- The coverage map format and its producer come from this repository's LLVM
  toolchain and harness. You need your own equivalent.
- One producer platform. The selection narrows only the leg that matches the
  producer's platform; every other leg runs everything. Narrowing more legs
  needs a producer (and a bundle) per platform.
- The weekly full refresh and the 7-day age limit are the same length. Each
  week, when the refresh runs late, there can be a short window in which maps
  are refused and every test runs.
- In merge bursts the producer skips commits. The ancestor fallback covers up
  to 20 of them.
- The answer schema is `cbm.test_impact.v0` and is marked experimental. It may
  change between releases. Pin the binary.

---

### Cbmignore (`docs/cbmignore.md`)

# `.cbmignore` — Excluding Files from Indexing

`.cbmignore` is a project-specific ignore file that controls which files the
indexer sees. It uses gitignore-style syntax and is read from the **root of
the indexed directory** (`<repo>/.cbmignore`). Nested `.cbmignore` files in
subdirectories are not read.

It applies at **file discovery time** — the directory walk that selects files
for parsing. Every indexing path uses the same discovery: the initial
`index_repository`, manual re-indexing, and background auto-sync. A path
matched by `.cbmignore` never enters the graph. Changes to `.cbmignore` take
effect on the next (re-)index.

Unlike `.gitignore`, it has no effect on git itself — it only shapes what the
indexer sees. Commit it to share indexing excludes with your team, or list it
in `.gitignore` to keep personal excludes untracked.

To verify it works: directory subtrees skipped during discovery are reported
in the `index_repository` response under `excluded`
(`{"dirs": [up to 25 paths], "count": <total>, "truncated": <bool>}`).

## Syntax

One pattern per line. Blank lines are ignored, lines starting with `#` are
comments, and trailing whitespace is trimmed.

| Feature | Meaning |
|---|---|
| `*` | matches any run of characters, except `/` |
| `?` | matches exactly one character, except `/` |
| `**` | matches across directory boundaries (`**/name`, `dir/**`, `a/**/b`) |
| `[abc]`, `[a-z]` | character classes; `[!a-z]` / `[^a-z]` negate the class |
| trailing `/` | pattern matches **directories only** |
| `/` anywhere else | anchors the pattern to the repo root |
| no `/` in pattern | matches the file/directory name at **any depth** |
| leading `!` | negation — re-includes a previously matched path; the **last matching pattern wins** |

Examples:

```gitignore
# Generated protobuf output, anywhere in the tree
*.pb.go

# A specific top-level directory (leading / anchors to the repo root)
/third_party/

# Any directory named "snapshots", at any depth (trailing / = directories only)
snapshots/

# Everything under any fixtures directory
**/fixtures/**

# Anchored glob: generated clients for any single-character API version
/api/v?/generated/

# Character class: yearly log folders 2020-2029
/logs/202[0-9]/

# Ignore all YAML, but keep CI configs (negation — last match wins)
*.yaml
!ci.yaml
```

## Precedence

Discovery applies its filters in a fixed order. In general, the first layer
that rejects a path wins, except `.cbmignore` negations (layer 4) can un-skip
ordinary built-in skip directories from layer 1 and rescue paths from layer 5.
For directories:

1. **Built-in skip list** — `.git`, `node_modules`, `dist`, `target`,
   `vendor`, tool caches, etc. (60+ names; the fast/moderate index modes add
  more, e.g. `docs`, `examples`, `testdata`). Generated output whose
  folder name alone is too common to skip everywhere is matched by path
  suffix instead: Laravel's compiled Blade view cache
  `storage/framework/views/` (at any depth) is skipped even without
  Laravel's stock `.gitignore` there, while other `views/` folders stay
  indexed. A `.cbmignore` negation
  (for example `!target/`) can un-skip these directories, except the
  non-negatable safety core: `.git`, `node_modules`, `.worktrees`, and
  `.claude-worktrees`.
2. **Repo `.gitignore`** — `<repo>/.gitignore` merged with
   `<git-common-dir>/info/exclude` (worktree-aware); later patterns win on
   conflict. Honored even when the indexed directory is not a git repo root.
3. **Nested `.gitignore` files** — picked up during the walk and matched
   relative to their own directory.
4. **`.cbmignore`** — a positive match skips the path; a negated match can
  un-skip built-in skip dirs from layer 1 (except the safety core) and can
  also rescue paths from layer 5.
5. **Git global excludes** — `core.excludesFile` from `~/.gitconfig` or the
   XDG git config (default `$XDG_CONFIG_HOME/git/ignore`); consulted only
   when the project is a git repo with a config.

For files, built-in suffix filters (`.png`, `.o`, `.db`, …; fast modes add
archives, media, lockfiles, `.min.js`, …) and fast-mode filename/substring
filters run **before** the ignore files, and a maximum-file-size cap runs
after them; none of these are overridable from `.cbmignore`. Symlinks are
always skipped.

## Negation (`!`) — current behavior

- **Within `.cbmignore`**: standard gitignore semantics. Patterns are
  evaluated top to bottom and the last matching pattern wins, so
  `!pattern` re-includes something an earlier line excluded.
- **Parent pruning** (same caveat as git): when a directory is excluded, the
  walk never descends into it — you cannot re-include a file whose parent
  directory is excluded. Negate the directory itself if you need its
  contents.
- **Across layers**: a `.cbmignore` negation overrides the **git global
  excludes** layer and can also un-skip ordinary built-in skip directories.
  Example: your `~/.config/git/ignore` ignores `*.sql`, but this project's
  SQL should be indexed — add `!*.sql` to `.cbmignore`. Negation still cannot
  override the safety core built-in dirs (`.git`, `node_modules`,
  `.worktrees`, `.claude-worktrees`), the repo `.gitignore`/`info/exclude`,
  nested `.gitignore` files, the built-in suffix/filename filters, or the
  size cap.

### Planned follow-up

- Auxiliary filesystem walkers will honor the same ignore predicate as
  discovery, so every code path sees an identical ignore decision
  (unification tracked in a follow-up issue).

---

### Llms (`docs/llms.txt`)

# codebase-memory-mcp

> Codebase Memory is an open-source local MCP server for AI coding agents. It connects code, documentation, configuration, and infrastructure in a persistent knowledge graph, with 17 MCP tools and native CLI access. Search for a feature, follow callers and dependencies, trace services and deployments, and inspect change impact. Indexing, graph queries, and semantic embeddings run locally without a hosted service or API key. CBM Lens is the optional visual companion, currently in open beta (v0.0.1); its optional browser model supports on-device code chat after an opt-in download.

## Capabilities

- Code exploration: symbol and structural search, BM25 text search, semantic search, source snippets and file outlines, call paths, data flow, architecture summaries, hotspots, cycles, and coverage diagnostics. The server registers 17 MCP tools.
- Semantic and similarity relationships: bundled embeddings support natural-language discovery; SEMANTICALLY_RELATED edges connect vocabulary-mismatch matches, and SIMILAR_TO edges expose near-clone candidates.
- Languages and formats: vendored parsers cover source, documentation, and configuration formats. Hybrid LSP adds semantic type resolution for Python, TypeScript/JavaScript/JSX/TSX, PHP, C#, Go, C/C++, Java, Kotlin, Rust, and Perl. Support and resolution depth differ by language; use the README language matrix and check_index_coverage for the indexed project.
- Documentation connected to code: Markdown, reStructuredText, and AsciiDoc sections; PDF text pages (no OCR); supported document and language doc-comment references to code; ADR metadata and supersession. Supported diagrams and OpenAPI descriptions contribute entities, with format-specific reference-resolution limits.
- Infrastructure and deployment: Docker/Compose, Kubernetes, Helm, Kustomize, Terraform/OpenTofu, Terragrunt, Ansible, shell, Make, and supported CI relationships. Trace deployments, images, entrypoints, configuration keys, and environment-variable access back to source when inputs can be resolved statically.
- Cross-service and cross-repository relationships: supported HTTP, asynchronous messaging, gRPC, GraphQL, and tRPC links. Cross-repo analysis explicitly links projects indexed in the same store; missing links do not prove that no runtime relationship exists.
- Change impact: detect_changes maps diffs to affected symbols and investigates related tests with scope: "tests"; compare_graphs compares indexed snapshots.
- Smart CI (experimental): repository-specific changed-file rules select CI jobs; test-impact select combines static reachability, changed tests, and admitted per-test coverage to select tests. Narrowing supports C/C++ family code with supported test-harness conventions and an explicitly configured runner. Missing or doubtful evidence broadens the selection or runs all tests. Other indexed languages are not automatically supported for test narrowing. test-impact publish creates the baseline graph-and-coverage bundle. Start in shadow mode; consult the guide for version requirements.
- Architecture decisions and runtime evidence: manage_adr stores the project's decision record; ingest_traces adds separately collected observed call relationships alongside static analysis.
- CBM Lens open beta (v0.0.1): optional visual companion to the MCP server, with a local browser interface with source exploration, Galaxy graph navigation, architecture and behavior views, ADR editing, index-coverage diagnostics, daemon/system information, and optional local code chat. Static graph animations are not live execution traces. Activity overlays are experimental source-build features, not a standard-release promise.
- Agent integrations: 45 automatic or conditional client surfaces. Installation configures supported MCP entries, durable instructions, skills, Scout/Verify/Auditor profiles, and applicable session, compaction, search, and read hooks. Coverage and activation vary by client and platform. Hooks provide context without blocking or replacing the user's tool call; some clients require trust or use parent-agent handoff instead.
- Persistent operation: a per-account daemon coordinates sessions and indexing. Existing indexes can stay synchronized during eligible active MCP sessions; initial auto-indexing is separately opt-in. Optional team graph artifacts share a baseline while each user keeps a local query database.
- Token-efficient retrieval: targeted graph queries, focused source snippets, and compact, paged results can reduce token use and repeated file reads during code exploration.
- Distribution: native C backend, MIT license, standard native releases for macOS, Linux, and Windows include CBM Lens; Linux portable archives provide static builds. Nix and development source builds can omit the UI. Package-manager options and release verification are documented in the README.

## Client coverage

- Supported agents: 45 automatic/conditional client surfaces (39 automatically detected + 6 conditional/explicit): Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Continue / cn, Visual Studio, TRAE, Roo Code, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob IDE, IBM Bob Shell, Pochi, Pi, Oh My Pi (omp), and Sourcegraph Cody.

## Measurement and limits

A March 2026 preprint across 31 real-world repositories reports 10× lower token use and 2.1× fewer tool calls compared with file-by-file exploration. These historical backend measurements cover the study’s workloads and agent setup. The paper documents the full evaluation; the README Performance section links to the research and earlier measurements.

Graph evidence depends on index generation, freshness, and exact path/scope coverage. Partial, skipped, excluded, stale, or unknown coverage requires source verification, especially for absence claims. Infrastructure relationships are static, and test-selection support is narrower than language-indexing support.

## Links

- Homepage: https://deusdata.github.io/codebase-memory-mcp/
- Source and feature reference: https://github.com/DeusData/codebase-memory-mcp
- Latest release: https://github.com/DeusData/codebase-memory-mcp/releases/latest
- Quick start and installation: https://github.com/DeusData/codebase-memory-mcp#quick-start
- Documentation indexing: https://github.com/DeusData/codebase-memory-mcp#documentation-connected-to-code
- Infrastructure and deployment: https://github.com/DeusData/codebase-memory-mcp#infrastructure-and-deployment
- CBM Lens: https://github.com/DeusData/codebase-memory-mcp#cbm-lens-beta-code-visualization-and-source-navigation
- Agent integrations: https://github.com/DeusData/codebase-memory-mcp#multi-agent-support
- Smart CI guide: https://github.com/DeusData/codebase-memory-mcp/blob/main/docs/SMART_CI.md
- Configuration: https://github.com/DeusData/codebase-memory-mcp/blob/main/docs/CONFIGURATION.md
- MCP tools: https://github.com/DeusData/codebase-memory-mcp#mcp-tools
- Token savings and evidence: https://deusdata.github.io/codebase-memory-mcp/#token-savings
- Performance and research: https://github.com/DeusData/codebase-memory-mcp#performance
- Benchmark report: https://github.com/DeusData/codebase-memory-mcp/blob/main/docs/BENCHMARK.md
- Research paper: https://arxiv.org/abs/2603.27277
- Security and release verification: https://github.com/DeusData/codebase-memory-mcp#security

---

### Robots (`docs/robots.txt`)

# codebase-memory-mcp — all crawlers welcome, including AI answer engines.
User-agent: *
Allow: /

# Explicitly allow AI / LLM crawlers used by answer engines.
User-agent: GPTBot
Allow: /

User-agent: OAI-SearchBot
Allow: /

User-agent: ChatGPT-User
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: Claude-Web
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: Google-Extended
Allow: /

User-agent: Applebot-Extended
Allow: /

User-agent: Bingbot
Allow: /

Sitemap: https://deusdata.github.io/codebase-memory-mcp/sitemap.xml

---

### Graph Ui Docs Adr 0001 Modellwahl (`graph-ui/docs/adr/0001-modellwahl.md`)

# ADR 0001: Optional local inference and model evidence

## Decision

CBM Lens offers optional browser inference. Weights are downloaded only after the
user opts in; they are not embedded in the executable. Model selection remains a
user choice. Browser model IDs, fixed revisions, allowed files and licenses live in
`src/browser-ai/model-policy.ts`; [THIRD_PARTY.md](../../THIRD_PARTY.md) documents them.

The older GGUF sidecar catalogue remains supported by local scripts and its recorded
evaluation. It is separate from the browser ONNX catalogue. Its measurements do not
predict browser performance, another machine's performance or a model's general rank.

## Sidecar model sources

The six Q4_K_M candidates below were recorded in August 2026. Class A used a 3072-token
context and class B used 8192 tokens. Exact download sizes refer to those evaluated
files, not runtime memory. Checksums are retained in `models/SHA256SUMS`.

| Model | Class | Download source | Recorded size | License recorded for the base model |
| --- | --- | --- | --- | --- |
| Qwen3.5-2B | A | [unsloth/Qwen3.5-2B-GGUF](https://huggingface.co/unsloth/Qwen3.5-2B-GGUF) | 1280835840 Bytes | Apache 2.0 |
| LFM2.5-1.2B-Instruct | A | [LiquidAI/LFM2.5-1.2B-Instruct-GGUF](https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct-GGUF) | 730895168 Bytes | LFM Open License v1.0; commercial terms apply at $10M annual revenue |
| MiniCPM5-1B | A | [openbmb/MiniCPM5-1B-GGUF](https://huggingface.co/openbmb/MiniCPM5-1B-GGUF) | 688065920 Bytes | Apache 2.0 |
| Qwen2.5-Coder-1.5B-Instruct | A | [Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF](https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF) | 1117320768 Bytes | Apache 2.0 |
| Qwen3.5-4B | B | [unsloth/Qwen3.5-4B-MTP-GGUF](https://huggingface.co/unsloth/Qwen3.5-4B-MTP-GGUF) | 2834975040 Bytes | Apache 2.0 |
| Gemma 4 E4B-it | B | [unsloth/gemma-4-E4B-it-GGUF](https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF) | 4977171584 Bytes | Apache 2.0 |

Review current model-card terms before downloading or redistribution. The recorded
LFM license allows free commercial use below $10M annual revenue; use beyond that
threshold requires separate consideration. No evaluation result overrides licensing.

## Evaluation limits

`verification/w5/eval.json` records 44 fixture questions, including 23 in German,
with temperature 0 and seed 42. The figures below summarize that saved run. Citation
compliance measures the evaluation's citation rule, not factual correctness.

| Model | Class | Pass rate | Citation compliance | Mean tokens/s, rounded |
| --- | --- | --- | --- | --- |
| Qwen3.5-2B | A | 0.682 | 0.932 | 86 |
| LFM2.5-1.2B | A | 0.295 | 0.432 | 110 |
| MiniCPM5-1B | A | 0.250 | 0.545 | 171 |
| Qwen2.5-Coder-1.5B | A | 0.227 | 0.705 | 84 |
| Qwen3.5-4B | B | 0.818 | 0.955 | 38 |
| gemma-4-E4B | B | 0.841 | 1.000 | 32 |

The recorded class recommendations were Qwen3.5-2B and gemma-4-E4B, with pass-rate
and citation thresholds of 0.6 and 0.9. They are recommendations, not installed
defaults. A reported repeat run has no separate saved artifact and is not independent
reproduction evidence. The original record does not measure unscorable citations;
missing measurements must not be displayed as zero.

## Engine boundary

The sidecar evaluation used llama.cpp on Apple Silicon with Metal and GGUF files.
Its source/build record is in [runtime provenance](../../vendor/llama/HERKUNFT.md).
`llm/start.sh` also supports a local model cache and OpenAI-compatible transport;
the class-specific script options reproduce the recorded evaluation.

Browser inference uses a different runtime and format. Compare actual device and
workload behavior before drawing performance conclusions, especially prompt prefill,
GPU memory limits and large-file handling. Relevant engine sources retained from
the original investigation are [llama.cpp](https://github.com/ggml-org/llama.cpp),
[wllama](https://github.com/ngxson/wllama), [WebLLM](https://webllm.mlc.ai/docs/),
[Transformers.js](https://github.com/huggingface/transformers.js),
[WebGPU limits](https://developer.mozilla.org/en-US/docs/Web/API/GPUSupportedLimits),
and the [ggml WebGPU study](https://arxiv.org/html/2605.20706v1).
These references explain the formats and tradeoffs; the sidecar timings above are
not browser benchmarks.

---

--- METRICS ---
- Files Extracted: 13
- Estimated Token Budget: ~71732 tokens
- Canonical Reference: https://codewiki.google/github.com/DeusData/codebase-memory-mcp
