diff --git a/README.md b/README.md index ee1db98bb..74a2eaf71 100644 --- a/README.md +++ b/README.md @@ -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. -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, diff --git a/docs/index.html b/docs/index.html index e39ded17e..2c48b3910 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,838 +3,440 @@ - codebase-memory-mcp — Code Intelligence Knowledge Graph for AI Coding Agents - - + Codebase Memory — Connect your code, docs and infrastructure + - - - - - + + + - - - - - - - + + + - - - - + + + - - - + - - - - - - - - - - -
-
-

codebase-memory-mcp

- -

- The fastest, most efficient code intelligence engine for AI coding agents. It indexes any - repository into a persistent knowledge graph — full-indexing an average repo in seconds and the - Linux kernel in 3 minutes — so your agent answers structural questions with ~120x fewer tokens. - Tree-sitter parsing across 162 languages, Hybrid LSP type resolution, native executable with verified assets. -

-
-
~120x
fewer tokens
-
158
languages
-
3 min
Linux kernel index
-
44
client surfaces
+ + +
+
+
+
+

Local code intelligence · Open source

+

Understand the
whole repository.

+

Your code, documentation and infrastructure, connected in one persistent graph. Give your agent structural context. Follow the evidence yourself in CBM Lens.

+ +

macOS · Linux · Windows  /  Native C · MIT licensed

+
+
+ For your agent

Precise graph queries, source evidence and coverage checks through MCP.

+ For you

Source navigation, architecture maps and a visible path from question to code.

+
-
- -
- 3D knowledge-graph visualization of the codebase-memory-mcp graph showing thousands of nodes and edges -

Built-in 3D graph visualization (UI variant) — explore your knowledge graph at localhost:9749.

-
- - - -
-

What is codebase-memory-mcp?

-

- codebase-memory-mcp is 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. Instead of reading files one at a time, - an AI coding agent queries the graph — answering structural questions with roughly 120x fewer - tokens. It parses 162 languages and ships as a native executable with authenticated release-owned assets; - the native install needs no language runtime. -

-

- It is a structural-analysis backend, not a chatbot: there is no embedded LLM and no API key. Your - MCP client (Claude Code, or any MCP-compatible agent) is the intelligence layer; codebase-memory-mcp - builds and serves the graph. All processing happens locally — your code never leaves your machine. -

-
- -
-

Why do AI agents waste tokens exploring code?

-

- AI coding agents explore codebases by reading files one at a time. Every structural question - triggers a cascade of grep → read file → grep again → read more files. The cost compounds fast. -

-

- Across five structural questions about a real codebase, file-by-file search consumed - ~412,000 tokens; the same questions answered from the - knowledge graph took ~3,400 tokens — a ~120x reduction. -

-

- The win is not about fitting the context window. It is cost (at $3–15 per million tokens, exploration - adds up), latency (sub-millisecond graph queries versus seconds of file reading), and accuracy - (less noise means better answers and no "lost in the middle" problem). -

-

Source: project benchmark, 5 structural queries — see the - full benchmark report.

-
- -
-

How do I install codebase-memory-mcp?

-

- Install with a single command, then tell your agent to index the project. The native runtime set contains - an executable with verified assets for macOS, Linux, and Windows — no Docker, language runtime, or API key. -

-
- # 1. One-line install (macOS / Linux). Add --ui for the 3D graph UI.
- curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash

- # 2. The installer configures detected clients; explicit flags cover conditional clients.

- # 3. Restart your agent, then say:
- "Index this project" +
+
CBM LENS BETAcodebase-memory-mcp / architectureLOCAL WORKSPACE
+ CBM Lens architecture map showing the source folders of codebase-memory-mcp +
CBM Lens, exploring this repository. Real product captures from October 2026. Capture details ↗
+
+
+
One connected graphSource, docs and infrastructure
+
17 MCP toolsPlus a CLI and CBM Lens
+
45 client surfacesAutomatic or conditional setup
+
Local processingYour index stays on your machine
-

- One command configures 45 automatic/conditional client surfaces. 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. Manual, UI, cloud, or repository-managed (not counted): Qodo, - Warp MCP, JetBrains AI/ACP, GitHub Copilot coding agent, Jules, CodeRabbit, Replit, BLACKBOX AI, - Plandex, and SWE-agent. Warp is counted above for its detected skill installation; its MCP connection - remains manual. Windows users run install.ps1. Also available via npm, - pip, Homebrew, Scoop, AUR, and go install. -

-

