cpp-semantic-graph
by FS-Yuao
README.md
# cpp-semantic-graph
> A C++ semantic knowledge graph — let AI understand your C++ codebase precisely
[中文文档](README_zh.md)
Builds a semantic knowledge graph of a C++ codebase for AI coding assistants
(Claude Code, Cursor, Windsurf, etc.) and exposes it through 9 query tools over
the [MCP protocol](https://modelcontextprotocol.io/). The AI can then search
class definitions, look up inheritance, trace call chains, and analyze the blast
radius of a change — no file hopping, no grep.
## Why this project exists: clang, not tree-sitter
Most general-purpose code-graph tooling parses source with **tree-sitter** — a
fast, incremental **syntax** parser. That choice is fine for many languages,
but it is the wrong tool for a **C++ semantic** graph.
**The distinction is syntax vs. semantics:**
- A **syntax parser** answers *"do these tokens form a valid C++ program, and
what does the tree look like?"* — it does **not** resolve what a name refers
to.
- **Semantic analysis** (what a compiler front-end does) answers *"which entity
does this symbol denote, what is its type, and how does it relate to
others?"* — this requires **type resolution and name lookup**.
C++ is a language where a great deal of information lives **in the type system,
not on the page**. Consider:
```cpp
namespace nv {
struct Base { virtual void PerformUpdate() = 0; };
template<typename T> struct Middle : virtual Base { // virtual inheritance + template
void PerformUpdate() override {}
};
struct ChipUpdate : Middle<SoC>, NonCopyable, public Logger {}; // multiple inheritance
}
```
A syntax parser (tree-sitter) can see that `ChipUpdate` is a class followed by
three base-specifier text fragments. It **cannot** determine:
- That `Middle<SoC>` is the specialization `nv::Middle<SoC>` — requires
**template instantiation**.
- Which namespace `Logger` lives in — requires **name lookup**.
- That the root of the chain is `virtual Base` — requires **expanding the
template and following the referenced declaration**.
- Which base-class method `PerformUpdate() override` actually overrides —
requires **cross-class virtual-function matching**.
These all require type resolution, which is exactly what a compiler front-end
(clang) provides and what a syntax parser structurally cannot. This is not
tree-sitter being "bad" — it is intentionally type-free, single-file, and fast,
designed for syntax highlighting and code folding. Using it to build a C++
*semantic* graph is simply the wrong tool for the job.
**This project uses clang/libclang's semantic AST instead.** The difference is
visible in the parser:
- `base_spec.referenced` — resolves a base class to its **real declaration
cursor** across files and namespaces (`parser/ast_visitor.py`).
- `clang_isVirtualBase` — detects **virtual inheritance**, the C++-specific
diamond-inheritance semantics that syntax trees have no notion of.
- `cursor.is_virtual_method()` / `is_pure_virtual_method()` — virtual-function
semantics.
- `access_specifier` — distinguishes public/protected/private inheritance.
- Cross-TU override matching — base and derived classes often live in different
files; clang's `referenced` cursor links them, which a per-file syntax parse
cannot.
## Why do you need it?
Common pain points when an AI assistant tries to understand C++ code:
| Pain point | cpp-semantic-graph's answer |
|------------|------------------------------|
| "Where is this class defined?" | `cpp_search_class("FirmwareUpdate")` → namespace + file location |
| "Who calls this function?" | `cpp_get_callers("QueryBootChain")` → all callers |
| "What does changing this header affect?" | Incremental update recursively walks include deps, re-parses only affected TUs |
| "Which overrides exist for this virtual?" | `cpp_get_overrides("PerformUpdate", "DeviceAdapter")` → all implementations |
| "What does this module's architecture look like?" | `cpp_traverse_graph("FirmwareUpdate")` → multi-hop traversal |
## ✨ Core features
- **9 MCP tools**: class search, function search, inheritance, call chains (caller/callee), overrides, file symbols, multi-hop traversal, doc search
- **Incremental updates**: based on the include-dependency graph, a single `.cpp` change refreshes in seconds (16× faster than a full rebuild)
- **Doc fusion**: bidirectional linking between project docs and code; searching docs auto-locates related code
- **Plug-and-play**: one YAML config + `compile_commands.json` to start; the MCP server auto-registers with AI tools
- **Project-agnostic**: no project-specific hardcoding in the schema or tool definitions — portable to any C++ project
## 🚀 Quick start
### Prerequisites
- Python 3.10+
- libclang (matching your LLVM version)
- `compile_commands.json` (generated by CMake/Bear)
### 1. Install
```bash
git clone https://github.com/FS-Yuao/cpp-semantic-graph.git
cd cpp-semantic-graph
# 一键创建 venv 并安装核心依赖 (推荐)
./setup_env.sh
# 默认在项目内创建 .venv;可选:
# ./setup_env.sh /path/to/venv 指定 venv 路径
# ./setup_env.sh --with-docs 同时装 doc embedding 依赖 (含 torch,体积大)
# 或手动创建:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
> **前置**: 系统 libclang (匹配 LLVM 版本;Ubuntu: `apt install libclang-18-dev`)。
> clang bindings 版本须与系统 libclang 一致 (本机 libclang-18 对应 `clang>=18,<19`,换 LLVM 版本时同步改 `requirements.txt`)。
### 2. Prepare compile_commands.json
If your project builds with CMake:
```bash
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ...
```
Without CMake, use [Bear](https://github.com/rizsotto/Bear) to intercept build commands:
```bash
bear -- make
```
### 3. Write the config file
Create `cpp_semantic_graph.yaml` (the only file you need to edit):
```yaml
project:
name: "your_project"
compile_commands: "/path/to/your/project/compile_commands.json"
# Source scope — decides what counts as "project code"
source_paths:
- "src"
- "include"
# Generated-code paths (e.g. ARA COM src-gen, protobuf)
generated_paths:
- "src-gen"
# Paths to ignore entirely
exclude_paths:
- "thirdparty"
- "build"
- "test/mock"
# libclang path (adjust for your system)
libclang_path: "/usr/lib/llvm-18/lib/libclang.so.1"
parse_options:
skip_function_bodies: false # must be false to extract call relations
max_workers: 4 # parallel parse processes
```
### 4. Full parse
> ⚠️ **Important**: stop the MCP server before a full parse, or the DB connection
> it holds will cause write failures (`disk I/O error`).
> Stop with `pkill -f "run_server.py"`, or disable the MCP server in Claude Code
> settings and restart.
```bash
# 1. Stop the MCP server (required!)
pkill -f "run_server.py"
# 2. Full parse
python3 -m cpp_semantic_graph full-parse \
--config cpp_semantic_graph.yaml \
--db semantic_graph_full.db
# 3. Restart the MCP server (after parsing finishes)
```
After parsing, the database contains:
- **Nodes**: classes, structs, functions (with signature, namespace, file location)
- **Edges**: inheritance, calls, overrides, belongs_to, type aliases (`type_alias`), using-declarations (`using_decl`), friends (`friend_of`), etc. (the `instantiates` template-instantiation edge is not yet enabled due to libclang AST shape — see [Complex scenarios](#-complex-scenarios-templatealiasfriend))
- **Include dependencies**: the include graph between translation units
- **Doc associations**: doc sections ↔ code entities (optional)
### 5. Start the MCP server
```bash
# Set the database path
export CPP_GRAPH_DB=/path/to/semantic_graph_full.db
# Start the MCP server (stdio transport)
python3 -m cpp_semantic_graph.mcp_server.server
```
### 6. Register with AI tools
#### Claude Code
Edit `~/.claude.json`, add under `mcpServers`:
```json
{
"mcpServers": {
"cpp-semantic-graph": {
"type": "stdio",
"command": "python3",
"args": ["/absolute/path/to/cpp-semantic-graph/mcp_server/run_server.py"],
"env": {
"CPP_GRAPH_DB": "/absolute/path/to/semantic_graph_full.db"
}
}
}
}
```
#### Cursor
Edit `.cursor/mcp.json`:
```json
{
"mcpServers": {
"cpp-semantic-graph": {
"command": "python3",
"args": ["/absolute/path/to/cpp-semantic-graph/mcp_server/run_server.py"],
"env": {
"CPP_GRAPH_DB": "/absolute/path/to/semantic_graph_full.db"
}
}
}
}
```
#### Windsurf / other MCP clients
Configuration is similar; refer to each tool's MCP docs. The core parameters:
- **command**: `python3`
- **args**: `["/path/to/mcp_server/run_server.py"]`
- **env.CPP_GRAPH_DB**: absolute path to the database
> 💡 **Tip**: the `CPP_GRAPH_PROJECT` env var sets the project name (used in MCP instructions); if unset it is inferred from the DB path.
---
## 🛠️ 11 MCP tools
| # | Tool | Purpose | Typical scenario |
|---|------|---------|------------------|
| 1 | `cpp_search_class` | Search class definitions by name | "Where is FirmwareUpdate defined?" |
| 2 | `cpp_search_function` | Search function definitions by name | "What's the signature of PerformUpdate?" |
| 3 | `cpp_get_inheritance` | Query a class's inheritance | "Which classes derive from DeviceAdapter?" |
| 4 | `cpp_get_callers` | Who calls a given function | "Who calls QueryBootChain?" |
| 5 | `cpp_get_callees` | What a given function calls | "What does PerformUpdate call internally?" |
| 6 | `cpp_get_overrides` | All overrides of a virtual function | "Which overrides exist for PerformUpdate?" |
| 7 | `cpp_get_file_symbols` | All symbols in a file | "What's in chip_update.cpp?" |
| 8 | `cpp_traverse_graph` | Multi-hop graph traversal | "What does changing FirmwareUpdate affect?" |
| 9 | `cpp_search_docs` | Search project docs (-> related code) | "The OTA upgrade-flow design doc" |
| 10 | `cpp_blast_radius` | Blast radius of a change (recursive callers + override expansion + file aggregation) | "I'm changing PerformUpdate — which files must I review?" |
| 11 | `cpp_get_code_docs` | Reverse: code symbol -> docs describing it | "What design docs describe PerformUpdate?" |
### Tool details
#### `cpp_search_class(name, exact=False)`
Search C++ class definitions by name. Supports fuzzy matching.
```
cpp_search_class("FirmwareUpdate")
-> ## Search results: class "FirmwareUpdate" (1)
### update::FirmwareUpdate
- File: chip_update.h:15-120
```
#### `cpp_search_function(name, class_name="")`
Search function definitions by name. Optionally restrict by owning class.
```
cpp_search_function("PerformUpdate", class_name="FirmwareUpdate")
-> ## Search results: function "PerformUpdate" (1)
### FirmwareUpdate::PerformUpdate [virtual, override]
- Signature: void PerformUpdate() override
- File: chip_update.cpp:45
```
#### `cpp_get_inheritance(class_name, direction="down", depth=1)`
Query inheritance. `direction="down"` for subclasses, `"up"` for base classes. `depth=-1` recurses fully.
```
cpp_get_inheritance("DeviceAdapter", direction="down", depth=-1)
-> ## Subclasses of DeviceAdapter (4)
- update::FirmwareUpdate --public--> DeviceAdapter
- update::McuUpdate --public--> DeviceAdapter
- ...
```
#### `cpp_get_callers(function_name, class_name="")`
Who calls a given function (impact analysis).
```
cpp_get_callers("QueryBootChain")
-> ## Code that calls "QueryBootChain" (3)
### UpdateManager::CheckBootChain
- File: update_manager.cpp:128
- Call type: calls_direct
```
#### `cpp_get_callees(function_name, class_name="")`
What a given function calls (call-chain analysis).
```
cpp_get_callees("PerformUpdate", class_name="FirmwareUpdate")
-> ## Code called by "PerformUpdate" (8)
...
```
#### `cpp_get_overrides(function_name, class_name)`
All override implementations of a virtual function. `class_name` is the base class that declares the virtual (required).
```
cpp_get_overrides("PerformUpdate", class_name="DeviceAdapter")
-> ## Overrides of "PerformUpdate" (4)
### update::FirmwareUpdate::PerformUpdate
- Signature: void PerformUpdate() override
- File: chip_update.cpp:45
- Overrides base: DeviceAdapter
```
#### `cpp_get_file_symbols(file_path)`
All class and function symbols in a file. `file_path` supports partial matching.
```
cpp_get_file_symbols("chip_update.h")
-> ## File symbols: chip_update.h (12 total)
### Classes/structs (2)
1. ### [class] update::FirmwareUpdate
### Functions (10)
...
```
#### `cpp_traverse_graph(start, relation_types=None, direction="outgoing", depth=3, max_results=50)`
Multi-hop graph traversal — the most flexible query. Walks related nodes along specified relation types.
Common relation types: `inherits_public`, `inherits_protected`, `overrides`, `belongs_to`, `calls_direct`, `calls_virtual`, `calls_callback`, `doc_describes_code`, `code_refers_to_doc`
```
cpp_traverse_graph("FirmwareUpdate", depth=2, max_results=30)
-> ## Traversal results: from "FirmwareUpdate" (18 nodes)
Depth: 2, edges traversed: 22
### Related nodes
- [class] update::FirmwareUpdate (chip_update.h)
- [class] update::DeviceAdapter (base_device_update.h)
- [function] update::FirmwareUpdate::PerformUpdate (chip_update.cpp)
...
```
#### `cpp_search_docs(keyword, tag="", max_results=10, min_confidence=0.7)`
Search project docs, returns doc sections + associated code.
```
cpp_search_docs("升级", tag="架构设计")
-> ## Doc search: "升级" (3 results)
### OTA complete upgrade flow
- File: docs/OTA_flow/OTA_COMPLETE_FLOW.md
- Word count: 2450
- Tags: 架构设计, OTA
Associated code:
- [class] DeviceAdapter confidence=0.92
- [class] FirmwareUpdate confidence=0.88
```
Note: default `min_confidence=0.7` filters low-quality co-occurrence associations
(confidence=0.6 makes up 63%, mostly noise like "刷写" matching Data/Response).
Pass 0.0 to see all associations.
#### `cpp_get_code_docs(symbol, min_confidence=0.0, max_results=10)`
Reverse lookup: given a code symbol, returns doc sections describing it (design
docs / HLD / architecture docs). Complement of `cpp_search_docs` — pass a code
symbol directly without thinking of keywords.
```
cpp_get_code_docs("PerformUpdate")
-> ## Docs describing "PerformUpdate" (6 sections)
### 3. Current flow
- Doc: SOC A/B partition switch design
- File: AB_Switch/AB_PARTITION_SWITCH_DESIGN.md:62-81
- Tags: 架构设计, A/B分区
### 4.3 Components
- File: ADC4.0_System_architecture/ADC4.0_OTA_SW_HLD.md:342-578
- Tags: 系统架构
```
Note: reverse associations are mostly content_scan (confidence=0.6). Unlike
forward, code symbols appearing in docs are usually genuinely about them, so
0.6 is typically valid. Default returns all, rely on `max_results` to bound.
---
## 📖 CLI usage
Besides MCP tools, a CLI is provided for direct queries:
```bash
# Search a class
python3 -m cpp_semantic_graph search-class "FirmwareUpdate"
# Query inheritance
python3 -m cpp_semantic_graph inheritance "DeviceAdapter" --direction down --depth -1
# Search a function
python3 -m cpp_semantic_graph search-func "PerformUpdate"
# File symbols
python3 -m cpp_semantic_graph file-symbols "chip_update.cpp"
# Include dependencies
python3 -m cpp_semantic_graph include "base_device_update.h" --mode all
# DB stats
python3 -m cpp_semantic_graph stats
```
### Incremental update
After a code change, the incremental update re-parses only affected translation units:
```bash
# Based on git diff (default HEAD~1)
python3 -m cpp_semantic_graph incremental --base HEAD~1
# Specify files manually
python3 -m cpp_semantic_graph incremental --files chip_update.cpp,base_device_update.h
# Detect only, don't execute
python3 -m cpp_semantic_graph incremental --files chip_update.cpp --dry-run
# Skip doc-association rebuild (faster)
python3 -m cpp_semantic_graph incremental --files chip_update.cpp --skip-associations
```
---
## 📐 Architecture
```
┌─────────────────────────────────────────────────┐
│ AI tool layer │
│ Claude Code / Cursor / Windsurf / other MCP │
└────────────────────┬────────────────────────────┘
│ MCP protocol (stdio)
┌────────────────────▼────────────────────────────┐
│ MCP Server (9 tools) │
│ FastMCP + Lazy-init DB connection + Markdown │
└────────────────────┬────────────────────────────┘
│ Python API
┌────────────────────▼────────────────────────────┐
│ Query layer (query/) │
│ GraphQuery │ CallQuery │ PolymorphismQuery │
│ TraverseQuery │ DocQuery │ IncludeQuery │
└────────────────────┬────────────────────────────┘
│
┌────────────────────▼────────────────────────────┐
│ Data layer (db/) │
│ SQLite + 9 indexes + CASCADE constraints │
│ node │ edge │ include_dep │ parse_status │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Parser layer (parser/) │
│ AST Visitor (libclang) │ CompileDB │ Config │
│ ChangeDetector │ ImpactAnalyzer │
└─────────────────────────────────────────────────┘
```
### Core data model
**Nodes (node table)**: classes, structs, functions — with namespace, file location, signature, etc.
**Edges (edge table)**: relations between nodes; direction convention:
- `inherits`: from=subclass → to=base class
- `calls`: from=caller → to=callee
- `belongs_to`: from=function → to=owning class
- `overrides`: from=derived function → to=base function
**include_dep table**: include dependencies between translation units — the core basis for incremental updates.
---
## 🧩 Complex scenarios (template/alias/friend)
| Feature | Relation edge | Status | Notes |
|---------|---------------|--------|-------|
| Type alias `using Alias = T` | `type_alias` | ✅ Enabled | Alias node stored + edge links to target; when the target comes from an external library the edge may be dropped as dangling, but the alias node keeps `target_type` metadata |
| using-declaration `using B::func` | `using_decl` | ✅ Enabled | Subclass function → base function; this project's source has no ordinary using-declarations, only 1 literal operator not extracted |
| Friend `friend class F` | `friend_of` | ✅ Enabled | friend → host class; this project's source has no friend declarations, 0 edges as expected |
| Template instantiation | `instantiates` | ⏸️ Not yet enabled | libclang does not produce a standalone CLASS_DECL node for template specializations (the specialization name only appears in the spelling of CONSTRUCTOR/TYPE_REF), so `walk_preorder` finds no class declaration containing `<` and extraction yields nothing. The extractor code is retained, to be enabled once we switch to LibTooling or rebuild from TYPE_REF |
> This area previously had "extractors written but not wired into the pipeline" (dead code). AliasExtractor/FriendExtractor are now integrated into `SemanticExtractor.parse()`, and output was verified against clangd.
---
## 🔄 Incremental-update mechanism
```
1. ChangeDetector ──-> detect file changes (git diff / manual)
2. ImpactAnalyzer ──-> analyze blast radius (.h -> recursive includers)
3. Delete stale data ──-> delete out-edges (not shared nodes) + include_dep + parse_status
4. Re-parse ──-> SemanticExtractor.parse() × affected TUs
5. Upsert new data ──-> node update / edge dedup
6. Cleanup remnants ──-> delete functions/classes no longer in the file
7. Rebuild doc links ──-> content_scan (optional embedding)
```
**Core deletion strategy**: only out-edges (edges whose from_id is in that file) are deleted; in-edges are kept; nodes are updated via upsert. This ensures classes/functions shared in headers are never mistakenly deleted.
| Scenario | Affected TUs | Update time |
|----------|-------------|-------------|
| Single .cpp change | 1 | ~11s |
| Single .h change | recursive includers | depends on TU count (7 TUs ~76s) |
| Idempotency (second run) | unchanged | edge count stable ✅ |
> **Note**: the git-diff auto-detect mode (`--base HEAD~1`) may not work under Android repo-tool-managed repos (`_ensure_repo_root` finds the repo top-level `.git` rather than the sub-repo's). Using `--files` to specify changed files manually is more reliable.
---
## 📚 Doc fusion
Bidirectionally links project docs (Markdown) with code entities, so searching docs auto-locates related code.
**This project has doc fusion configured**: 58 docs → 546 sections → 1,756 association edges (doc_describes_code + code_refers_to_doc). `cpp_search_docs` works out of the box.
### Configuration
Create `config/doc_config.yaml`:
```yaml
doc_dirs:
- "docs/"
exclude_patterns:
- "*.html"
- "*/build/*"
# Auto-tag by directory
tag_rules:
- path_pattern: "architecture/**"
tags: ["架构设计"]
- path_pattern: "api/**"
tags: ["接口规约"]
section_split:
min_level: 2 # split on ##
min_word_count: 20 # sections under 20 words are merged
# Manual precise links (no intrusion into doc text)
manual_links:
- doc: "architecture/UPDATE_FLOW.md"
heading: "Update flow"
code:
- "DeviceAdapter"
- "FirmwareUpdate"
- "PerformUpdate"
```
### Embedding association (optional, experimental)
> ⚠️ **Experimental — not validated.** The embedding pipeline has not been tested
> end-to-end. It is retained from the design phase but excluded from the default
> association strategy. Results and accuracy are unknown.
After installing `sentence-transformers`, docs and code can be auto-associated by semantic similarity. This is **not enabled by default** — pass `--rebuild-embeddings` to `incremental` or call `ingest_embedding_associations()` explicitly:
```bash
pip install sentence-transformers
```
Defaults to `all-MiniLM-L6-v2`. For Chinese projects, `bge-small-zh-v1.5` or `multilingual-e5-small` is recommended.
---
## 🧠 Memory layer (findings) — stop re-deriving the same conclusions
The `doc_graph/` sub-project extends doc fusion into a **team memory hub**: durable
conclusions from AI sessions (constraints, lessons, decisions, facts, risks) are
stored as first-class records, **anchored to code symbols**, and automatically
surfaced whenever those symbols are queried.
```
Session derives a conclusion
→ record_finding_tool (idempotent, symbol-anchored)
→ next session queries the symbol → finding surfaces automatically
→ code evolves → check_finding_freshness marks it stale
```
**What it adds on top of doc fusion:**
| Capability | How |
|------------|-----|
| Finding store | Independent SQLite tables (finding / finding_symbol / finding_fts), survives full graph rebuilds |
| Symbol anchoring | Findings link to code symbols; any doc-graph query touching that symbol brings them back |
| 3-way RRF retrieval | FTS5 + LIKE + symbol backref run in parallel, fused with Reciprocal Rank Fusion |
| CJK & camelCase tokenization | `QueryBootChain` is found by "boot chain"; Chinese matched per-character |
| Synonym expansion | Built-in zh↔en tech table (崩溃↔crash↔挂↔abort…) — colloquial queries hit formal conclusions |
| Vector rerank (optional) | Local bge-small via fastembed, CPU-only, reranks lexical hits only |
| Staleness detection | Anchored symbols checked against the code graph; vanished symbols → `stale` |
**Honest engineering notes** (all measured, see `doc_graph/` docs):
- Bi-encoders (bge-small 33M, e5-large 560M) **cannot** separate relevant from
irrelevant pairs for "short zh query vs short zh conclusion" — everything sits
in a dense 0.8+ similarity band. Vectors are therefore used **only for reranking
lexical hits**, never as an independent recall source.
- A cross-encoder (bge-reranker-base) gives reliable *strong negatives* but
unreliable *weak positives* — kept as an optional gate, not shipped as default.
- The winning combo is boring: caller-side rewriting (the MCP caller *is* an LLM)
+ a deterministic synonym table. Zero API, zero GPU, fully offline.
Runs as a **streamable-HTTP singleton** (systemd), so all MCP clients share one
process — see `doc_graph/README.md`.
---
## 🧪 Validation
After a full parse, you can run correctness validation (cross-checked against clangd):
```bash
python3 -m cpp_semantic_graph full-parse \
--config cpp_semantic_graph.yaml \
--validate \
--baseline validation/clangd_baseline.json
```
---
## 📋 Dependencies
### Core (required)
| Package | Version | Purpose |
|---------|---------|---------|
| `clang` | ≥18 | libclang Python bindings, AST parsing |
| `PyYAML` | ≥6.0 | Config file parsing |
| `mcp` | ≥1.0 | MCP protocol implementation (FastMCP) |
### Doc fusion (optional)
| Package | Version | Purpose |
|---------|---------|---------|
| `sentence-transformers` | ≥2.0 | doc-code semantic association |
| `torch` | ≥2.0 | sentence-transformers dependency |
### Install
```bash
# Core
pip install clang PyYAML mcp
# Doc fusion (optional)
pip install sentence-transformers
```
Or with requirements files:
```bash
pip install -r requirements.txt # core
pip install -r requirements-docs.txt # doc fusion (optional)
```
---
## 🗂️ Project structure
```
cpp_semantic_graph/
├── __init__.py # package declaration
├── __main__.py # python -m entry point
├── cli.py # CLI tools (search/inheritance/incremental/...)
├── pipeline.py # full-parse pipeline
├── incremental_updater.py # incremental-update orchestrator
│
├── parser/ # parser layer
│ ├── ast_visitor.py # AST extractor (libclang)
│ ├── config.py # project config loading
│ ├── compile_db.py # compile_commands.json parsing
│ ├── change_detector.py # file-change detection (git diff)
│ ├── impact_analyzer.py # blast-radius analysis
│ ├── doc_parser.py # doc parser
│ ├── doc_association.py # doc-code association
│ ├── association_ingester.py # association-edge ingestion
│ └── models.py # data models
│
├── query/ # query layer
│ ├── graph_query.py # class/function/file-symbol queries
│ ├── call_query.py # call-relation queries
│ ├── polymorphism_query.py # polymorphism queries
│ ├── traverse.py # multi-hop traversal
│ ├── doc_query.py # doc-fusion queries
│ ├── include_query.py # include-dependency queries
│ ├── architecture_query.py # architecture-overview queries
│ └── fusion_query.py # fusion queries
│
├── db/ # data layer
│ ├── graph_db.py # SQLite operations
│ ├── importer.py # JSON->SQLite import
│ ├── schema.sql # schema + indexes
│ └── relation_types.py # relation-type enum
│
├── mcp_server/ # MCP server
│ ├── server.py # FastMCP server + 9 tools
│ └── run_server.py # launch script
│
├── validation/ # correctness validation
│ ├── accuracy_validator.py # accuracy validator
│ └── clangd_baseline.py # clangd baseline
│
├── config/ # config templates
│ ├── doc_config.yaml # doc-config example
│ └── template_whitelist.yaml # template whitelist
│
└── cpp_semantic_graph.yaml # project config (user-authored)
```
---
## ❓ FAQ
### Q: Which LLVM/libclang version do I need?
One matching the LLVM used to compile your project. How to check:
```bash
# clang version
clang --version
# corresponding libclang path
ls /usr/lib/llvm-*/lib/libclang.so*
```
Specify it via `libclang_path` in the config.
### Q: How do I generate compile_commands.json?
- **CMake project**: `cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ...`
- **Make project**: `bear -- make`
- **Ninja project**: `bear -- ninja`
- **Bazel project**: use [bazel-compile-commands](https://github.com/kiron1/bazel-compile-commands)
### Q: Are incremental updates safe?
Yes. The deletion strategy only removes out-edges (edges whose from_id is in that file), not nodes. Classes/functions shared in headers are updated via upsert and never mistakenly deleted. For extreme cases (e.g. renaming a class), a full rebuild is recommended.
### Q: Which AI tools are supported?
Any AI tool that supports the MCP protocol: Claude Code, Cursor, Windsurf, Continue, etc. The MCP server uses stdio transport, the most broadly supported transport.
### Q: How big is the database?
A typical C++ project (~100 translation units): about 1500 nodes / 5000 edges / 20000 include relations, SQLite file ~5-10 MB. With doc fusion, node and edge counts roughly double, DB ~5-15 MB.
### Q: How does this differ from clangd?
| | cpp-semantic-graph | clangd |
|---|---|---|
| Data storage | offline SQLite graph | real-time AST |
| Query scope | cross-file, cross-module | single file + index |
| Call chains | full call graph + multi-hop traversal | single-hop references |
| Doc association | supported | not supported |
| Incremental update | based on include-dependency graph | real-time |
| Best for | architecture understanding, blast-radius analysis | real-time editing, signature lookup |
**They complement each other**: use clangd for day-to-day editing, cpp-semantic-graph for architecture understanding and impact analysis.
### Q: Why are some call edges missing? (conditional compilation)
cpp-semantic-graph builds its AST from the **preprocessed** translation unit of a single compile configuration — the one recorded in `compile_commands.json`. The C preprocessor drops every `#if` / `#ifdef` / `#else` branch not selected by the current `-D` flags **before** libclang sees the code, so any function call inside a non-selected branch is invisible to the graph. `get_callers` / `get_callees` return empty for such a call even though it clearly exists in the source.
This is an inherent limitation of the libclang single-config method, **not a bug** — clangd has the same blind spot, since it also parses one configuration.
**Typical shape.** A function whose *signature* sits outside the `#if` still gets a node, but calls inside the non-selected `#else` body are missing. The node therefore exists yet appears to have no callers / no callees for those calls:
```c
bool Foo::CompareVersion(const char* path) { // signature -> node exists
#if SKIP_CHECK // current config: SKIP_CHECK == 1, this branch kept
LogWarning("skipped"); // -> call edge extracted (Logger::Warning)
#else // dead branch, removed by preprocessor
ExtractVersion(path); // -> NO call edge (invisible to libclang)
CompareVersions(...); // -> NO call edge
#endif
}
```
**How to tell a blind spot from a real omission.** If `get_callers` is empty for a function you can see called in source, first check whether the call site is inside a `#if`/`#else` block, and whether the controlling macro's current value selects that branch. The graph only reflects the configuration that was compiled; it is correct for that configuration.
**Mitigation.** To analyze another configuration, regenerate `compile_commands.json` with the corresponding `-D` flags and rebuild the graph. A single preprocessed AST cannot show all configurations at once; there is no way to union them from one parse.
---
## 📄 License
MIT License
---
## 📋 Test reports
| Report | Description |
|--------|-------------|
| [TEST_REPORT.md](tests/TEST_REPORT.md) | Comprehensive test report (functionality/accuracy/efficiency/bug-fix) |
| [TEST_CASES.md](tests/TEST_CASES.md) | Test-case table (62 cases, 98.4% pass) |
| [TEST_THREE_LAYERS.md](tests/TEST_THREE_LAYERS.md) | Three-layer tests (question→tool call→code verification, 25 real questions) |
| [TEST_DOC_FUSION.md](tests/TEST_DOC_FUSION.md) | Doc-fusion tests (27 cases, 89% pass) |
| [PROJECT_EVALUATION.md](tests/PROJECT_EVALUATION.md) | Overall project evaluation (six-dimension scoring + comparison + conclusion) |
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues