Merge branch 'main' into feat/path-allow-broad-too-shallow-override

This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-20 19:45:03 +02:00
commit cf1b443a16
35 files changed
+1791 -956

No files matched your search

+72 -29
View File
@@ -1,4 +1,4 @@
# codebase-memory-mcp: local MCP server for code search, lower token usage and visualization
# codebase-memory-mcp: local code intelligence for developers and AI agents
[![GitHub Release](https://img.shields.io/github/v/release/DeusData/codebase-memory-mcp?style=flat&color=blue)](https://github.com/DeusData/codebase-memory-mcp/releases/latest)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
@@ -15,19 +15,13 @@
[![VirusTotal](https://img.shields.io/badge/VirusTotal-scanned_every_release-brightgreen?logo=virustotal)](https://github.com/DeusData/codebase-memory-mcp/releases/latest)
[![arXiv](https://img.shields.io/badge/arXiv-2603.27277-b31b1b?logo=arxiv)](https://arxiv.org/abs/2603.27277)
**Search your code, follow call graphs, and explore software architecture in your browser.** codebase-memory-mcp is a local [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that gives Claude Code, Codex, and other coding agents a persistent code knowledge graph. It indexes functions, classes, imports, HTTP routes, and their relationships across [162 languages](#language-support). [Hybrid LSP](#hybrid-lsp) adds semantic type resolution for **Python, TypeScript, JavaScript, JSX/TSX, PHP, C#, Go, C, C++, Java, Kotlin, Rust, and Perl**.
**Understand your repository, from source code to the systems around it.** codebase-memory-mcp 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.
**About 120× fewer tokens** in the project's reported five-query comparison: ~3,400 vs ~412,000 with file-by-file exploration. Published backend benchmarks also report **Linux kernel indexing in ~3 minutes** (28M lines of code) and **Cypher relationship queries under 1 ms** on an Apple M3 Pro. [Benchmark details](#performance).
Give your coding agent structural context through [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), query the graph from the CLI, or explore it yourself in **CBM Lens Beta**. Lens brings together an IDE-style source reader, architecture maps, the Galaxy graph, coverage diagnostics, architecture decisions, and optional local code chat.
**CBM Lens Beta helps you understand unfamiliar code and stay in control as it grows.** Use it to get up to speed in a new codebase, tackle rescue engineering on an inherited project, or make sense of 50,000 lines of AI-generated code from a single day. Its IDE-style source reader, local code chat, 3D architecture map, and Galaxy graph let you see how the code fits together and follow dependencies and call paths back to the source. Use that context to review changes, investigate fragile areas, and keep code quality high. Architecture decisions stay alongside the project's index.
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.
The native C backend exposes 17 MCP tools and supports 45 automatic or conditional client integrations. Indexing and graph queries need no hosted service or API key.
> **CBM Lens Beta is new on `main`:** it arrived with [PR #2068](https://github.com/DeusData/codebase-memory-mcp/pull/2068) and is not part of the current v0.11.0 release, whose installer still ships the previous graph UI. To run the interface shown below now, [build from source](#build-from-source).
[Quick start](#quick-start) · [Explore CBM Lens](#cbm-lens-beta-code-visualization-and-source-navigation) · [Chat with your codebase locally](#chat-with-your-codebase-locally) · [MCP tools](#mcp-tools) · [Build from source](#build-from-source)
> **Research:** The [preprint](https://arxiv.org/abs/2603.27277) compares agents across 31 repositories, reporting **10× lower token use** and **2.1× fewer tool calls**, with answer-quality scores of 83% for CBM versus 92% for file-by-file exploration. These backend results are separate from the five-query comparison above and from CBM Lens.
[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.
@@ -62,23 +56,23 @@ These captures show CBM Lens Beta on 12–13 October 2026, using public source f
## Why codebase-memory-mcp
- **Indexing performance**: Linux kernel (28M LOC, 75K files) in 3 minutes. RAM-first pipeline: LZ4 compression, in-memory SQLite, fused Aho-Corasick pattern matching. Memory released after indexing.
- **Plug and play**: native executable plus authenticated release-owned assets for macOS (arm64/amd64), Linux (arm64/amd64), and Windows (arm64/amd64). The native install needs no Docker, language runtime, or API keys. Download → `install` → restart agent → done.
- **162 languages**: vendored tree-sitter grammars compiled into the binary. No separate parser installation.
- **About 120× fewer tokens in the reported five-query comparison**: ~3,400 tokens vs ~412,000 via file-by-file search. See [Performance](#performance) for the scope and sources.
- **45 supported automatic/conditional client surfaces**: `install` configures detected clients and safely activates conditional clients only when their documented platform, marker, or explicit existing config path is present. See [Multi-Agent Support](#multi-agent-support) for the complete matrix and manual/UI-only boundaries.
- **CBM Lens Beta**: read source, chat about selected code with a local model, explore architecture and call graphs, inspect coverage gaps, and edit architecture decision records in your browser.
- **Infrastructure-as-code indexing**: Dockerfiles, container images, docker compose, Kubernetes manifests, and Kustomize overlays indexed as graph nodes with cross-references. `Resource` nodes for K8s kinds and compose services, `Image` nodes joining the images that are built and run, `Module` nodes for Kustomize overlays with `IMPORTS` edges to referenced resources; a Service walks to the program its container runs.
- **17 MCP tools**: search, trace, architecture, impact analysis, targeted index-coverage checks, Cypher queries, dead code detection, cross-service HTTP linking, ADR management, and more.
- **Find the right code with context**: combine symbol, text, and semantic search with callers, dependencies, source snippets, and file outlines. Reduce repeated file exploration; 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). For CBM Lens Beta shown above, [build from source](#build-from-source) until the next release.
**Install the latest release** (macOS / Linux):
```bash
curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash
```
The released graph UI is included in the standard installer.
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
@@ -136,7 +130,19 @@ Restart your coding agent. Say **"Index this project"**: done.
The `install` command automatically strips macOS quarantine attributes and ad-hoc signs the binary; no manual `xattr`/`codesign` needed.
</details>
The `install` command auto-detects installed coding agents and configures their documented MCP entries plus durable instructions, skills, and lifecycle hooks where supported.
### 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
@@ -162,10 +168,10 @@ The ordinary `cli` mode runs one command locally, but it is not disconnected fro
### CBM Lens Beta: code visualization and source navigation
CBM Lens Beta is the browser interface built into the binary. It builds on [CodeAtlasWeb](graph-ui/README.md) by [Bernhard Jackiewicz](https://github.com/BernhardJackiewicz) (design and tracking in #1964). It runs on loopback; the only request that leaves your machine is the optional model download from Hugging Face, and only after you start it. Each panel names where its facts come from, and what the index did not record is shown as a gap. Start the local server:
CBM Lens Beta is the browser interface included in standard native releases and UI-enabled source builds. 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
./build/c/codebase-memory-mcp daemon start --open --port=9749
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.
@@ -246,7 +252,7 @@ Enable automatic indexing on MCP session start:
codebase-memory-mcp config set auto_index true
```
When enabled, new projects are indexed automatically on first connection. Previously-indexed projects are registered with the background watcher for ongoing git-based change detection. Configurable file limit: `config set auto_index_limit 50000`.
`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.
@@ -299,9 +305,10 @@ The install script placed beside the binary is **reported, not deleted**: uninst
### 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 architectural decisions across sessions
- **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
- **Git diff impact mapping**: `detect_changes` maps uncommitted changes to affected symbols with risk classification
- **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`
@@ -312,6 +319,27 @@ The install script placed beside the binary is **reported, not deleted**: uninst
- **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
@@ -323,6 +351,21 @@ The install script placed beside the binary is **reported, not deleted**: uninst
- **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
@@ -426,7 +469,7 @@ Published backend benchmarks on Apple M3 Pro. These are separate from the CBM Le
**RAM-first pipeline**: All indexing runs in memory (LZ4 HC compressed read, in-memory SQLite, single dump at end). Memory is released back to the OS after indexing completes.
**Token efficiency**: The project's [original README report](https://github.com/DeusData/codebase-memory-mcp/blob/4ff6feeab639c4668da0f491930326ed5ec63093/README.md#performance) lists ~3,400 tokens for five structural queries via codebase-memory-mcp versus ~412,000 via file-by-file exploration. That is approximately **120× fewer tokens**, or a **99.2% reduction**, for that comparison. The separate [31-repository study](https://arxiv.org/html/2603.27277v1#S4.SS1) reports a 10× token reduction. Its [system-performance measurements](https://arxiv.org/html/2603.27277v1#S4.SS3) include Linux kernel indexing in about 3 minutes and Cypher relationship queries under 1 ms on an Apple M3 Pro.
**Token efficiency, within the reported workloads**: The project's [original README report](https://github.com/DeusData/codebase-memory-mcp/blob/4ff6feeab639c4668da0f491930326ed5ec63093/README.md#performance) lists ~3,400 tokens for five structural queries via codebase-memory-mcp versus ~412,000 via file-by-file exploration: approximately **120× fewer tokens** for that comparison. The separate [31-repository preprint](https://arxiv.org/html/2603.27277v1#S4.SS1) reports **10× lower token use** and **2.1× fewer tool calls**, with answer-quality scores of **83% for CBM versus 92% for file-by-file exploration**. These are scoped backend measurements, not a promised saving for every task or a benchmark of CBM Lens. Actual savings depend on the question, repository, agent, and required verification. The study's [system-performance measurements](https://arxiv.org/html/2603.27277v1#S4.SS3) include the Linux indexing and Cypher timings above.
To measure comparable quality, latency, and agent-efficiency metrics on your own workload, see [Measuring quality, latency, and agent savings](docs/MEASURING_SAVINGS.md). Exact reproduction of the figures above requires the original inputs and raw artifacts.
@@ -627,7 +670,7 @@ Restart your agent. Verify with `/mcp`; you should see `codebase-memory-mcp` wit
## Multi-Agent Support
`install` configures 45 client surfaces: 39 detected automatically and 6
`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,
+377 -776
View File
File diff suppressed because it is too large. Load diff
+33 -14
View File
@@ -1,26 +1,45 @@
# codebase-memory-mcp
> An open-source Model Context Protocol (MCP) server that indexes a codebase into a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. AI coding agents query the graph instead of reading files one by one, answering structural questions with roughly 120x fewer tokens (~3,400 vs ~412,000 across five structural queries). Parses 162 languages via vendored tree-sitter grammars, with Hybrid LSP semantic type resolution for Python, TypeScript/JavaScript, PHP, C#, Go, C/C++, Java, Kotlin, and Rust. It also generates semantic graph edges (SEMANTICALLY_RELATED for vocabulary-mismatch matches, SIMILAR_TO for near-clone detection) and supports semantic vector search via bundled nomic-embed-code embeddings compiled into the native executable — no API key, no Ollama, no Docker. Ships a native executable with an authenticated integration asset; the UI variant adds one content-addressed frontend pack. All processing is local — the embeddings run on-device; there is no embedded LLM and no API key.
> Local code intelligence for developers and AI agents. Connect code, documentation, configuration, and infrastructure in a persistent knowledge graph. Search for a feature, follow callers and dependencies, trace services and deployments, inspect change impact, and explore the evidence in CBM Lens. Access the same backend through MCP or the CLI. Indexing, graph queries, and semantic embeddings run locally without a hosted service or API key; Lens optionally downloads a browser model after opt-in for on-device code chat.
## Key facts
## 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 Beta: 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.
- 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
- License: MIT, open source.
- Languages: 158 (158 vendored tree-sitter grammars compiled into the binary).
- Hybrid LSP type resolution: 9 language families (Python, TypeScript/JavaScript/JSX/TSX, PHP, C#, Go, C/C++, Java, Kotlin, Rust) — a lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers including tsserver, pyright, gopls, Roslyn, Eclipse JDT, and rust-analyzer.
- MCP tools: 15 (search_graph incl. semantic_query vector search, trace_path (alias: trace_call_path), check_index_coverage, query_graph (Cypher), detect_changes, get_architecture, get_code_snippet, manage_adr, and more).
- Semantic search: natural-language code discovery via bundled nomic-embed-code embeddings (768-dim, compiled into the binary); 11-signal combined scoring; fully local, no API key.
- Semantic & similarity edges: SEMANTICALLY_RELATED (vocabulary-mismatch matches) and SIMILAR_TO (MinHash + LSH near-clone / duplicate detection).
- Cross-repo intelligence: CROSS_* edges link nodes across multiple repos indexed in one store; multi-galaxy 3D layout and cross-repo architecture summary.
- Cross-service linking: HTTP route ↔ call-site matching, plus gRPC/GraphQL/tRPC detection and pub/sub channels (EMITS/LISTENS_ON for Socket.IO, EventEmitter, generic buses).
- 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.
- Agent profiles: documented custom-agent formats receive Scout (fast/provisional), Verify (default/task-directed), and Auditor (bounded/full verification) definitions. Every direct tier checks exact path/scope coverage with check_index_coverage and falls back to source for flagged gaps; unsafe child-MCP formats use explicit parent handoff. Kiro and Junie use positive-allowlist Scout/Analysis server profiles (7/11 tools); Qoder combines named-server selection with exact tier-specific MCP tool IDs, while Factory uses exact registered IDs without additive whole-server exposure. Foreign Junie aliases are preserved and force parent handoff.
- Performance: Linux kernel (28M LOC, 75K files) full index in 3 minutes → 4.81M nodes, 7.72M edges; Cypher queries in under 1ms.
- Distribution: verified runtime set (native executable plus authenticated release-owned assets); fully static Linux builds are available in the `-portable` archives. Also available through npm, PyPI, Homebrew, Scoop, AUR, and `go install`.
## Measurement and limits
Published performance and token comparisons apply to specific workloads, hardware, and agent setups, not a universal percentage saving. The README Performance section preserves the historical comparison and research quality tradeoff. Check index generation, freshness, and exact path/scope coverage before relying on graph results, especially absence claims; read source when coverage is partial, skipped, excluded, stale, or unknown. Infrastructure relationships are static, and test-selection support is narrower than language-indexing support.
## Links
- Homepage: https://deusdata.github.io/codebase-memory-mcp/
- Source code (GitHub): https://github.com/DeusData/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
- Performance and research limits: 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
+161 -40
View File
@@ -59,6 +59,10 @@ void cbm_ts_full_registry_builds_reset(void) {
* constructs it does not model. -1 = unlimited (no entry point armed it). */
static _Thread_local long g_ts_type_budget = -1;
static _Thread_local bool g_ts_type_budget_warned;
#ifdef CBM_ENABLE_TEST_SEAMS
static _Thread_local int g_ts_max_member_lookup_steps;
static _Thread_local int g_ts_max_method_lookup_steps;
#endif
static void ts_type_budget_reset(size_t source_len) {
const char *e = cbm_runtime_getenv("CBM_TS_TYPE_BUDGET");
@@ -170,6 +174,12 @@ long cbm_ts_lsp_test_budget_remaining(void) {
bool cbm_ts_lsp_test_budget_warned(void) {
return g_ts_type_budget_warned;
}
int cbm_ts_lsp_test_max_member_lookup_steps(void) {
return g_ts_max_member_lookup_steps;
}
int cbm_ts_lsp_test_max_method_lookup_steps(void) {
return g_ts_max_method_lookup_steps;
}
#endif
#define TS_LSP_MAX_EVAL_DEPTH 64
@@ -190,6 +200,10 @@ static const CBMType *type_of_identifier(TSLSPContext *ctx, const char *name);
static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name);
static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *recv,
const char *method_name);
typedef struct TSLookupState TSLookupState;
static const CBMRegisteredFunc *lookup_method_at(TSLSPContext *ctx, const CBMType *recv,
const char *method_name, int depth,
TSLookupState *state);
static char *node_text(TSLSPContext *ctx, TSNode node);
// Collect a node's children into an arena array via a single O(n) cursor pass.
@@ -1569,28 +1583,77 @@ static const char *builtin_wrapper_class(const char *builtin_name) {
}
// Look up a property `name` on a receiver type. Returns the property type or UNKNOWN.
static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType *recv,
const char *name);
#define TS_LSP_MAX_MEMBER_DEPTH 64
#define TS_LSP_INLINE_VISITED_TYPES 16
/* Depth-guarded entry: member lookup recurses through wrapper classes, union
* members and registered-type expansion; cyclic type graphs in real-world TS
* (microsoft/TypeScript reallyLargeFile.ts) recursed without bound — SIGBUS
* stack overflow under endless lookup_member_type frames. Past the cap the
* member resolves as unknown — graceful degradation, not a crash. */
static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name) {
if (!ctx || ctx->member_depth >= TS_LSP_MAX_MEMBER_DEPTH)
return cbm_type_unknown();
ctx->member_depth++;
const CBMType *r = lookup_member_type_inner(ctx, recv, name);
ctx->member_depth--;
return r;
typedef struct {
const char *qn;
int depth;
} TSVisitedType;
struct TSLookupState {
TSVisitedType inline_visited[TS_LSP_INLINE_VISITED_TYPES];
TSVisitedType *visited;
int visited_count;
int visited_capacity;
#ifdef CBM_ENABLE_TEST_SEAMS
int steps;
#endif
};
/* A named type needs another visit only when reached along a shallower path.
* Grow the visited table rather than silently dropping entries at a fixed cap:
* the depth bound then limits each named type to at most 64 admitted visits. */
static bool ts_lookup_visit_named(CBMArena *arena, TSLookupState *state, const char *qn,
int depth) {
for (int i = 0; i < state->visited_count; i++) {
if (strcmp(state->visited[i].qn, qn) == 0) {
if (state->visited[i].depth <= depth)
return false;
state->visited[i].depth = depth;
return true;
}
}
if (!state->visited) {
state->visited = state->inline_visited;
state->visited_capacity = TS_LSP_INLINE_VISITED_TYPES;
} else if (state->visited_count == state->visited_capacity) {
int capacity = state->visited_capacity * 2;
TSVisitedType *larger =
(TSVisitedType *)cbm_arena_alloc(arena, (size_t)capacity * sizeof(*larger));
if (!larger) {
fprintf(stderr, "[tslsp] lookup visited allocation failed\n");
return false;
}
memcpy(larger, state->visited, (size_t)state->visited_count * sizeof(*larger));
state->visited = larger;
state->visited_capacity = capacity;
}
state->visited[state->visited_count++] = (TSVisitedType){.qn = qn, .depth = depth};
return true;
}
static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType *recv,
const char *name) {
if (!ctx || !recv || !name)
static const CBMType *lookup_member_type_at(TSLSPContext *ctx, const CBMType *recv,
const char *name, int depth, TSLookupState *state);
/* Depth and shallower-only named-type visits bound work on cyclic inheritance. */
static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name) {
TSLookupState state = {0};
const CBMType *result = lookup_member_type_at(ctx, recv, name, 0, &state);
#ifdef CBM_ENABLE_TEST_SEAMS
if (state.steps > g_ts_max_member_lookup_steps)
g_ts_max_member_lookup_steps = state.steps;
#endif
return result;
}
static const CBMType *lookup_member_type_at(TSLSPContext *ctx, const CBMType *recv,
const char *name, int depth, TSLookupState *state) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (state)
state->steps++;
#endif
if (!ctx || !recv || !name || !state || depth >= TS_LSP_MAX_MEMBER_DEPTH)
return cbm_type_unknown();
const CBMType *base = simplify_type(ctx, recv);
if (!base)
@@ -1604,7 +1667,7 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
const char *wrap = builtin_wrapper_class(base->data.builtin.name);
if (wrap) {
const CBMType *wrapped = cbm_type_named(ctx->arena, wrap);
return lookup_member_type(ctx, wrapped, name);
return lookup_member_type_at(ctx, wrapped, name, depth + 1, state);
}
return cbm_type_unknown();
}
@@ -1659,7 +1722,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
// first hit instead of building a union of results.)
if (base->data.union_type.members) {
for (int i = 0; i < base->data.union_type.count; i++) {
const CBMType *m = lookup_member_type(ctx, base->data.union_type.members[i], name);
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *m = lookup_member_type_at(ctx, base->data.union_type.members[i],
name, depth + 1, state);
if (!cbm_type_is_unknown(m))
return m;
}
@@ -1670,7 +1736,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
if (base->kind == CBM_TYPE_INTERSECTION) {
if (base->data.union_type.members) {
for (int i = 0; i < base->data.union_type.count; i++) {
const CBMType *m = lookup_member_type(ctx, base->data.union_type.members[i], name);
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *m = lookup_member_type_at(ctx, base->data.union_type.members[i],
name, depth + 1, state);
if (!cbm_type_is_unknown(m))
return m;
}
@@ -1682,6 +1751,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
return cbm_type_unknown();
const char *recv_qn = base->data.named.qualified_name;
if (!recv_qn)
return cbm_type_unknown();
if (!ts_lookup_visit_named(ctx->arena, state, recv_qn, depth))
return cbm_type_unknown();
const CBMRegisteredType *rt = cbm_registry_resolve_alias(ctx->registry, recv_qn);
if (!rt) {
rt = cbm_registry_lookup_type(ctx->registry, recv_qn);
@@ -1711,8 +1784,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
// Walk extends/implements.
if (rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *parent = cbm_type_named(ctx->arena, rt->embedded_types[i]);
const CBMType *m = lookup_member_type(ctx, parent, name);
const CBMType *m = lookup_member_type_at(ctx, parent, name, depth + 1, state);
if (!cbm_type_is_unknown(m))
return m;
}
@@ -1795,9 +1870,6 @@ static const CBMType *eval_indexed_access(TSLSPContext *ctx, const CBMType *obj,
return cbm_type_unknown();
}
static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBMType *recv,
const char *method_name);
/* Depth-guarded entry, same contract as lookup_member_type: the method walk
* recurses through wrapper classes, union members and the extends/implements
* chain. A cyclic chain (#1743: the Effect idiom `class Service extends
@@ -1806,18 +1878,24 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
* cap the method is simply not found. */
static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *recv,
const char *method_name) {
if (!ctx || ctx->method_depth >= TS_LSP_MAX_MEMBER_DEPTH)
return NULL;
ctx->method_depth++;
const CBMRegisteredFunc *f = lookup_method_inner(ctx, recv, method_name);
ctx->method_depth--;
return f;
TSLookupState state = {0};
const CBMRegisteredFunc *result = lookup_method_at(ctx, recv, method_name, 0, &state);
#ifdef CBM_ENABLE_TEST_SEAMS
if (state.steps > g_ts_max_method_lookup_steps)
g_ts_max_method_lookup_steps = state.steps;
#endif
return result;
}
// Look up a method on a receiver type — returns the registered func.
static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBMType *recv,
const char *method_name) {
if (!ctx || !recv || !method_name)
/* Depth and shallower-only named-type visits bound cyclic inheritance work. */
static const CBMRegisteredFunc *lookup_method_at(TSLSPContext *ctx, const CBMType *recv,
const char *method_name, int depth,
TSLookupState *state) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (state)
state->steps++;
#endif
if (!ctx || !recv || !method_name || !state || depth >= TS_LSP_MAX_MEMBER_DEPTH)
return NULL;
const CBMType *base = simplify_type(ctx, recv);
if (!base)
@@ -1831,7 +1909,7 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
const char *wrap = builtin_wrapper_class(base->data.builtin.name);
if (wrap) {
const CBMType *wrapped = cbm_type_named(ctx->arena, wrap);
return lookup_method(ctx, wrapped, method_name);
return lookup_method_at(ctx, wrapped, method_name, depth + 1, state);
}
return NULL;
}
@@ -1860,8 +1938,10 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
if (base->kind == CBM_TYPE_UNION || base->kind == CBM_TYPE_INTERSECTION) {
if (base->data.union_type.members) {
for (int i = 0; i < base->data.union_type.count; i++) {
const CBMRegisteredFunc *f =
lookup_method(ctx, base->data.union_type.members[i], method_name);
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMRegisteredFunc *f = lookup_method_at(ctx, base->data.union_type.members[i],
method_name, depth + 1, state);
if (f)
return f;
}
@@ -1872,6 +1952,11 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
if (base->kind != CBM_TYPE_NAMED)
return NULL;
const char *recv_qn = base->data.named.qualified_name;
if (!recv_qn)
return NULL;
if (!ts_lookup_visit_named(ctx->arena, state, recv_qn, depth))
return NULL;
const CBMRegisteredFunc *f =
cbm_registry_lookup_method_aliased(ctx->registry, recv_qn, method_name);
@@ -1882,8 +1967,11 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, recv_qn);
if (rt && rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *parent = cbm_type_named(ctx->arena, rt->embedded_types[i]);
const CBMRegisteredFunc *pf = lookup_method(ctx, parent, method_name);
const CBMRegisteredFunc *pf =
lookup_method_at(ctx, parent, method_name, depth + 1, state);
if (pf)
return pf;
}
@@ -3757,6 +3845,30 @@ static void process_function_body(TSLSPContext *ctx, TSNode func_node, const cha
ctx->current_scope = saved_scope;
}
static void process_top_level_variable_arrows(TSLSPContext *ctx, TSNode declaration) {
if (!ctx || ts_node_is_null(declaration) || !ctx->module_qn)
return;
uint32_t count = ts_node_named_child_count(declaration);
for (uint32_t i = 0; i < count; i++) {
TSNode declarator = ts_node_named_child(declaration, i);
if (!node_kind_is(declarator, "variable_declarator"))
continue;
TSNode name = ts_node_child_by_field_name(declarator, "name", TS_LSP_FIELD_LEN("name"));
TSNode value = ts_node_child_by_field_name(declarator, "value", TS_LSP_FIELD_LEN("value"));
if (!node_kind_is(name, "identifier") || !node_kind_is(value, "arrow_function"))
continue;
char *function_name = node_text(ctx, name);
if (!function_name)
continue;
const char *function_qn =
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, function_name);
process_function_body(ctx, value, function_qn, NULL);
}
}
void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
if (!ctx || ts_node_is_null(root))
return;
@@ -3782,7 +3894,9 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
TSNode child = kids[i];
const char *kind = ts_node_type(child);
if (strcmp(kind, "function_declaration") == 0) {
if (strcmp(kind, "lexical_declaration") == 0 || strcmp(kind, "variable_declaration") == 0) {
process_top_level_variable_arrows(ctx, child);
} else if (strcmp(kind, "function_declaration") == 0) {
TSNode name = ts_node_child_by_field_name(child, "name", TS_LSP_FIELD_LEN("name"));
if (ts_node_is_null(name) || !ctx->module_qn)
continue;
@@ -3821,6 +3935,9 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
continue;
const char *fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fn);
process_function_body(ctx, inner, fqn, NULL);
} else if (strcmp(ik, "lexical_declaration") == 0 ||
strcmp(ik, "variable_declaration") == 0) {
process_top_level_variable_arrows(ctx, inner);
} else if (strcmp(ik, "class_declaration") == 0 ||
strcmp(ik, "internal_module") == 0) {
process_node(ctx, inner);
@@ -3838,6 +3955,10 @@ void ts_lsp_init(TSLSPContext *ctx, CBMArena *arena, const char *source, int sou
bool jsx_mode, bool dts_mode, CBMResolvedCallArray *out) {
if (!ctx)
return;
#ifdef CBM_ENABLE_TEST_SEAMS
g_ts_max_member_lookup_steps = 0;
g_ts_max_method_lookup_steps = 0;
#endif
memset(ctx, 0, sizeof(TSLSPContext));
ctx->arena = arena;
ctx->source = source;
+4 -7
View File
@@ -65,13 +65,6 @@ typedef struct {
// first completed eval; see TsEvalMemo in ts_lsp.c). Kills the exponential
// re-evaluation of shared subexpressions under overload resolution.
struct TsEvalMemo *eval_memo;
// Recursion guard for lookup_member_type: cyclic type graphs (mutually
// recursive unions/wrappers across registered types) otherwise recurse
// without bound — stack overflow on real repos.
int member_depth;
// Same guard for lookup_method: its extends/implements walk recursed
// without bound on a cyclic heritage chain (#1743).
int method_depth;
} TSLSPContext;
#ifdef CBM_ENABLE_TEST_SEAMS
@@ -79,6 +72,10 @@ typedef struct {
// for the calling thread, valid after a cbm_run_ts_lsp on the same thread.
long cbm_ts_lsp_test_budget_remaining(void);
bool cbm_ts_lsp_test_budget_warned(void);
// Maximum recursive calls in one member/method lookup during the last TS LSP run,
// including attempts rejected by the depth or visited-type guard.
int cbm_ts_lsp_test_max_member_lookup_steps(void);
int cbm_ts_lsp_test_max_method_lookup_steps(void);
#endif
// --- Initialization ---
+18 -21
View File
@@ -5,9 +5,9 @@
[![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/DeusData/codebase-memory-mcp/blob/main/LICENSE)
[![Platform](https://img.shields.io/badge/macOS_%7C_Linux_%7C_Windows-supported-lightgrey)](https://github.com/DeusData/codebase-memory-mcp/releases/latest)
**The fastest and most efficient code intelligence engine for AI coding agents.** Full-indexes an average repository in milliseconds, the Linux kernel (28M LOC, 75K files) in 3 minutes. Answers structural queries in under 1ms. This npm wrapper downloads, verifies, and caches the selected native runtime set: the executable, its authenticated integration asset, and—when requested—the content-addressed UI pack.
**Local code intelligence for developers and AI agents.** CBM connects source code, documentation and infrastructure in a persistent knowledge graph. Explore through MCP, the CLI or CBM Lens to find code, trace dependencies and deployments, and understand what a change affects.
High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST analysis across 162 languages — producing a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. 17 MCP tools. No hosted service or API key; this package requires Node.js to manage and launch the cached native runtime set. Plug and play across 45 automatic/conditional client surfaces.
This npm wrapper downloads, verifies and caches the native executable, its authenticated integration asset and the content-addressed Lens frontend pack. Node.js manages installation and launch; the graph is processed locally without a hosted service or API key. The installer offers 45 client integration targets, with capabilities that vary by client.
## Installation
@@ -25,14 +25,16 @@ Then configure your coding agents:
codebase-memory-mcp install
```
Restart your agent. Say **"Index this project"** — done.
Restart your agent and ask it to **"Index this project"**. Use `codebase-memory-mcp daemon start --open` to open CBM Lens. Supported integrations also install graph instructions, skills, agent profiles and context hooks; consult the full client matrix for differences.
## Why codebase-memory-mcp
- **Extreme indexing speed** — Linux kernel (28M LOC, 75K files) in 3 minutes. RAM-first pipeline with LZ4 compression and in-memory SQLite.
- **Connected repository knowledge** — source symbols and calls, documentation sections and references, configuration, infrastructure and cross-service links.
- **Plug and play** — verified native runtime sets for macOS (arm64/amd64), Linux (arm64/amd64), and Windows (arm64/amd64). No Docker or API keys; Node.js owns package download, cache repair, and launch.
- **162 languages** — vendored tree-sitter grammars compiled into the binary. Nothing to install, nothing that breaks.
- **120x fewer tokens** — 5 structural queries: ~3,400 tokens vs ~412,000 via file-by-file search.
- **Broad language and format support** — bundled parsers, with deeper semantic resolution for supported language families. See the [language reference](https://github.com/DeusData/codebase-memory-mcp#language-support) for coverage.
- **Focused context** — retrieve symbols, relationships and bounded source excerpts instead of repeatedly reading whole files. Savings depend on the task and workflow.
- **CBM Lens** — explore source, architecture, call paths, decisions and index coverage in a local browser workspace.
- **Smart CI** — experimental change-aware test selection for supported C/C++ test runners, using graph relationships and admitted per-test coverage. Missing evidence broadens execution. See the [Smart CI guide](https://github.com/DeusData/codebase-memory-mcp/blob/main/docs/SMART_CI.md).
- **45 supported automatic/conditional client surfaces** — `install` configures the appropriate MCP, durable-context, and documented hook surfaces without widening client permissions.
- **Detected automatically (39)** — 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, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob Shell, Pochi, Pi, and Oh My Pi (omp).
- **Conditional or explicit (6)** — Continue / cn, Visual Studio, TRAE, Roo Code, IBM Bob IDE, and Sourcegraph Cody. Bob IDE is touched only when `~/.bob/mcp.json` already exists.
@@ -68,30 +70,25 @@ Every MCP tool is also available directly from the command line:
```bash
codebase-memory-mcp cli index_repository '{"repo_path": "/path/to/repo"}'
codebase-memory-mcp cli search_graph '{"name_pattern": ".*Handler.*", "label": "Function"}'
codebase-memory-mcp cli trace_call_path '{"function_name": "main", "direction": "both"}'
codebase-memory-mcp cli get_architecture '{}'
codebase-memory-mcp cli list_projects '{}'
# Replace PROJECT with the indexed project name returned above.
codebase-memory-mcp cli search_graph '{"project":"PROJECT","name_pattern":".*Handler.*","label":"Function"}'
codebase-memory-mcp cli trace_path '{"project":"PROJECT","function_name":"main","direction":"both"}'
codebase-memory-mcp cli get_architecture '{"project":"PROJECT"}'
```
## MCP Tools
| Category | Tools |
|----------|-------|
| **Indexing** | `index_repository`, `list_projects`, `delete_project`, `index_status` |
| **Querying** | `search_graph`, `trace_call_path`, `detect_changes`, `query_graph` |
| **Analysis** | `get_architecture`, `get_graph_schema`, `get_code_snippet`, `search_code` |
| **Advanced** | `manage_adr`, `ingest_traces` |
| **Indexing and coverage** | `index_repository`, `list_projects`, `delete_project`, `index_status`, `check_index_coverage` |
| **Search and source** | `search_graph`, `search_code`, `get_code_snippet`, `get_file_outline` |
| **Relationships and changes** | `trace_path`, `query_graph`, `detect_changes`, `compare_graphs` |
| **Architecture and evidence** | `get_architecture`, `get_graph_schema`, `manage_adr`, `ingest_traces` |
## Performance
Benchmarked on Apple M3 Pro:
| Operation | Time |
|-----------|------|
| Linux kernel full index (28M LOC, 75K files) | 3 min |
| Django full index | ~6s |
| Cypher query | <1ms |
| Trace call path (depth=5) | <10ms |
Performance and context savings depend on the repository, query, machine and agent workflow. See the [scoped benchmark results](https://github.com/DeusData/codebase-memory-mcp#performance) and [methodology](https://github.com/DeusData/codebase-memory-mcp/blob/main/docs/BENCHMARK.md) for recorded measurements and limitations.
## Full Documentation
+1 -1
View File
@@ -293,7 +293,7 @@ build_mcpb_bundle() {
"name": "codebase-memory-mcp",
"display_name": "Codebase Memory",
"version": "$mcpb_version",
"description": "Codebase knowledge graph for AI agents — 162 languages, sub-ms queries, 99% fewer tokens.",
"description": "Local code intelligence connecting code, docs and infrastructure for developers and AI agents.",
"author": { "name": "DeusData", "url": "https://github.com/DeusData" },
"repository": { "type": "git", "url": "https://github.com/DeusData/codebase-memory-mcp" },
"homepage": "https://deusdata.github.io/codebase-memory-mcp/",
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.DeusData/codebase-memory-mcp",
"title": "Codebase Memory",
"description": "Codebase knowledge graph for AI agents \u2014 162 languages, sub-ms queries, 99% fewer tokens.",
"description": "Local code intelligence connecting code, docs and infrastructure for developers and AI agents.",
"repository": {
"url": "https://github.com/DeusData/codebase-memory-mcp",
"source": "github"
+26 -4
View File
@@ -435,29 +435,51 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
sb_append(&sb, "const BIN = '");
sb_append(&sb, bin);
sb_append(&sb, "';\n\n");
sb_append(&sb, "';\n"
"const AUGMENT_TIMEOUT_MS = 10000;\n\n");
/* hook-augment requires hook_event_name; its default dialect accepts
* Grep/Glob under PreToolUse, Read under PostToolUse, and the
* SessionStart lifecycle event — and emits the Claude JSON envelope, so
* the plugin unwraps additionalContext instead of pasting raw JSON into
* the tool output. Every failure path resolves to '' (fail open). */
* the tool output. Every failure path resolves to '' (fail open) —
* including a child that never exits: augment() is awaited inline in
* tool.execute.after, so a hung hook-augment would stall every
* grep/glob/read completion. The timer SIGKILLs the child and resolves
* ''; the promise keeps the first resolution, so the kill-triggered
* 'close' event cannot override it. Stream errors (EPIPE on a write
* still buffered when the child dies, spawn ENOENT) fire on child.stdin,
* not on the child, and would crash the host as an unhandled 'error'
* event — hence the stdin listener and the guarded end(). */
sb_append(&sb, "function augment(payload) {\n"
" return new Promise((resolve) => {\n"
" const child = spawn(BIN, ['hook-augment'], {\n"
" stdio: ['pipe', 'pipe', 'ignore'],\n"
" env: { ...process.env, CBM_LOG_LEVEL: 'error' },\n"
" });\n"
" // Stream errors fire on the stream, not the child.\n"
" child.stdin.on('error', () => {});\n"
" const timer = setTimeout(() => {\n"
" child.kill('SIGKILL');\n"
" child.stdout.destroy();\n"
" resolve('');\n"
" }, AUGMENT_TIMEOUT_MS);\n"
" let out = '';\n"
" child.stdout.on('data', (d) => (out += d.toString()));\n"
" child.on('error', () => resolve(''));\n"
" child.on('error', () => {\n"
" clearTimeout(timer);\n"
" resolve('');\n"
" });\n"
" child.on('close', () => {\n"
" clearTimeout(timer);\n"
" try {\n"
" const ctx = JSON.parse(out)?.hookSpecificOutput?.additionalContext;\n"
" resolve(typeof ctx === 'string' ? ctx : '');\n"
" } catch { resolve(''); }\n"
" });\n"
" child.stdin.end(JSON.stringify(payload));\n"
" try {\n"
" child.stdin.end(JSON.stringify(payload));\n"
" } catch { resolve(''); }\n"
" });\n"
"}\n\n");
+47 -34
View File
@@ -31,6 +31,8 @@ enum {
CBM_DAEMON_IPC_RETRY_INTERVAL_MS = 10,
CBM_DAEMON_IPC_COORDINATION_CLEANUP_MS = 500,
CBM_DAEMON_IPC_WINDOWS_RENDEZVOUS_RETRY_MS = 250,
CBM_DAEMON_IPC_RELEASE_ERROR = -1,
CBM_DAEMON_IPC_RELEASE_COMPLETE = 1,
};
/* Last private-namespace validation refusal, set at the exact failing check.
@@ -3988,19 +3990,19 @@ bool cbm_daemon_ipc_local_transition_begin_work(cbm_daemon_ipc_local_transition_
return true;
}
bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t **transition_io) {
int cbm_daemon_ipc_local_transition_try_release(cbm_daemon_ipc_local_transition_t **transition_io) {
if (!transition_io) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
cbm_daemon_ipc_local_transition_t *transition = *transition_io;
if (!transition) {
return true;
return CBM_DAEMON_IPC_RELEASE_COMPLETE;
}
if (transition->legacy_fd < 0 || !transition->legacy_process_entry ||
transition->owner_pid != getpid() ||
(!transition->work_begun &&
(transition->startup_v2_fd < 0 || !transition->startup_v2_process_entry))) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
posix_named_lock_release(transition->legacy_fd, transition->legacy_process_entry);
if (!transition->work_begun) {
@@ -4008,7 +4010,7 @@ bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t *
}
free(transition);
*transition_io = NULL;
return true;
return CBM_DAEMON_IPC_RELEASE_COMPLETE;
}
#else /* _WIN32 */
@@ -6203,35 +6205,39 @@ static void win_participant_group_release_complete(cbm_private_file_lock_t **gro
}
}
static bool win_participant_state_release(const cbm_daemon_ipc_endpoint_t *endpoint,
cbm_private_lock_directory_t *directory,
cbm_private_file_lock_t **startup_v2_io,
win_legacy_mutex_guard_t **legacy_guard_io,
cbm_private_file_lock_t **group_io, HANDLE *sentinel_io) {
static int win_participant_state_release(const cbm_daemon_ipc_endpoint_t *endpoint,
cbm_private_lock_directory_t *directory,
cbm_private_file_lock_t **startup_v2_io,
win_legacy_mutex_guard_t **legacy_guard_io,
cbm_private_file_lock_t **group_io, HANDLE *sentinel_io) {
if (!endpoint || !directory || !startup_v2_io || !legacy_guard_io || !group_io ||
!sentinel_io) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
if (!*startup_v2_io) {
int startup = win_startup_v2_try_acquire(directory, startup_v2_io);
if (startup != 1) {
return false;
return startup;
}
}
if (!*legacy_guard_io) {
int legacy = win_legacy_mutex_guard_try_acquire(endpoint, legacy_guard_io);
if (legacy != 1) {
(void)cbm_private_file_lock_release(startup_v2_io);
return false;
if (cbm_private_file_lock_release(startup_v2_io) != CBM_PRIVATE_FILE_LOCK_OK) {
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
return legacy;
}
}
if (!win_participant_group_release_locked(group_io, sentinel_io)) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
if (!win_legacy_mutex_guard_release(legacy_guard_io)) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
return cbm_private_file_lock_release(startup_v2_io) == CBM_PRIVATE_FILE_LOCK_OK;
return cbm_private_file_lock_release(startup_v2_io) == CBM_PRIVATE_FILE_LOCK_OK
? CBM_DAEMON_IPC_RELEASE_COMPLETE
: CBM_DAEMON_IPC_RELEASE_ERROR;
}
cbm_daemon_ipc_listener_t *cbm_daemon_ipc_listen_reserved(
@@ -7007,9 +7013,9 @@ bool cbm_daemon_ipc_startup_lock_release(cbm_daemon_ipc_startup_lock_t **lock_io
if (!lock) {
return true;
}
if (!win_participant_state_release(lock->endpoint, lock->directory, &lock->startup_v2_lock,
&lock->legacy_guard, &lock->group_lock,
&lock->legacy_sentinel)) {
if (win_participant_state_release(lock->endpoint, lock->directory, &lock->startup_v2_lock,
&lock->legacy_guard, &lock->group_lock,
&lock->legacy_sentinel) != CBM_DAEMON_IPC_RELEASE_COMPLETE) {
return false;
}
cbm_private_lock_directory_close(lock->directory);
@@ -7072,9 +7078,10 @@ bool cbm_daemon_ipc_participant_guard_release(cbm_daemon_ipc_participant_guard_t
if (!guard) {
return true;
}
if (!win_participant_state_release(
guard->endpoint, guard->directory, &guard->teardown_startup_v2_lock,
&guard->teardown_legacy_guard, &guard->group_lock, &guard->legacy_sentinel)) {
if (win_participant_state_release(guard->endpoint, guard->directory,
&guard->teardown_startup_v2_lock,
&guard->teardown_legacy_guard, &guard->group_lock,
&guard->legacy_sentinel) != CBM_DAEMON_IPC_RELEASE_COMPLETE) {
return false;
}
cbm_private_lock_directory_close(guard->directory);
@@ -7169,39 +7176,45 @@ bool cbm_daemon_ipc_local_transition_begin_work(cbm_daemon_ipc_local_transition_
return true;
}
bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t **transition_io) {
int cbm_daemon_ipc_local_transition_try_release(cbm_daemon_ipc_local_transition_t **transition_io) {
if (!transition_io) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
cbm_daemon_ipc_local_transition_t *transition = *transition_io;
if (!transition) {
return true;
return CBM_DAEMON_IPC_RELEASE_COMPLETE;
}
if (!transition->endpoint || !transition->directory) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
if (transition->group_lock || transition->legacy_sentinel != INVALID_HANDLE_VALUE ||
transition->teardown_legacy_guard) {
if (!win_participant_state_release(transition->endpoint, transition->directory,
&transition->startup_v2_lock,
&transition->teardown_legacy_guard,
&transition->group_lock, &transition->legacy_sentinel)) {
return false;
int released = win_participant_state_release(
transition->endpoint, transition->directory, &transition->startup_v2_lock,
&transition->teardown_legacy_guard, &transition->group_lock,
&transition->legacy_sentinel);
if (released != CBM_DAEMON_IPC_RELEASE_COMPLETE) {
return released;
}
} else if (transition->startup_v2_lock &&
cbm_private_file_lock_release(&transition->startup_v2_lock) !=
CBM_PRIVATE_FILE_LOCK_OK) {
return false;
return CBM_DAEMON_IPC_RELEASE_ERROR;
}
cbm_private_lock_directory_close(transition->directory);
transition->directory = NULL;
free(transition);
*transition_io = NULL;
return true;
return CBM_DAEMON_IPC_RELEASE_COMPLETE;
}
#endif /* _WIN32 */
bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t **transition_io) {
return cbm_daemon_ipc_local_transition_try_release(transition_io) ==
CBM_DAEMON_IPC_RELEASE_COMPLETE;
}
bool cbm_daemon_ipc_send_frame(cbm_daemon_ipc_connection_t *connection,
cbm_daemon_frame_type_t type, uint16_t flags, const void *payload,
uint32_t length) {
+7 -1
View File
@@ -320,7 +320,13 @@ bool cbm_daemon_ipc_local_transition_begin_work(cbm_daemon_ipc_local_transition_
* Windows teardown reacquires startup-v2 and the legacy mutex, releases group
* SH, closes this participant's sentinel last, then releases mutex and v2.
* Success consumes and clears *transition_io; a retryable native cleanup
* failure returns false and retains it. */
* failure or startup-gate contention returns false and retains it. */
bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t **transition_io);
/* Nonblocking release: 1 consumes and clears *transition_io (also when already
* empty), 0 retains it while another startup owns a required gate, and -1
* retains it on an unsafe/native I/O failure. A NULL transition_io is invalid.
* Callers can give healthy contention its own wait policy without extending
* the deadline for repeated native cleanup failures. */
int cbm_daemon_ipc_local_transition_try_release(cbm_daemon_ipc_local_transition_t **transition_io);
#endif /* CBM_DAEMON_IPC_H */
+6 -1
View File
@@ -1197,7 +1197,12 @@ static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *fram
struct stat st;
bool is_symlink = false;
if (safe_stat(abs_path, &st, &is_symlink) != 0) {
if (out->count_only) {
/* A count aborts when it cannot observe an entry, because its caller
* (auto-index admission) treats an unreliable count as a refusal. A
* symlink is not that case: it is skipped deliberately, on every walk.
* Failing here made one symlink anywhere in a tree refuse the whole
* repo, and the refusal surfaced as "unsafe_or_unavailable_path". */
if (out->count_only && !is_symlink) {
out->failed = true;
} else if (is_symlink) {
/* Deliberately not indexed (#963): record so callers can
+2
View File
@@ -168,6 +168,8 @@ static const lang_name_entry_t LANG_NAME_TABLE[] = {
{"form", CBM_LANG_FORM},
{"magma", CBM_LANG_MAGMA},
{"wolfram", CBM_LANG_WOLFRAM},
{"jinja2", CBM_LANG_JINJA2},
{"jinja", CBM_LANG_JINJA2},
};
#define LANG_NAME_TABLE_SIZE (sizeof(LANG_NAME_TABLE) / sizeof(LANG_NAME_TABLE[0]))
+10 -1
View File
@@ -257,7 +257,16 @@ static bool ws_prefix_equal(const char *a, const char *b, size_t len) {
}
/* True when b is a or lives under a. Compares on a separator boundary so
* "/a/bc" is not treated as living under "/a/b". */
* "/a/bc" is not treated as living under "/a/b".
*
* '/' and '\' are equivalent here, as they are everywhere else in this module
* (ws_is_sep). The Windows sensitive-grant escape hatch (#1718) records the
* grant from cbm_canonical_path — backslashes — while the indexer candidate
* arrives normalized to '/'. A byte-exact comparison made the grant never
* match the candidate on Windows, so --approve-sensitive could not lift a
* "Program Files" refusal. Comparing on the separator-equivalence boundary
* keeps the containment semantics (a separator byte is never "part of a
* component name") while erasing the platform spelling difference. */
static bool ws_is_ancestor_or_equal(const char *a, const char *b) {
if (!a || !b || !a[0] || !b[0]) {
return false;
+14 -10
View File
@@ -1652,20 +1652,24 @@ static bool main_project_lock_manager_close(cbm_project_lock_manager_t **manager
}
static bool main_local_transition_close(cbm_daemon_ipc_local_transition_t **transition) {
uint64_t deadline = main_deadline_after(MAIN_COORDINATION_CLEANUP_MS);
uint64_t ceiling = main_deadline_after(MAIN_STARTUP_CONTENTION_CEILING_MS);
uint64_t error_deadline = main_deadline_after(MAIN_COORDINATION_CLEANUP_MS);
while (transition && *transition) {
/* A failed release always RETAINS the transition and is retriable by
* contract: the Windows release transition must briefly try-hold the
* shared startup-v2/legacy gates, so concurrent one-shot teardowns
* legitimately collide and succeed on retry. Success consumes the
* transition; only never-finishing cleanup is a failure, and the
* deadline escalation below owns that. */
(void)cbm_daemon_ipc_local_transition_release(transition);
/* Windows teardown reacquires the startup-v2/legacy gates before
* releasing its participant claim. A live startup owner can hold them
* beyond the native cleanup deadline. Retain protection and use the
* acquisition backstop for contention; repeated native errors still
* get only the short cleanup deadline. Success consumes the handle. */
int status = cbm_daemon_ipc_local_transition_try_release(transition);
if (*transition) {
if (cbm_now_ms() >= deadline) {
uint64_t now = cbm_now_ms();
if (now >= ceiling || (status < 0 && now >= error_deadline)) {
main_coordination_cleanup_fail_stop("local_transition_cleanup");
}
cbm_usleep(1000);
if (status == 0) {
error_deadline = main_deadline_after(MAIN_COORDINATION_CLEANUP_MS);
}
cbm_usleep(status == 0 ? 10000 : 1000);
}
}
return true;
+3 -1
View File
@@ -21029,7 +21029,9 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
cbm_clock_gettime(CLOCK_MONOTONIC, &t1);
long long dur_us = ((long long)(t1.tv_sec - req_t0.tv_sec) * MCP_S_TO_US) +
((long long)(t1.tv_nsec - req_t0.tv_nsec) / MCP_MS_TO_US);
cbm_log_mcp_request(req.method, NULL, true, dur_us);
/* server/discover is the modern connect probe. -32601 stays so the
* client falls back to initialize; that miss is not an error (#2433). */
cbm_log_mcp_request(req.method, NULL, strcmp(req.method, "server/discover") != 0, dur_us);
cbm_mcp_server_request_scope_end(srv);
cbm_jsonrpc_request_free(&req);
return err;
+3
View File
@@ -329,6 +329,9 @@ static char *resolve_js_relative(char *buf, size_t buf_size, const char *module_
continue;
}
if (seg_len == FQN_DOTDOT_LEN && seg_start[0] == '.' && seg_start[FQN_SEP_LEN] == '.') {
if (!buf[0]) {
return NULL;
}
path_pop_segment(buf);
continue;
}
+37
View File
@@ -414,6 +414,43 @@ static inline bool cbm_pipeline_invocation_leaf_matches(const CBMResolvedCall *r
call_leaf && strcmp(cbm_lsp_bare_segment(resolved->reason), call_leaf) == 0;
}
/* #606: a sub-floor `php_method_typed_unindexed` row still proves the receiver
* is not a project class, so a weak short-name fallback must not bind it. A
* project class with the same simple name keeps the fallback, since the PHP
* LSP does not follow every cross-file type. */
static inline bool cbm_pipeline_php_receiver_type_unindexed(const CBMResolvedCallArray *arr,
const CBMCall *call,
const cbm_registry_t *registry) {
if (!arr || !call || !call->enclosing_func_qn || !call->callee_name) {
return false;
}
for (int i = 0; i < arr->count; i++) {
const CBMResolvedCall *rc = &arr->items[i];
if (rc->kind != CBM_RESOLVED_INVOCATION || !rc->caller_qn || !rc->callee_qn ||
!rc->strategy || strcmp(rc->strategy, "php_method_typed_unindexed") != 0 ||
strcmp(rc->caller_qn, call->enclosing_func_qn) != 0) {
continue;
}
int site_rank = cbm_pipeline_invocation_site_rank(rc, call);
if (site_rank == 0 || !cbm_pipeline_invocation_leaf_matches(rc, call, site_rank)) {
continue;
}
const char *method_dot = strrchr(rc->callee_qn, '.');
if (!method_dot || method_dot == rc->callee_qn) {
continue;
}
char class_qn[CBM_SZ_512];
size_t class_len = (size_t)(method_dot - rc->callee_qn);
if (class_len >= sizeof(class_qn)) {
continue;
}
memcpy(class_qn, rc->callee_qn, class_len);
class_qn[class_len] = '\0';
return !cbm_registry_has_type_named(registry, cbm_lsp_bare_segment(class_qn));
}
return false;
}
/* Look up the highest-confidence LSP-resolved call entry whose caller QN
* matches the textual call's enclosing function and whose callee QN
* short-name matches the textual callee. Returns a pointer into `arr`
+4 -1
View File
@@ -1158,7 +1158,10 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
* paths are module chains) — MUST match pass_parallel.c exactly. */
(lang == CBM_LANG_PYTHON && cbm_suppress_weak_import_bound_call(true, true, res.strategy) &&
cbm_python_import_binding_contradicts(imports, call->callee_name, res.qualified_name,
ctx->gbuf, ctx->project_name, rel));
ctx->gbuf, ctx->project_name, rel)) ||
/* MUST match pass_parallel.c exactly. */
(lang == CBM_LANG_PHP && cbm_suppress_weak_member_match(true, true, res.strategy) &&
cbm_pipeline_php_receiver_type_unindexed(lsp_calls, call, ctx->registry));
/* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g.
* `requests.get("/api/orders/{id}")`) resolve to a QN containing the library
+4 -1
View File
@@ -3421,7 +3421,10 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
cbm_suppress_weak_import_bound_call(true, true, res.strategy) &&
cbm_python_import_binding_contradicts(&result->imports, call->callee_name,
res.qualified_name, rc->main_gbuf,
rc->project_name, rel));
rc->project_name, rel)) ||
/* MUST match pass_calls.c exactly. */
(lang == CBM_LANG_PHP && cbm_suppress_weak_member_match(true, true, res.strategy) &&
cbm_pipeline_php_receiver_type_unindexed(&result->resolved_calls, call, rc->registry));
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
* service signal lives in the callee_name. The registry can mis-resolve
+40
View File
@@ -2115,6 +2115,37 @@ static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx
return found;
}
/* The File an explicit relative import (`./x`, `../x`) names exactly, for
* files whose File QN differs from the module QN Strategy 1 tries
* (`../styles/global.css`, `../layouts/Layout.astro`, #2558). Resolved in
* stack buffers by cbm_repo_rel_join, which refuses a path above the
* repository root. */
static const cbm_gbuf_node_t *resolve_relative_file(const cbm_pipeline_ctx_t *ctx,
const char *source_rel, const char *module_path,
bool php_only) {
if (!source_rel || !module_path || module_path[0] != '.' ||
!(module_path[1] == '/' || (module_path[1] == '.' && module_path[2] == '/'))) {
return NULL;
}
char dir[PKGMAP_PATH_BUF];
int dir_len = snprintf(dir, sizeof(dir), "%s", source_rel);
if (dir_len < 0 || (size_t)dir_len >= sizeof(dir)) {
return NULL;
}
char *slash = strrchr(dir, '/');
if (slash) {
*slash = '\0';
} else {
dir[0] = '\0';
}
char path[PKGMAP_PATH_BUF];
if (!cbm_repo_rel_join(dir, module_path, path, sizeof(path))) {
return NULL;
}
const cbm_gbuf_node_t *file = cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, path);
return file && (!php_only || node_in_php_file(file)) ? file : NULL;
}
/* ── PHP PSR-4 class imports (#1186) ──────────────────────────────── */
typedef enum {
@@ -2389,6 +2420,15 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
}
}
/* Strategy 1c: relative non-module files can have a File QN that does not
* match the module QN above. Resolve the exact file after module and
* sibling lookup. */
const cbm_gbuf_node_t *rel_file =
resolve_relative_file(ctx, source_rel, imp->module_path, php_only);
if (rel_file) {
return rel_file;
}
/* Strategy 2: namespace map. `using App.Utils`, `import com.example.Foo`,
* `use App\Utils\Helper` name a NAMESPACE (or a member of it) that the
* path-based QN cannot express. Try the full module path and progressively
+2
View File
@@ -505,6 +505,8 @@ const char *cbm_registry_value_target(const cbm_registry_t *r, const char *qn);
int cbm_registry_find_by_name(const cbm_registry_t *r, const char *name, const char ***out,
int *count);
bool cbm_registry_has_type_named(const cbm_registry_t *r, const char *name);
/* Return total number of entries. */
int cbm_registry_size(const cbm_registry_t *r);
+15
View File
@@ -1496,6 +1496,21 @@ const char *cbm_registry_unique_field_qn(const cbm_registry_t *r, const char *me
return only;
}
bool cbm_registry_has_type_named(const cbm_registry_t *r, const char *name) {
if (!r || !name || !name[0]) {
return false;
}
const char **qns = NULL;
int count = 0;
cbm_registry_find_by_name(r, name, &qns, &count);
for (int i = 0; i < count; i++) {
if (cbm_label_is_type_like(cbm_registry_label_of(r, qns[i]))) {
return true;
}
}
return false;
}
int cbm_registry_size(const cbm_registry_t *r) {
return r ? (int)cbm_ht_count(r->exact) : 0;
}
+25
View File
@@ -1338,6 +1338,30 @@ TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077) {
PASS();
}
/* augment() is awaited inline inside 'tool.execute.after', so a hook-augment
* that never exits would stall every grep/glob/read completion in the host
* session until the agent is restarted. The generated plugin must bound the
* wait: SIGKILL the child after AUGMENT_TIMEOUT_MS and resolve with '' (fail
* open). The promise keeps the first resolution, so the kill-triggered
* 'close' event arriving late cannot smuggle in partial output. Stream
* errors fire on child.stdin, not the child — the emitted listener and the
* guarded end() keep a failed write (EPIPE, ENOENT) from crashing the host
* as an unhandled 'error' event (reproduced under Bun 1.4 with a >64KB
* payload and a non-reading child). */
TEST(client_adapter_opencode_bounds_a_hung_hook_augment_child) {
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
ASSERT_NOT_NULL(js);
ASSERT_NOT_NULL(strstr(js, "const AUGMENT_TIMEOUT_MS = 10000;"));
ASSERT_NOT_NULL(strstr(js, "const timer = setTimeout("));
ASSERT_NOT_NULL(strstr(js, "child.kill('SIGKILL');"));
/* error and close must both stop the timer once they own the outcome. */
ASSERT_NOT_NULL(strstr(js, "clearTimeout(timer);"));
/* stream-level failures must not escape as unhandled 'error' events. */
ASSERT_NOT_NULL(strstr(js, "child.stdin.on('error', () => {});"));
free(js);
PASS();
}
/* Empty/NULL inputs must not produce a module at all. */
TEST(client_adapter_rejects_missing_binary_path) {
ASSERT_NULL(cbm_client_adapter_pi(NULL));
@@ -1385,5 +1409,6 @@ SUITE(agent_clients) {
RUN_TEST(client_adapter_opencode_sends_the_required_hook_event);
RUN_TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction);
RUN_TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077);
RUN_TEST(client_adapter_opencode_bounds_a_hung_hook_augment_child);
RUN_TEST(client_adapter_rejects_missing_binary_path);
}
+88 -2
View File
@@ -2892,6 +2892,91 @@ TEST(daemon_ipc_local_participants_overlap_and_allow_modern_startup) {
PASS();
}
TEST(daemon_ipc_windows_local_transition_release_reports_live_gate_contention) {
#ifndef _WIN32
PASS();
#else
static const char key[] = "6060717182829393";
char parent[TEST_PATH_CAP] = {0};
char canonical_parent[TEST_PATH_CAP] = {0};
char runtime_dir[TEST_PATH_CAP] = {0};
char legacy_pipe[CBM_DAEMON_IPC_WINDOWS_NAME_CAP] = {0};
char legacy_startup[CBM_DAEMON_IPC_WINDOWS_NAME_CAP] = {0};
cbm_daemon_ipc_endpoint_t *endpoint = NULL;
cbm_daemon_ipc_local_transition_t *transition = NULL;
cbm_daemon_ipc_startup_lock_t *startup = NULL;
bool parent_ok = ipc_test_parent_new(parent, "local-release-busy") &&
ipc_test_full_path(canonical_parent, parent);
bool names_ok = parent_ok && cbm_daemon_ipc_windows_legacy_names(canonical_parent, key,
legacy_pipe, legacy_startup);
if (names_ok) {
endpoint = cbm_daemon_ipc_endpoint_new(key, parent);
}
if (endpoint) {
ipc_test_copy_path(runtime_dir, cbm_daemon_ipc_endpoint_runtime_dir(endpoint));
}
int acquired =
endpoint ? cbm_daemon_ipc_local_transition_try_acquire(endpoint, &transition) : -1;
int sealed = acquired == 1 ? cbm_daemon_ipc_local_transition_seal_legacy(transition) : -1;
int lifetime =
sealed == 1 ? cbm_daemon_ipc_local_transition_lifetime_probe(endpoint, transition) : -1;
bool begun = lifetime == 0 && cbm_daemon_ipc_local_transition_begin_work(transition);
int startup_acquired = begun ? cbm_daemon_ipc_startup_lock_try_acquire(endpoint, &startup) : -1;
int blocked = cbm_daemon_ipc_local_transition_try_release(&transition);
bool retained_while_busy = transition != NULL;
bool boolean_blocked = cbm_daemon_ipc_local_transition_release(&transition);
/* Keep the already-validated named mutex alive after the startup owner
* exits. Holding just this mutex exercises the second contention path. */
wchar_t *legacy_name = names_ok ? cbm_utf8_to_wide(legacy_startup) : NULL;
HANDLE legacy_mutex = startup_acquired == 1 && legacy_name
? OpenMutexW(SYNCHRONIZE | MUTEX_MODIFY_STATE, FALSE, legacy_name)
: NULL;
free(legacy_name);
bool startup_released = cbm_daemon_ipc_startup_lock_release(&startup);
DWORD mutex_wait =
startup_released && legacy_mutex ? WaitForSingleObject(legacy_mutex, 0) : WAIT_FAILED;
bool mutex_owned = mutex_wait == WAIT_OBJECT_0 || mutex_wait == WAIT_ABANDONED;
int legacy_blocked =
mutex_owned ? cbm_daemon_ipc_local_transition_try_release(&transition) : -1;
bool retained_while_legacy_busy = transition != NULL;
bool mutex_released = mutex_owned && ReleaseMutex(legacy_mutex);
if (legacy_mutex) {
(void)CloseHandle(legacy_mutex);
}
int legacy_retained = endpoint ? cbm_daemon_ipc_legacy_generation_probe(endpoint) : -1;
int released = cbm_daemon_ipc_local_transition_try_release(&transition);
bool consumed = transition == NULL;
int legacy_after = endpoint ? cbm_daemon_ipc_legacy_generation_probe(endpoint) : -1;
(void)cbm_daemon_ipc_startup_lock_release(&startup);
(void)cbm_daemon_ipc_local_transition_release(&transition);
cbm_daemon_ipc_endpoint_free(endpoint);
ipc_test_remove_tree(runtime_dir, parent);
ASSERT_TRUE(parent_ok);
ASSERT_TRUE(names_ok);
ASSERT_EQ(acquired, 1);
ASSERT_EQ(sealed, 1);
ASSERT_EQ(lifetime, 0);
ASSERT_TRUE(begun);
ASSERT_EQ(startup_acquired, 1);
ASSERT_EQ(blocked, 0);
ASSERT_TRUE(retained_while_busy);
ASSERT_FALSE(boolean_blocked);
ASSERT_TRUE(startup_released);
ASSERT_EQ(mutex_wait, WAIT_OBJECT_0);
ASSERT_EQ(legacy_blocked, 0);
ASSERT_TRUE(retained_while_legacy_busy);
ASSERT_TRUE(mutex_released);
ASSERT_EQ(legacy_retained, 1);
ASSERT_EQ(released, 1);
ASSERT_TRUE(consumed);
ASSERT_EQ(legacy_after, 0);
PASS();
#endif
}
TEST(daemon_ipc_windows_local_transition_release_retries_retained_mutex) {
#ifndef _WIN32
PASS();
@@ -2919,7 +3004,7 @@ TEST(daemon_ipc_windows_local_transition_release_retries_retained_mutex) {
if (begun) {
cbm_daemon_ipc_windows_legacy_guard_release_failures_set_for_test(1);
}
bool first_release = cbm_daemon_ipc_local_transition_release(&transition);
int first_release = cbm_daemon_ipc_local_transition_try_release(&transition);
bool retained_after_failure = transition != NULL;
bool retry_release = cbm_daemon_ipc_local_transition_release(&transition);
cbm_daemon_ipc_windows_legacy_guard_release_failures_set_for_test(0);
@@ -2935,7 +3020,7 @@ TEST(daemon_ipc_windows_local_transition_release_retries_retained_mutex) {
ASSERT_EQ(sealed, 1);
ASSERT_EQ(lifetime, 0);
ASSERT_TRUE(begun);
ASSERT_FALSE(first_release);
ASSERT_EQ(first_release, -1);
ASSERT_TRUE(retained_after_failure);
ASSERT_TRUE(retry_release);
ASSERT_NULL(transition);
@@ -5792,6 +5877,7 @@ SUITE(daemon_ipc) {
RUN_TEST(daemon_ipc_activation_probe_ignores_matching_startup_claim_only);
RUN_TEST(daemon_ipc_local_transition_coexists_with_active_daemon_lifetime);
RUN_TEST(daemon_ipc_local_participants_overlap_and_allow_modern_startup);
RUN_TEST(daemon_ipc_windows_local_transition_release_reports_live_gate_contention);
RUN_TEST(daemon_ipc_windows_local_transition_release_retries_retained_mutex);
RUN_TEST(daemon_ipc_windows_startup_release_retains_retry_authority);
RUN_TEST(daemon_ipc_frame_timeout_poisons_connection);
+31
View File
@@ -2122,6 +2122,34 @@ static CBMLanguage discover_lang_of(const cbm_file_info_t *files, int count, con
return CBM_LANG_COUNT;
}
/* #1803: a symlink is skipped by policy on every walk. A bounded count must
* skip it too, not abort: aborting made auto-index admission refuse every
* repository containing any symlink ("unsafe_or_unavailable_path"), while
* an explicit index of the same tree succeeded. */
TEST(discover_count_bounded_survives_a_symlink) {
#ifdef _WIN32
SKIP_PLATFORM("Windows: symlinks need admin / symlink() unavailable");
#else
char *base = th_mktempdir("cbm_disc_link");
ASSERT(base != NULL);
th_mkdir_p(TH_PATH(base, ".git"));
th_write_file(TH_PATH(base, "main.go"), "package main\n");
th_write_file(TH_PATH(base, "sub/helper.go"), "package sub\n");
ASSERT_EQ(symlink("sub", TH_PATH(base, "linkdir")), 0);
cbm_discover_opts_t opts = {0};
int count = -1;
/* deadline 0 = no deadline: only the symlink can decide this count. */
cbm_discover_status_t status = cbm_discover_count_bounded(base, &opts, 50000, 0, &count);
ASSERT_EQ(status, CBM_DISCOVER_OK);
ASSERT_EQ(count, 2);
th_cleanup(base);
PASS();
#endif
}
/* Write raw bytes (may contain embedded NUL) to base/rel. Parent must exist. */
static int write_bytes_file(const char *path, const void *data, size_t n) {
FILE *f = cbm_fopen(path, "wb");
@@ -2507,4 +2535,7 @@ SUITE(discover) {
RUN_TEST(discover_nested_gitignore_below_ancestor_gitignore_issue1973);
RUN_TEST(discover_nested_gitignore_negation_overrides_ancestor_pattern);
RUN_TEST(discover_nested_gitignore_three_levels_deeper_file_wins);
/* A symlink must not abort a bounded count (auto-index admission, #1803) */
RUN_TEST(discover_count_bounded_survives_a_symlink);
}
+98
View File
@@ -267,6 +267,102 @@ static int ei_edge_present(const EILangFile *files, int nfiles, const char *edge
return got >= floor;
}
typedef struct {
const char *importer;
const char *target;
} EIFileEdgeExpect;
static int ei_file_edges_match(const EILangFile *files, int nfiles, const EIFileEdgeExpect *want,
int nwant, int expected_count) {
EILangProj lp;
cbm_store_t *store = ei_index_files(&lp, files, nfiles);
int ok = store && cbm_store_count_edges_by_type(store, lp.project, "IMPORTS") == expected_count;
if (store && !ok) {
int got = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS");
fprintf(stderr, " [IMPORTS] FAIL count=%d expected==%d\n", got, expected_count);
}
for (int i = 0; store && i < nwant; i++) {
int64_t source_id = ei_node_id_for_file_label(store, lp.project, want[i].importer, "File");
cbm_edge_t *edges = NULL;
int edge_count = 0;
int found = 0;
if (source_id > 0 &&
cbm_store_find_edges_by_source_type(store, source_id, "IMPORTS", &edges, &edge_count) ==
CBM_STORE_OK) {
for (int j = 0; j < edge_count; j++) {
cbm_node_t target;
memset(&target, 0, sizeof(target));
if (cbm_store_find_node_by_id(store, edges[j].target_id, &target) == CBM_STORE_OK) {
found = target.file_path && strcmp(target.file_path, want[i].target) == 0;
cbm_node_free_fields(&target);
}
if (found) {
break;
}
}
}
cbm_store_free_edges(edges, edge_count);
if (!found) {
fprintf(stderr, " [IMPORTS] FAIL %s -> %s\n", want[i].importer, want[i].target);
ok = 0;
}
}
ei_cleanup(&lp, store);
return ok;
}
TEST(ei_relative_astro_css_imports_issue2558) {
static const EILangFile f[] = {
{"Badge.astro", "<span>outside</span>\n"},
{"src/components/shared/Button.astro",
"---\nexport function buttonLabel(t: string): string { return t; }\n---\n<button />\n"},
{"src/components/Badge.astro", "<span>badge</span>\n"},
{"src/components/Card.astro",
"---\nimport Badge from './Badge.astro';\n"
"import Button from './shared/Button.astro';\n---\n<div><Badge /><Button /></div>\n"},
{"src/layouts/local.css", "body { margin: 0; }\n"},
{"src/styles/global.css", "body { color: black; }\n"},
{"src/layouts/Layout.astro",
"---\nimport '../styles/global.css';\nimport './local.css';\n---\n"
"<html><body><slot /></body></html>\n"},
{"src/lib/format.ts", "export function formatTitle(t: string): string { return t.trim(); }\n"},
{"src/lib/registry.ts",
"import Badge from '../components/Badge.astro';\n"
"import '../styles/global.css';\n"
"import { formatTitle } from './format';\n"
"export function registry(): unknown[] { return [Badge, formatTitle]; }\n"},
{"src/lib/sub/deep.ts",
"import { formatTitle } from '../format';\n"
"import { registry } from '../registry.ts';\n"
"export function deep(): string { registry(); return formatTitle('x'); }\n"},
{"src/pages/index.astro",
"---\nimport Layout from '../layouts/Layout.astro';\n"
"import Card from '../components/Card.astro';\n"
"import Outside from '../../../Badge.astro';\n"
"import { formatTitle } from '../lib/format';\n"
"const title = formatTitle(' Home ');\n---\n"
"<Layout><h1>{title}</h1><Card /></Layout>\n"},
};
static const EIFileEdgeExpect want[] = {
{"src/components/Card.astro", "src/components/Badge.astro"},
{"src/components/Card.astro", "src/components/shared/Button.astro"},
{"src/layouts/Layout.astro", "src/styles/global.css"},
{"src/layouts/Layout.astro", "src/layouts/local.css"},
{"src/lib/registry.ts", "src/components/Badge.astro"},
{"src/lib/registry.ts", "src/styles/global.css"},
{"src/lib/registry.ts", "src/lib/format.ts"},
{"src/lib/sub/deep.ts", "src/lib/format.ts"},
{"src/lib/sub/deep.ts", "src/lib/registry.ts"},
{"src/pages/index.astro", "src/layouts/Layout.astro"},
{"src/pages/index.astro", "src/components/Card.astro"},
{"src/pages/index.astro", "src/lib/format.ts"},
};
ASSERT_TRUE(ei_file_edges_match(f, (int)(sizeof(f) / sizeof(f[0])), want,
(int)(sizeof(want) / sizeof(want[0])), 12));
PASS();
}
/* ═══════════════════════════════════════════════════════════════════════════
* GREEN GUARD — Python
*
@@ -2299,6 +2395,8 @@ TEST(ei_php_psr4_missing_class_leaves_type_evidence_unknown) {
* ═══════════════════════════════════════════════════════════════════════════ */
SUITE(edge_imports) {
RUN_TEST(ei_relative_astro_css_imports_issue2558);
/* ── GREEN GUARDS — Python (must stay passing) ── */
RUN_TEST(ei_python_relative_from_import);
RUN_TEST(ei_python_absolute_import);
+54
View File
@@ -2463,6 +2463,59 @@ TEST(server_handle_unknown_method) {
PASS();
}
/* Issue #2433: a modern client probes server/discover before initialize.
* The -32601 stays so it falls back; only that probe is logged as info. */
TEST(server_handle_discover_probe_logs_info) {
mcp_log_buf[0] = '\0';
CBMLogLevel prev_level = cbm_log_get_level();
cbm_log_set_level(CBM_LOG_DEBUG);
cbm_log_set_format(CBM_LOG_FORMAT_TEXT);
cbm_log_set_sink_ex(mcp_capture_log, CBM_LOG_SINK_REPLACE);
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
char *resp =
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"server/discover\"}");
ASSERT_NOT_NULL(resp);
ASSERT_NOT_NULL(strstr(resp, "\"code\":-32601"));
ASSERT_NOT_NULL(strstr(resp, "Method not found"));
ASSERT_NOT_NULL(strstr(mcp_log_buf, "method=server/discover"));
ASSERT_NOT_NULL(strstr(mcp_log_buf, "level=info"));
ASSERT_NOT_NULL(strstr(mcp_log_buf, "status=ok"));
ASSERT_NULL(strstr(mcp_log_buf, "level=warn"));
ASSERT_NULL(strstr(mcp_log_buf, "status=error"));
free(resp);
mcp_log_buf[0] = '\0';
resp =
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"unknown/method\"}");
ASSERT_NOT_NULL(resp);
ASSERT_NOT_NULL(strstr(resp, "-32601"));
ASSERT_NOT_NULL(strstr(mcp_log_buf, "method=unknown/method"));
ASSERT_NOT_NULL(strstr(mcp_log_buf, "level=warn"));
ASSERT_NOT_NULL(strstr(mcp_log_buf, "status=error"));
free(resp);
resp = cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"initialize\","
"\"params\":{\"protocolVersion\":\"2026-07-28\"}}");
ASSERT_NOT_NULL(resp);
ASSERT_NOT_NULL(strstr(resp, "2025-11-25"));
ASSERT_NULL(strstr(resp, "2026-07-28"));
free(resp);
resp =
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"resources/list\"}");
ASSERT_NOT_NULL(resp);
ASSERT_NOT_NULL(strstr(resp, "\"resources\":[]"));
ASSERT_NULL(strstr(resp, "-32601"));
free(resp);
cbm_mcp_server_free(srv);
cbm_log_set_sink(NULL);
cbm_log_set_level(prev_level);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* TOOL HANDLERS (via server_handle)
* ══════════════════════════════════════════════════════════════════ */
@@ -25013,6 +25066,7 @@ SUITE(mcp) {
RUN_TEST(server_handle_prompts_get_validates_arguments);
RUN_TEST(server_handle_logs_request_without_params);
RUN_TEST(server_handle_unknown_method);
RUN_TEST(server_handle_discover_probe_logs_info);
/* Server handle — edge cases */
RUN_TEST(server_handle_invalid_json);
+1
View File
@@ -1476,6 +1476,7 @@ static const char *const JSX_AMP_BODIES[] = {
"<p title=\"a&\">x</p>", /* '&' right before the quote */
"<p>Tom &Jerry</p>", /* '&' + letter in JSX text */
"<p>Tom & Jerry</p>", /* '&' + space in JSX text */
"<th className=\"p-4 font-semibold\">Role & Dept</th>", /* #2481 report reproduction */
"<p>x &1 y</p>", /* '&' + digit in JSX text */
/* controls: references keep parsing; '& ' / '&1' in attributes */
"<p title=\"x&amp;y&#38;z & w &1\">A&amp;B &#38; C</p>",
+159
View File
@@ -9659,6 +9659,163 @@ TEST(pipeline_python_receiver_suppresses_weak_method_edge) {
PASS();
}
/* #606: a receiver typed as an unindexed (vendor/) class must not weakly bind a
* same-named project method. */
static void write_php_receiver_fixture(const char *tmp) {
write_temp_file(tmp, "src/Item.php",
"<?php\n"
"namespace App;\n"
"\n"
"class Item\n"
"{\n"
" public function getUid(): int\n"
" {\n"
" return 1;\n"
" }\n"
"}\n");
write_temp_file(tmp, "src/Domain/Repository.php",
"<?php\n"
"namespace App\\Domain;\n"
"\n"
"use App\\Item;\n"
"use Vendor\\Storage\\File;\n"
"\n"
"class Repository\n"
"{\n"
" public function findItem(): Item\n"
" {\n"
" return new Item();\n"
" }\n"
"\n"
" public function findFile(): File\n"
" {\n"
" return new File();\n"
" }\n"
"}\n");
write_temp_file(tmp, "src/Listener.php",
"<?php\n"
"namespace App;\n"
"\n"
"use Vendor\\Storage\\File;\n"
"use function sprintf;\n"
"\n"
"class Listener\n"
"{\n"
" public function vendorCall(File $file): int\n"
" {\n"
" return $file->getUid();\n"
" }\n"
"\n"
" public function selfCall(): int\n"
" {\n"
" return $this->localHelper();\n"
" }\n"
"\n"
" private function localHelper(): int\n"
" {\n"
" return 1;\n"
" }\n"
"}\n");
write_temp_file(tmp, "src/ChainListener.php",
"<?php\n"
"namespace App;\n"
"\n"
"use App\\Domain\\Repository;\n"
"\n"
"class ChainListener\n"
"{\n"
" public function chainedVendorCall(Repository $repository): int\n"
" {\n"
" return $repository->findFile()->getUid();\n"
" }\n"
"\n"
" public function projectCall(Repository $repository): Item\n"
" {\n"
" return $repository->findItem();\n"
" }\n"
"}\n");
}
/* Fewer than 50 files exercises pass_calls.c. */
TEST(pipeline_php_receiver_suppresses_weak_method_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_php_recv_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_php_receiver_fixture(tmp);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/php_recv.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
ASSERT_GTE(fixture_node_count(s, project, "src/Item.php", "getUid", "Method"), 1);
ASSERT_FALSE(cross_file_call_exists(s, project, "vendorCall", "getUid"));
ASSERT_FALSE(cross_file_call_exists(s, project, "chainedVendorCall", "getUid"));
ASSERT_TRUE(cross_file_call_exists(s, project, "projectCall", "findItem"));
ASSERT_GTE(named_edge_count(s, project, "CALLS", "selfCall", "localHelper"), 1);
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
/* >= 50 files forces pass_parallel.c. */
TEST(pipeline_php_receiver_parallel_suppresses_weak_method_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_php_par_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_php_receiver_fixture(tmp);
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "src/Filler%d.php", i);
snprintf(body, sizeof(body), "<?php\nfunction filler%d(): int\n{\n return %d;\n}\n", i,
i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/php_par.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
ASSERT_GTE(fixture_node_count(s, project, "src/Item.php", "getUid", "Method"), 1);
ASSERT_FALSE(cross_file_call_exists(s, project, "vendorCall", "getUid"));
ASSERT_FALSE(cross_file_call_exists(s, project, "chainedVendorCall", "getUid"));
ASSERT_TRUE(cross_file_call_exists(s, project, "projectCall", "findItem"));
ASSERT_GTE(named_edge_count(s, project, "CALLS", "selfCall", "localHelper"), 1);
cbm_store_close(s);
cbm_pipeline_free(p);
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
PASS();
}
/* The member guard's one exemption (2026-09-16, v0.10.8 → v0.11.0 A/B on
* django/django): a member call whose receiver is an attribute chain rooted at
* self/cls, whose callee has exactly ONE definition in the project and is not a
@@ -27522,6 +27679,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354);
RUN_TEST(pipeline_ts_param_property_injection_issue514);
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_php_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_php_receiver_parallel_suppresses_weak_method_edge);
RUN_TEST(pipeline_python_receiver_keeps_specific_unique_name_member_call);
RUN_TEST(pipeline_html_embedded_member_call_stays_unbound);
RUN_TEST(pipeline_go_method_caller_keeps_lsp_join);
+110 -10
View File
@@ -12,6 +12,7 @@
#include "test_framework.h"
#include "cbm.h"
#include "lang_specs.h" /* cbm_ts_language — direct-parse GLR cap regression (#913) */
#include "lsp/ts_lsp.h"
#include <stdio.h>
#include <stdlib.h>
@@ -399,11 +400,11 @@ TEST(cpp_large_templated_header_no_crash_issue424) {
* java_resolve_calls_in_node frames (bind_lambda_args), bitcoin → SIGSEGV
* under deep c_resolve_calls_in_node frames (cbm_type_substitute via
* c_adl_resolve), microsoft/TypeScript → SIGBUS under an unbounded
* lookup_member_type cycle. The walks now carry depth guards; these
* reproductions fork a child so a regression cannot kill the test runner
* (the TS cyclic-type shape is only reachable with a real cross-file
* registry, so that one is verified at the real-repo tier; the synthetic
* cyclic fixture here guards the in-file path).
* lookup_member_type cycle. The walks now carry depth guards. Non-TS crash
* fixtures fork on POSIX and run in-process on Windows. TypeScript cycle work
* runs in-process on both platforms to assert test-only lookup counters.
* The synthetic single-file fixtures here guard the local inheritance path;
* real-repo scale extraction is checked separately.
* ═══════════════════════════════════════════════════════════════════ */
#if !defined(_WIN32)
@@ -670,9 +671,106 @@ TEST(lsp_ts_cyclic_types_no_crash) {
"interface D extends C { d: number; }\n"
"declare const a: A;\n"
"declare const c: C;\n"
"function useIt(p: C) { return p.missing_member; }\n"
/* Both lookup paths must traverse the inheritance cycle. */
"function useIt(p: C) { p.missing(); return p.missing_member; }\n"
"const y = c.also_missing;\n";
ASSERT_FALSE(so_extract_crashes(src, CBM_LANG_TYPESCRIPT, "cycle.ts"));
CBMFileResult *r = extract(src, CBM_LANG_TYPESCRIPT, "test", "cycle.ts");
ASSERT_NOT_NULL(r);
/* C and D each have one parent: <= 64 admitted visits per type, plus
* one attempted (possibly rejected) parent visit per admitted visit. */
ASSERT_GT(cbm_ts_lsp_test_max_member_lookup_steps(), 0);
ASSERT_GT(cbm_ts_lsp_test_max_method_lookup_steps(), 0);
ASSERT_LTE(cbm_ts_lsp_test_max_member_lookup_steps(), 2 * 64 * 2);
ASSERT_LTE(cbm_ts_lsp_test_max_method_lookup_steps(), 2 * 64 * 2);
cbm_free_result(r);
PASS();
}
TEST(lsp_ts_branching_cycle_has_bounded_work) {
const char *src =
"interface A extends B, C {}\n"
"interface B extends A, C {}\n"
"interface C extends A, B {}\n"
"function useIt(value: A) { value.missing(); return value.missing_member; }\n";
CBMFileResult *r = extract(src, CBM_LANG_TYPESCRIPT, "test", "branching-cycle.ts");
ASSERT_NOT_NULL(r);
/* A, B, C each have two parents: <= 64 admitted visits per type, plus
* two attempted (possibly rejected) parent visits per admitted visit. */
ASSERT_GT(cbm_ts_lsp_test_max_member_lookup_steps(), 0);
ASSERT_GT(cbm_ts_lsp_test_max_method_lookup_steps(), 0);
ASSERT_LTE(cbm_ts_lsp_test_max_member_lookup_steps(), 3 * 64 * 3);
ASSERT_LTE(cbm_ts_lsp_test_max_method_lookup_steps(), 3 * 64 * 3);
cbm_free_result(r);
PASS();
}
TEST(lsp_ts_shallower_revisit_preserves_inherited_method) {
enum { CHAIN = 62, CAP = 16384 };
char *src = malloc(CAP);
ASSERT_NOT_NULL(src);
size_t used =
(size_t)snprintf(src, CAP,
"interface Service { ping(): void; }\n"
"interface Target { inherited(): void; inheritedField: Service; }\n"
"interface Shared extends Target {}\n"
"interface Root extends D0, Shared {}\n");
for (int i = 0; i < CHAIN; i++) {
const char *parent = i + 1 < CHAIN ? "D" : "Shared";
int wrote = i + 1 < CHAIN
? snprintf(src + used, CAP - used, "interface D%d extends %s%d {}\n", i,
parent, i + 1)
: snprintf(src + used, CAP - used, "interface D%d extends Shared {}\n", i);
ASSERT_TRUE(wrote > 0 && (size_t)wrote < CAP - used);
used += (size_t)wrote;
}
int wrote = snprintf(
src + used, CAP - used,
"function useIt(value: Root) { value.inherited(); value.inheritedField.ping(); }\n");
ASSERT_TRUE(wrote > 0 && (size_t)wrote < CAP - used);
CBMFileResult *r = extract(src, CBM_LANG_TYPESCRIPT, "test", "shallower.ts");
free(src);
ASSERT_NOT_NULL(r);
int inherited_method_found = 0;
int inherited_field_method_found = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &r->resolved_calls.items[i];
if (resolved->confidence > 0 && resolved->caller_qn &&
strstr(resolved->caller_qn, "useIt") && resolved->callee_qn &&
strstr(resolved->callee_qn, "inherited")) {
inherited_method_found = 1;
}
if (resolved->confidence > 0 && resolved->caller_qn &&
strstr(resolved->caller_qn, "useIt") && resolved->callee_qn &&
strstr(resolved->callee_qn, "ping")) {
inherited_field_method_found = 1;
}
}
cbm_free_result(r);
ASSERT_TRUE(inherited_method_found);
ASSERT_TRUE(inherited_field_method_found);
PASS();
}
TEST(lsp_ts_inherited_method_lookup) {
const char *src = "interface Base { inherited(): void; }\n"
"interface Child extends Base {}\n"
"function useIt(c: Child) { c.inherited(); }\n";
CBMFileResult *r = extract(src, CBM_LANG_TYPESCRIPT, "test", "inherited.ts");
ASSERT_NOT_NULL(r);
int found = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &r->resolved_calls.items[i];
if (resolved->confidence > 0 && resolved->caller_qn &&
strstr(resolved->caller_qn, "useIt") && resolved->callee_qn &&
strstr(resolved->callee_qn, "inherited")) {
found = 1;
break;
}
}
cbm_free_result(r);
ASSERT_TRUE(found);
PASS();
}
@@ -777,9 +875,8 @@ TEST(lsp_kotlin_deep_nesting_no_crash) {
/* Split into three sub-suites so parallel/sharded runs are not serialized
* behind one ~4-minute suite (it was the wall-clock critical path: every
* other suite finished underneath it). Pure re-registration — the 20
* RUN_TEST entries are exactly the ones the single suite carried; the
* before/after test-count parity is asserted in the shard runner. */
* other suite finished underneath it). New regressions stay in the matching
* shard; the shard runner asserts aggregate test-count parity. */
SUITE(stack_overflow_a) {
cbm_init();
@@ -799,6 +896,9 @@ SUITE(stack_overflow_b) {
RUN_TEST(perl_glr_deep_parse_recursion_capped);
RUN_TEST(lsp_ts_cyclic_types_no_crash);
RUN_TEST(lsp_ts_branching_cycle_has_bounded_work);
RUN_TEST(lsp_ts_shallower_revisit_preserves_inherited_method);
RUN_TEST(lsp_ts_inherited_method_lookup);
RUN_TEST(lsp_python_deep_nesting_no_crash);
RUN_TEST(lsp_go_deep_nesting_no_crash);
RUN_TEST(lsp_php_deep_nesting_no_crash);
+121
View File
@@ -1156,6 +1156,125 @@ TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites) {
PASS();
}
TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity) {
const char *source = "function Card(): JSX.Element { return <span/>; }\n"
"const Direct = async () => <Card/>;\n"
"export const Exported = async () => <Card/>;\n"
"function Declared(): JSX.Element { return <Card/>; }\n";
const char *const declarations[] = {"const Direct", "export const Exported",
"function Declared"};
const char *const callers[] = {"test.main.Direct", "test.main.Exported", "test.main.Declared"};
CBMFileResult *r = extract_tsx(source);
const bool result_created = r != NULL;
bool exact_carrier[3] = {false};
bool exact_resolution[3] = {false};
bool resolved_invocation[3] = {false};
bool resolved_card_callee[3] = {false};
bool resolved_jsx_strategy[3] = {false};
int carrier_count[3] = {0};
int failed_cases = result_created ? 0 : 1;
if (!result_created)
printf(" JSX caller diagnostic: extraction failed\n");
if (r) {
for (int i = 0; i < 3; i++) {
const char *declaration = strstr(source, declarations[i]);
const char *site = declaration ? strstr(declaration, "<Card/>") : NULL;
if (!site) {
printf(" JSX caller diagnostic: source site missing for %s\n", callers[i]);
failed_cases++;
continue;
}
const uint32_t start = (uint32_t)(site - source);
const uint32_t end = start + (uint32_t)strlen("<Card/>");
const CBMCall *carrier = NULL;
for (int j = 0; j < r->calls.count; j++) {
const CBMCall *call = &r->calls.items[j];
if (call->callee_name && strcmp(call->callee_name, "Card") == 0 &&
call->site_start_byte == start && call->site_end_byte == end) {
carrier = call;
carrier_count[i]++;
}
}
const CBMResolvedCall *resolved = carrier ? cbm_pipeline_find_lsp_resolution(
&r->resolved_calls, carrier, false)
: NULL;
exact_carrier[i] = carrier && carrier->enclosing_func_qn &&
strcmp(carrier->enclosing_func_qn, callers[i]) == 0 &&
carrier->site_start_byte == start && carrier->site_end_byte == end &&
carrier->requires_lsp_resolution;
exact_resolution[i] = resolved && resolved->caller_qn &&
strcmp(resolved->caller_qn, callers[i]) == 0 &&
resolved->site_start_byte == start && resolved->site_end_byte == end;
resolved_invocation[i] = resolved && resolved->kind == CBM_RESOLVED_INVOCATION;
if (resolved && resolved->callee_qn) {
const size_t callee_len = strlen(resolved->callee_qn);
resolved_card_callee[i] = callee_len >= strlen(".Card") &&
strcmp(resolved->callee_qn + callee_len - strlen(".Card"),
".Card") == 0;
}
resolved_jsx_strategy[i] =
resolved && resolved->strategy && strcmp(resolved->strategy, "lsp_ts_jsx") == 0;
if (carrier_count[i] != 1 || !exact_carrier[i] || !exact_resolution[i] ||
!resolved_invocation[i] || !resolved_card_callee[i] || !resolved_jsx_strategy[i]) {
printf(" JSX caller diagnostic: expected=%s span=%u:%u carriers=%d caller=%s "
"requires_lsp=%d resolution=%s resolution_span=%u:%u kind=%d callee=%s "
"strategy=%s\n",
callers[i], start, end, carrier_count[i],
carrier && carrier->enclosing_func_qn ? carrier->enclosing_func_qn : "(null)",
carrier ? carrier->requires_lsp_resolution : 0,
resolved && resolved->caller_qn ? resolved->caller_qn : "(null)",
resolved ? resolved->site_start_byte : 0, resolved ? resolved->site_end_byte : 0,
resolved ? resolved->kind : -1,
resolved && resolved->callee_qn ? resolved->callee_qn : "(null)",
resolved && resolved->strategy ? resolved->strategy : "(null)");
failed_cases++;
}
}
}
cbm_free_result(r);
ASSERT_EQ(failed_cases, 0);
PASS();
}
/* An exported arrow const is a module definition, not a lexical local. Bound
* into the module scope, it made the JSX resolver fail closed on `<Badge />`
* (the edge was lost: JSX carriers have no other resolver) and the exact
* same-module call path skip `formatLabel()`. Both callers must resolve both
* targets exactly, whether the caller is a function or an arrow const. */
static bool ts_resolved_with_strategy(const CBMFileResult *r, const char *caller,
const char *callee, const char *strategy) {
int idx = require_resolved(r, caller, callee);
const char *got = idx >= 0 ? r->resolved_calls.items[idx].strategy : NULL;
if (idx >= 0 && (!got || strcmp(got, strategy) != 0)) {
printf(" %s -> %s resolved by %s, expected %s\n", caller, callee, got ? got : "(null)",
strategy);
}
return got && strcmp(got, strategy) == 0;
}
TEST(tslsp_jsx_exported_arrow_component_resolves_in_same_module) {
CBMFileResult *r =
extract_tsx("export const Badge = () => <span/>;\n"
"export const formatLabel = (s: string) => s.trim();\n"
"export function FnUser() { formatLabel('a'); return <Badge/>; }\n"
"export const ArrowUser = () => { formatLabel('b'); return <Badge/>; };\n");
ASSERT_NOT_NULL(r);
bool fn_jsx = ts_resolved_with_strategy(r, "main.FnUser", "main.Badge", "lsp_ts_jsx");
bool arrow_jsx = ts_resolved_with_strategy(r, "main.ArrowUser", "main.Badge", "lsp_ts_jsx");
bool fn_call = ts_resolved_with_strategy(r, "main.FnUser", "main.formatLabel", "lsp_ts_local");
bool arrow_call =
ts_resolved_with_strategy(r, "main.ArrowUser", "main.formatLabel", "lsp_ts_local");
cbm_free_result(r);
ASSERT_TRUE(fn_jsx);
ASSERT_TRUE(arrow_jsx);
ASSERT_TRUE(fn_call);
ASSERT_TRUE(arrow_call);
PASS();
}
/* A callable value named like a module component owns the JSX tag. The LSP
* pass must not confidently redirect it to the module function, and the
* carrier must require semantic resolution so graph construction cannot do
@@ -4863,6 +4982,8 @@ SUITE(ts_lsp) {
RUN_TEST(tslsp_jsx_component_with_children);
RUN_TEST(tslsp_jsx_import_requires_registered_component);
RUN_TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites);
RUN_TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity);
RUN_TEST(tslsp_jsx_exported_arrow_component_resolves_in_same_module);
RUN_TEST(tslsp_jsx_local_tag_shadow_blocks_module_component);
RUN_TEST(tslsp_jsx_intrinsic_skipped);
RUN_TEST(tslsp_jsx_nested_component);
+2 -1
View File
@@ -39,7 +39,7 @@ TEST(userconfig_project_basic) {
char proj[512];
snprintf(proj, sizeof(proj), "%s/.codebase-memory.json", dir);
ASSERT_EQ(
write_json(proj, "{\"extra_extensions\":{\".blade.php\":\"php\",\".mjs\":\"javascript\"}}"),
write_json(proj, "{\"extra_extensions\":{\".blade.php\":\"php\",\".mjs\":\"javascript\",\".html\":\"jinja2\"}}"),
0);
cbm_userconfig_t *cfg = cbm_userconfig_load(dir);
@@ -47,6 +47,7 @@ TEST(userconfig_project_basic) {
ASSERT_EQ(cbm_userconfig_lookup(cfg, ".blade.php"), CBM_LANG_PHP);
ASSERT_EQ(cbm_userconfig_lookup(cfg, ".mjs"), CBM_LANG_JAVASCRIPT);
ASSERT_EQ(cbm_userconfig_lookup(cfg, ".html"), CBM_LANG_JINJA2);
ASSERT_EQ(cbm_userconfig_lookup(cfg, ".go"), CBM_LANG_COUNT); /* not in user config */
cbm_userconfig_free(cfg);
+95
View File
@@ -484,6 +484,100 @@ TEST(ws_manifest_approval_refuses_overbroad_requests) {
PASS();
}
/* #1718: the sensitive-root escape hatch broke on Windows separator spelling.
* The grant is recorded from cbm_canonical_path — backslashes — while the
* indexer hands the candidate already normalized to '/'. A byte-exact
* comparison made the grant never match the candidate, so an approved
* "Program Files" root stayed refused. '/' and '\' are equivalent everywhere
* else in this module (ws_is_sep); the boundary comparison must treat them
* equally. The classify_root home rule uses the same ws_paths_equal, so a
* mixed-separator home spelling is the string-level regression check; the
* FS-backed grant round-trip lives in the _WIN32 half, where the real
* canonicalization exercises it. */
TEST(ws_approve_sensitive_matches_across_separator_styles) {
/* canonical spelled with '\' vs home spelled with '/' — must classify as
* the same directory. Pre-fix: strncmp mismatch at the separator byte, so
* the home rule never matched and an ordinary path under "C:\Users" fell
* through to CBM_WS_ALLOW. */
ASSERT_EQ(cbm_workspace_classify_root("C:\\Users\\dev", "C:/Users/dev", CACHE),
CBM_WS_DENY_SENSITIVE);
ASSERT_EQ(cbm_workspace_classify_root("C:/Users/dev", "C:\\Users\\dev", CACHE),
CBM_WS_DENY_SENSITIVE);
/* A sibling that differs after the same separator boundary is still a
* different directory: "/a/bc" must never look like "/a/b". */
ASSERT_EQ(cbm_workspace_classify_root("C:\\Users\\devx", "C:/Users/dev", CACHE), CBM_WS_ALLOW);
#ifdef _WIN32
/* Full grant round-trip on real filesystem paths: the grant stored with
* '\' must lift the exact candidate the indexer passes with '/'. The
* sensitive class is exercised by a credential-named directory (".ssh"),
* which is DENY_SENSITIVE at any depth — equivalent to "Program Files"
* for the refusal path under test. */
char *cache = th_mktempdir("cbm_ws_sw");
ASSERT(cache != NULL);
char dir[1024];
snprintf(dir, sizeof(dir), "%s/.ssh/cfg", cache);
ASSERT_TRUE(cbm_mkdir_p(dir, 0700));
char path[1024];
snprintf(path, sizeof(path), "%s/allowed_roots", cache);
char grant[2048];
snprintf(grant, sizeof(grant), "!%s\\.ssh\\cfg\n", cache);
FILE *f = cbm_fopen(path, "wb");
ASSERT_NOT_NULL(f);
(void)fprintf(f, "%s", grant);
(void)fclose(f);
char candidate[2048];
snprintf(candidate, sizeof(candidate), "%s/.ssh/cfg", cache);
/* Convert to '/' spelling, exactly as cbm_normalize_path_sep does for the
* indexer's candidate. */
char *p;
for (p = candidate; *p; p++) {
if (*p == '\\') {
*p = '/';
}
}
char err[1024];
err[0] = '\0';
ASSERT_TRUE(cbm_workspace_root_allowed(candidate, NULL, cache, NULL, err, sizeof(err)));
/* An unapproved sibling under the same sensitive tree stays refused. It
* lies outside every granted root, so the refusal is the boundary wording
* (kept verbatim by contract), not the sensitive hint. */
char sibling[2048];
snprintf(sibling, sizeof(sibling), "%s/.ssh/keys", cache);
for (p = sibling; *p; p++) {
if (*p == '\\') {
*p = '/';
}
}
err[0] = '\0';
ASSERT_FALSE(cbm_workspace_root_allowed(sibling, NULL, cache, NULL, err, sizeof(err)));
ASSERT_TRUE(strstr(err, "outside the allowed root") != NULL);
/* A strict descendant INSIDE the approved sensitive grant is contained
* (boundary passes) but not the exact grant, so the sensitive refusal
* applies and the hint names the flag. */
char descendant[2048];
snprintf(descendant, sizeof(descendant), "%s/.ssh/cfg/sub", cache);
for (p = descendant; *p; p++) {
if (*p == '\\') {
*p = '/';
}
}
/* Containment canonicalizes the candidate, which on Windows opens it
* (CreateFileW, OPEN_EXISTING): a descendant that is not on disk is
* "outside the allowed root" instead of reaching the sensitive hint. */
ASSERT_TRUE(cbm_mkdir_p(descendant, 0700));
err[0] = '\0';
ASSERT_FALSE(cbm_workspace_root_allowed(descendant, NULL, cache, NULL, err, sizeof(err)));
ASSERT_TRUE(strstr(err, "--approve-sensitive") != NULL);
th_cleanup(cache);
#endif
PASS();
}
/* ── Environment helpers ────────────────────────────────────────────────── */
static char *ws_env_save(const char *name) {
@@ -906,6 +1000,7 @@ SUITE(workspace) {
RUN_TEST(ws_posix_matching_is_case_sensitive);
RUN_TEST(ws_null_context_disables_dependent_checks);
RUN_TEST(ws_every_verdict_has_a_reason);
RUN_TEST(ws_approve_sensitive_matches_across_separator_styles);
RUN_TEST(ws_home_dir_is_resolved);
RUN_TEST(ws_linked_home_classified_as_home);
RUN_TEST(ws_manifest_entry_classified_in_resolved_form);
+120
View File
@@ -520,6 +520,125 @@ def section_churn_stability(binary, work):
kill_pid(daemon_pid)
def section_local_cleanup_waits_for_startup_gate(binary, work):
"""A live startup owner must not turn completed local work into failure."""
if os.name != "nt":
print("SKIP: local teardown reacquires the startup gate only on Windows")
return True
import msvcrt
cache = section_dirs(work, "cleanup-contention")
env = cli_env(cache)
env["CBM_LOG_LEVEL"] = "info"
env["CBM_LOG_FORMAT"] = "text"
warm = subprocess.run([binary, "cli", "list_projects"], capture_output=True,
timeout=90, env=env)
if warm.returncode != 0:
raise SetupFailure("could not initialize the private runtime:\n" + out_text(warm))
gates = []
for parent, _, files in os.walk(runtime_for(cache)):
if "cbm-startup-v2.lock" in files:
gates.append(os.path.join(parent, "cbm-startup-v2.lock"))
if len(gates) != 1:
raise SetupFailure("expected one private startup gate, got %r" % gates)
repo = os.path.join(work, "cleanup-repo")
os.makedirs(repo)
for index in range(200):
with open(os.path.join(repo, "unit_%d.py" % index), "w") as handle:
handle.write("".join("def function_%d_%d():\n return %d\n"
% (index, item, item) for item in range(100)))
err_path = os.path.join(work, "cleanup-contention.stderr")
process = None
locked = False
traces = {}
def read_trace():
# The daemon supervises indexing in a worker. Worker progress goes to
# its own durable log; the outer CLI only reports the final response.
paths = [err_path]
log_dir = os.path.join(cache, "logs")
if os.path.isdir(log_dir):
paths.extend(os.path.join(log_dir, name) for name in os.listdir(log_dir)
if name.startswith(".worker-log-"))
for path in paths:
try:
with open(path, "rb") as handle:
traces[path] = handle.read()
except FileNotFoundError:
pass # The supervisor removes a successful worker's log.
return b"\n".join(traces.values())
with open(gates[0], "r+b") as gate, open(err_path, "wb") as errors:
try:
process = subprocess.Popen(
[binary, "cli", "index_repository", "--progress"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=errors, env=env)
process.stdin.write(json.dumps({"repo_path": repo, "mode": "fast"}).encode())
process.stdin.close()
process.stdin = None
deadline = time.monotonic() + 120
# Extraction is after begin_work released startup-v2. Observe that
# event before acquiring the real one-byte native startup lock.
while time.monotonic() < deadline:
trace = read_trace()
if b"msg=parallel.extract" in trace or b"msg=pass.definitions" in trace:
gate.seek(0)
try:
msvcrt.locking(gate.fileno(), msvcrt.LK_NBLCK, 1)
except OSError:
time.sleep(0.01)
continue
locked = True
break
if process.poll() is not None:
raise SetupFailure("index exited before the gate collision:\n" +
excerpt(trace))
time.sleep(0.01)
if not locked:
raise SetupFailure("index never entered local work:\n" + excerpt(trace))
while time.monotonic() < deadline:
trace = read_trace()
if b"Completed index_repository (" in trace:
break
if process.poll() is not None:
print("RED: index exited while a live peer held startup-v2:\n" +
excerpt(trace))
return False
time.sleep(0.01)
else:
raise SetupFailure("CLI did not finish its work:\n" + excerpt(trace))
# The outer CLI flushes this completion message after receiving
# the worker result and closing its daemon session, immediately
# before local cleanup. pipeline.done is too early: worker result
# handling can outlast a short lock hold. Keep the gate for longer
# than the old 500 ms cleanup deadline after this real boundary.
time.sleep(1)
stayed_alive = process.poll() is None
gate.seek(0)
msvcrt.locking(gate.fileno(), msvcrt.LK_UNLCK, 1)
locked = False
stdout, _ = process.communicate(timeout=30)
trace = read_trace()
if not stayed_alive or process.returncode != 0 or b"cleanup_timeout" in trace:
print("RED: completed work must wait out startup contention and exit 0 "
"after release (rc=%s):\n%s" %
(process.returncode, excerpt(stdout + trace)))
return False
print("PASS: completed local work waited for the live startup owner and exited 0")
return True
finally:
if locked:
gate.seek(0)
msvcrt.locking(gate.fileno(), msvcrt.LK_UNLCK, 1)
if process is not None and process.poll() is None:
subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)],
capture_output=True, timeout=30)
process.wait(timeout=30)
status = run_cli(binary, cache, ["daemon", "status"], timeout=30)
kill_pid(pid_from(out_text(status)))
def section_cold_storm(binary, work):
cache = section_dirs(work, "storm")
results = _parallel_one_shots(binary, cache, 6)
@@ -558,6 +677,7 @@ def main():
section_stop_refuses_busy,
section_crash_recovery,
section_churn_stability,
section_local_cleanup_waits_for_startup_gate,
section_cold_storm,
]
failed = []