- Lifecycle installation follows documented context contracts: Qoder uses SessionStart, - SubagentStart, and post-Read coverage, including its documented Windows - PowerShell executor. Kimi uses UserPromptSubmit; - on macOS/Linux, GitLab Duo gets a fail-open user SessionStart, while Devin gets - UserPromptSubmit, PostCompaction, and a deduplicated SessionStart - when Claude does not already provide it. GitLab Duo, Devin, and Factory hooks are withheld on - Windows where no deterministic shell/executor contract is documented. Cline's auto-activating file hooks - are withheld because their context output is not reliably consumed; CodeBuddy's beta hooks are not - auto-installed; Junie's EAP - SessionStart output is documented as ignored; and Cursor context hooks are withheld because - the documented events cannot safely provide race-free MCP context to read-only subagents. -

-

- Documented custom-agent formats receive three exact-owned evidence tiers: Scout for narrow provisional - discovery, Verify as the task-directed default, and Auditor for bounded, paginated, current-generation - verification. Every direct tier calls check_index_coverage for cited paths and relevant scopes, - then reads flagged ranges or skipped files directly. Kiro and Junie use positive-allowlist - --tool-profile scout and --tool-profile analysis server surfaces; - Junie selects dedicated named aliases because its subagent schema filters by server. - Qoder combines named-server selection with exact tier-specific MCP tool IDs. Factory uses - exact registered tool IDs without its additive whole-server mcpServers field. - A foreign Junie alias is preserved and causes the installed profiles to fail closed to parent handoff. - Cursor, Rovo, Augment, Pochi, and Cline use explicit - parent handoff where direct child MCP is unavailable or unsafe; Pochi is limited to readFile. - Neither IBM Bob surface receives an invented hook or custom agent. Amazon Q Developer - IDE defaults to ~/.aws/amazonq/default.json while preserving either existing documented - alternative. -

-
-

What is Hybrid LSP?

-

- Hybrid LSP is semantic type resolution beyond tree-sitter. Tree-sitter alone produces a syntactic - AST — it handles naming, structure, and call sites, but it cannot tell that - user.profile.display_name() resolves to Profile.display_name declared - three modules away, because it does not track imports, generics, inheritance, or stdlib types. -

-

- codebase-memory-mcp ships a lightweight C implementation of language type-resolution - algorithms, structurally inspired by and compatible with major language servers — - tsserver/typescript-go, pyright, gopls, Roslyn, Eclipse JDT, and rust-analyzer — - embedded directly into the native executable. There is no language-server process, no - per-project setup, and no API key. This layer runs alongside tree-sitter on every parse and refines - CALLS, USAGE, and RESOLVED_CALLS edges with type information, - so the graph mirrors what an IDE "Go to Definition" would resolve. -

-

Languages with full Hybrid LSP

- - - - - - - - - - - - - -
LanguageWhat it resolves
PythonImports and dotted submodule walks, dataclasses, Self return types, generics, @property, match/case patterns, SQLAlchemy 2.0 Mapped[T], Pydantic models, typing annotations, async/await, isinstance/walrus narrowing, and common stdlib.
TypeScript / JavaScript / JSX / TSXGenerics, JSX component dispatch, JSDoc inference for plain JS, .d.ts declarations, module re-exports, and method chaining via return-type propagation across a shared cross-file registry.
PHPNamespaces, traits, late-static-binding, PHPDoc inference, parameter binding, and return-type inference.
C#Global usings, file-scoped namespaces, records (incl. C# 12 primary constructors), LINQ method syntax, async Task<T>/ValueTask<T> unwrap, generic methods, var inference, and common BCL stdlib.
GoPre-built per-package cross-file registry, generics, embedded structs, interface satisfaction, and package-aware import resolution.
C / C++Shared cross-language registry: macros, typedef chains, and header-vs-source linking on the C side; templates, namespaces, auto inference, and class-hierarchy method resolution on the C++ side.
Java (new in v0.8.0)Imports (single-type, on-demand, static), class hierarchies with this/super dispatch, generics, annotations, overload matching by arity and parameter types, lambdas and method references bound to functional interfaces, and common JDK stdlib.
Kotlin (new in v0.8.0)Imports and same-package resolution, classes / objects / companion objects, extension functions, data classes, nullable-type unwrapping, scope functions (let/apply/run/also/with), infix calls, and common stdlib.
Rust (new in v0.8.0)use declarations and module paths, impl blocks and trait methods, struct fields, generics with trait bounds, operator-trait desugaring, derive-macro method synthesis, UFCS static paths, and common std prelude.
-

- The two-layer pipeline runs a fast syntactic tree-sitter pass for every one of the 162 languages, - then a type-aware Hybrid LSP pass on top for the families above. Languages without a Hybrid LSP pass - yet fall back to textual resolution, so you always get an answer. -

-
- -
- - + - diff --git a/docs/llms.txt b/docs/llms.txt index 0d0c5a49c..ba3c3dc25 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -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 diff --git a/internal/cbm/lsp/ts_lsp.c b/internal/cbm/lsp/ts_lsp.c index 7a7a43edc..8ee5e6642 100644 --- a/internal/cbm/lsp/ts_lsp.c +++ b/internal/cbm/lsp/ts_lsp.c @@ -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; diff --git a/internal/cbm/lsp/ts_lsp.h b/internal/cbm/lsp/ts_lsp.h index 5fc368f39..1096bea90 100644 --- a/internal/cbm/lsp/ts_lsp.h +++ b/internal/cbm/lsp/ts_lsp.h @@ -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 --- diff --git a/pkg/npm/README.md b/pkg/npm/README.md index 63dc88f0b..bce4d5217 100644 --- a/pkg/npm/README.md +++ b/pkg/npm/README.md @@ -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 diff --git a/scripts/package-release.sh b/scripts/package-release.sh index ff399c76d..aefaf20b3 100755 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -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/", diff --git a/server.json b/server.json index 91a46a646..2313e40ab 100644 --- a/server.json +++ b/server.json @@ -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" diff --git a/src/cli/client_adapter.c b/src/cli/client_adapter.c index 68feb6102..83a404276 100644 --- a/src/cli/client_adapter.c +++ b/src/cli/client_adapter.c @@ -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"); diff --git a/src/daemon/ipc.c b/src/daemon/ipc.c index a0c1ddbcd..2afe49976 100644 --- a/src/daemon/ipc.c +++ b/src/daemon/ipc.c @@ -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) { diff --git a/src/daemon/ipc.h b/src/daemon/ipc.h index f8195f3d5..4646618f5 100644 --- a/src/daemon/ipc.h +++ b/src/daemon/ipc.h @@ -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 */ diff --git a/src/discover/discover.c b/src/discover/discover.c index 134dd18c8..aaa604771 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -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 diff --git a/src/discover/userconfig.c b/src/discover/userconfig.c index 7e14734a2..905209a0b 100644 --- a/src/discover/userconfig.c +++ b/src/discover/userconfig.c @@ -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])) diff --git a/src/foundation/workspace.c b/src/foundation/workspace.c index 3f77c4879..c658d111c 100644 --- a/src/foundation/workspace.c +++ b/src/foundation/workspace.c @@ -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; diff --git a/src/main.c b/src/main.c index 8f31d3d79..d3f431d52 100644 --- a/src/main.c +++ b/src/main.c @@ -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; diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index f6e43b317..5bd02e4e3 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -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; diff --git a/src/pipeline/fqn.c b/src/pipeline/fqn.c index f4603dc80..f7b5b5efb 100644 --- a/src/pipeline/fqn.c +++ b/src/pipeline/fqn.c @@ -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; } diff --git a/src/pipeline/lsp_resolve.h b/src/pipeline/lsp_resolve.h index 647b3f801..419f29e26 100644 --- a/src/pipeline/lsp_resolve.h +++ b/src/pipeline/lsp_resolve.h @@ -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` diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 738e5b932..f3570904d 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -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 diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 302e76faa..73ce742ad 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -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 diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 18f2b3134..ef3ba0f10 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -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 diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index a04e0144a..ddc14e737 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -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); diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 7de52a262..8da94b096 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -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; } diff --git a/tests/test_agent_clients.c b/tests/test_agent_clients.c index df8515e73..f1bc3699e 100644 --- a/tests/test_agent_clients.c +++ b/tests/test_agent_clients.c @@ -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); } diff --git a/tests/test_daemon_ipc.c b/tests/test_daemon_ipc.c index 35ad5433c..ac03da3f3 100644 --- a/tests/test_daemon_ipc.c +++ b/tests/test_daemon_ipc.c @@ -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); diff --git a/tests/test_discover.c b/tests/test_discover.c index 3b1c5a213..c98ec26a8 100644 --- a/tests/test_discover.c +++ b/tests/test_discover.c @@ -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); } diff --git a/tests/test_edge_imports.c b/tests/test_edge_imports.c index b5f1e7fbf..266cc255a 100644 --- a/tests/test_edge_imports.c +++ b/tests/test_edge_imports.c @@ -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", "outside\n"}, + {"src/components/shared/Button.astro", + "---\nexport function buttonLabel(t: string): string { return t; }\n---\n