Skip to content

Releases: sdsrss/code-graph-mcp

v0.16.8 — callgraph tree, JSON contracts, dead-code defaults, E2E hardening

27 Apr 17:52

Choose a tag to compare

v0.16.8 — callgraph tree, JSON contracts, dead-code defaults, E2E hardening

End-to-end usability pass: simulated a Claude Code session driving every
MCP tool and CLI subcommand on real symbols. Five independent fixes for
issues that surfaced — none blocking on their own, but each was eroding
the trust-layer agents need to act on tool output.

1. callgraph rendered depth>1 nodes under the wrong parent. The
recursive CTE was collapsing duplicates with GROUP BY MIN(depth),
which lost the actual traversal parent and made every depth-N node
appear nested under the last depth-(N-1) sibling. So A→B→C plus
D→B printed as if D lived under A once B was already shown.

Fix: the CTE now tracks parent_id (the cg row that produced each
new node) on each inductive step, and dedup uses
ROW_NUMBER() OVER (PARTITION BY node_id ORDER BY depth) so the
shortest-path parent survives. CLI renderer builds a parent_id → children map per direction and recurses, so callers/callees subtrees
stay separate under --direction=both. JSON output now includes
parent_id (null for the root) for any consumer that wants to rebuild
the tree.

2. similar and deps violated the --json empty-result contract.
Both subcommands had paths that wrote nothing to stdout and exited
with stderr only — breaking machine consumers per
feedback_cli_json_empty_contract. Added: similar --json writes
[] when vector search returns no neighbors; deps --json writes a
JSON error object {"file":..., "depends_on":[], "depended_by":[], "error":"..."} when the file has no tracked imports. Two new
regression tests guard these paths.

Bonus: similar 1010 (digits as positional) used to print the
unhelpful "Symbol not found: 1010". Now nudges toward
similar --node-id 1010. And similar with an existing symbol that
hasn't been embedded yet ("No embedding for node_id 342") explains
why ((1033/1321 nodes embedded — embeddings still generating; try again shortly or pick a node with --node-id from \show X`)`).

3. MCP tool descriptions misled agents on subtle defaults. Two
tools had descriptions that didn't match their actual behavior, so
agents made decisions on stale info:

  • module_overview — caller counts include test callers, but the
    description didn't say so; agents reading "5 callers" couldn't tell
    if a function was prod-hot or only test-driven. Description now
    states "callers count includes tests" so the LLM picks a different
    tool when it actually needs prod-only callers.
  • find_references — for constants, only imports edges are
    recorded; usage sites where the const is read don't appear because
    Rust grammar emits them as identifiers without an import-context.
    Description now says "consts: imports only, not value-uses" so the
    agent escalates to grep when auditing a const for rename.

Also added one line to the MCP instructions payload telling the
agent that impact_analysis/find_dead_code/find_similar_code/
dependency_graph/trace_http_chain are CLI-only after the v0.10.0
core/advanced split — Claude Code only sees the 7 core tools, so
agents trying to invoke the advanced 5 directly via MCP would 404.

4. E2E suite was passing on dead queries. scripts/e2e-validate.js
called get_call_graph(handle_call_tool), impact_analysis( handle_call_tool), and dependency_graph(src/mcp/server.rs)
all three symbols/paths had been renamed/moved sessions ago. The
assertions only checked "response contains non-empty text", so
"[code-graph] Symbol not found: handle_call_tool" passed as
success. 24/24 green, but actually testing zero-result paths. Real
response sizes told the story: get_call_graph 221 bytes (now 2628),
impact_analysis 220 bytes (now 498), dependency_graph 304 bytes
(now 2291).

Fix: swapped the queries to stable hot symbols (handle_message,
conn, src/mcp/server/mod.rs) and added two stricter assertions:
assertNotEmptyResult(resp, label) rejects 6 known empty-result
patterns ("Symbol not found", "No callers found", etc.); the MCP
dependency_graph returns JSON, not the human "Depends on" text, so
its assertion now JSON.parses and checks depends_on is a non-empty
array.

5. dead-code falsely flagged Criterion benchmarks as orphan.
benches/indexing.rs defines three bench functions, all referenced
only via criterion_group!(benches, bench_full_index, ...). The AST
relation extractor doesn't parse macro arguments as references, so
the benches showed up as ORPHAN every time — drowning out the four
real EXPORTED-UNUSED results worth attention.

Fix: added benches/ to domain::default_dead_code_ignores(),
mirroring the existing claude-plugin/ exclusion for shell-invoked
hook scripts. The rule generalizes: any directory whose entry points
are reached through tokens the AST can't resolve (macro arguments,
shell command strings, settings.json hook definitions) belongs in
the default ignore list. CLI --no-ignore still surfaces them. New
unit test pins the policy.

Together these don't change any external schema, but they materially
improve the signal an agent gets per tool call — fewer phantom
orphans, a callgraph tree that reads like one, and an E2E suite that
actually fails when a hot symbol moves.

Full Changelog: v0.16.7...v0.16.8

v0.16.7 — install reliability

27 Apr 15:22

Choose a tag to compare

Fix-only release for /plugin install reliability. Three independent install-chain bugs caused MCP-disconnect on a clean /plugin install code-graph-mcp on a fresh machine. Each layer is fixed and tested separately so the chain is fault-tolerant on first install.

What's fixed

1. find-binary.js didn't search npm global node_modules. require.resolve('@sdsrs/code-graph-{platform}-{arch}/package.json') only walks the chain from the requiring file — it does NOT search global installs (no NODE_PATH on nvm / standard prefixes). A working npm install -g @sdsrs/code-graph-linux-x64 was previously invisible to the launcher even when the binary was sitting at ~/.nvm/.../lib/node_modules/....

Fix: new globalNodeModulesCandidates() probes 4 prefix sources (process.execPath-derived, NPM_CONFIG_PREFIX, ~/.npm-global, npm root -g); new findPlatformBinary() combines fast-path (require.resolve) with slow-path (global probe).

2. auto-update.js trusted state file over filesystem. When installedVersion === latestVersion, the no-update branch short-circuited without verifying that ~/.cache/code-graph/bin/code-graph-mcp actually exists. Once state recorded "installed v0.16.6", a wiped cache or silently-failed prior download was never repaired.

Fix: extracted downloadBinary() helper. Throttle bypassed when cache binary is missing (a hard failure overrides the 6h check window). No-update branch self-heals via downloadBinary(latest) when binary is absent.

3. mcp-launcher.js had only one fallback strategy. npm install -g @sdsrs/code-graph silently tolerates failed optionalDependencies (OS-mismatch tolerance + flaky registry), so the wrapper would install successfully while the platform binary was dropped.

Fix: second-stage fallback runs auto-update.js --silent which downloads the platform binary directly from this GitHub release into ~/.cache/code-graph/bin/, bypassing npm registry entirely.

Verification

  • 117 plugin JS + 385 Rust = 502 tests green
  • clippy 1.95 clean on both --no-default-features and default
  • 7 new tests for the install-chain helpers

Upgrade

/plugin update code-graph-mcp inside Claude Code, or fresh /plugin install code-graph-mcp if you hit the prior failure mode.

Full Changelog: v0.16.6...v0.16.7

v0.16.6 — semantic_code_search doc demotion + find_references include_tests

23 Apr 20:23

Choose a tag to compare

Two MCP tool UX fixes surfaced during a user-simulation pass

semantic_code_search: README headings no longer outrank code

Query merkle tree change detection previously returned README.md License (0.45), Features (0.44), Build (0.42) ahead of DirectoryCache struct in src/indexer/merkle.rs (0.37). Root cause: markdown heading nodes get nontrivial vector-similarity for unrelated queries, and the re-ranker had no doc-tier preference — the tool name is semantic_code_search, so prose should not dominate code for code-intent queries.

Fix: doc_penalty = 0.4 multiplier in src/mcp/server/tools.rs candidate re-rank, applied when candidate language is markdown AND caller did not pass language="markdown". Same query after fix: TOP 6 all from merkle.rs / watcher.rs; first result DirectoryCache rose to 0.60. Explicit language="markdown" bypasses the penalty (verified Installation h2 returns at 0.59 for "installation instructions").

find_references: new include_tests opt-out

upsert_file returned 27 references where 24 were test_* callers, drowning the 3 production usage sites. Inconsistent with get_call_graph and get_ast_node include_impact=true, which already default to hiding test callers.

Fix: new include_tests boolean parameter (default true to preserve rename-audit semantics — tests ARE usage sites). Response adds test_references_filtered count when callers opt out. Schema published in src/mcp/tools.rs.

Verification

  • 385 tests green (253 unit + 56 cli_e2e + 6 hardening + 44 integration + 19 parser + 6 plugin_e2e + 1 routing_bench)
  • cargo +1.95.0 clippy --no-default-features -- -D warnings clean
  • cargo +1.95.0 clippy --all-targets -- -D warnings clean
  • Live stdio verification: both fixes confirmed on a fresh binary

No changes to ast_search / get_call_graph / project_map / module_overview / get_ast_node / impact_analysis / trace_http_chain / dependency_graph / find_similar_code / find_dead_code.

Full Changelog: v0.16.5...v0.16.6

v0.16.5 — impact UNKNOWN for non-function symbols

23 Apr 19:53

Choose a tag to compare

Bugfix: impact_analysis silently reported LOW risk for non-function symbols

Three impact-analysis paths (cmd_impact, tool_impact_analysis, append_impact_summary) each maintained their own inline list of "non-function" node types to flag as UNKNOWN. The lists had drifted:

  • cmd_impact and tool_impact_analysis matched only struct|class|enum|interface|type_alias — missing constant and trait.
  • append_impact_summary (used by the core-7 get_ast_node include_impact=true — the path Claude Code actually reaches) had no type check at all.

Symptom

$ code-graph-mcp impact REL_CALLS
Impact: REL_CALLS — Risk: LOW
  0 direct callers, 0 total, 0 files, 0 routes (0 tests affected)

…even though REL_CALLS is imported by 16 files. An LLM acting on the LOW signal would confidently change the constant and break every importer.

Fix

Single source of truth in src/domain.rs:

  • is_function_node_type(&str) -> bool
  • NON_FUNCTION_IMPACT_WARNING: &str

All three paths share them. Non-function symbols with zero call-graph callers now return:

risk_level: UNKNOWN
warning: "Impact analysis tracks function call chains. This symbol is
  not a function — actual usage (imports, field access, type
  annotations, instantiation) may be broader than shown. Use
  `find_references` (MCP) or `code-graph-mcp refs <symbol>` (CLI) to
  find all references."

Function / method impact behavior is unchanged — HIGH/MEDIUM/LOW still flow from compute_risk_level.

Full Changelog: v0.16.4...v0.16.5

v0.16.4 — watcher UNC path cfg-gate

23 Apr 01:14

Choose a tag to compare

v0.16.3 canonicalized the FSEvents watcher root on every platform; on Windows that regressed the watcher because std::fs::canonicalize returns UNC paths (\\?\C:\...) while notify's ReadDirectoryChangesW backend emits plain C:\... — the same strip_prefix silently-drop-all-events failure as the macOS bug we were fixing, mirrored. Canonicalize is now #[cfg(not(windows))].

This only affected the Windows unit-test matrix; the user-facing npm package smoke (map --json) has been green since v0.16.2.

npm install -g @sdsrs/code-graph@0.16.4

Full Changelog: v0.16.3...v0.16.4

v0.16.3 — macOS watcher canonicalization

23 Apr 01:03

Choose a tag to compare

Follow-up to v0.16.2. Windows + Ubuntu matrices went green, but macOS watcher tests still timed out.

Root cause: FSEvents on macOS emits every event path via realpath. A watch on a non-canonical root (e.g. tempfile's default /var/folders/xx/T/foo, realpath /private/var/folders/...) never prefix-matches → every event dropped.

Fix: FileWatcher::start canonicalizes the root before passing it to notify. No-op when no symlinks are present (so Linux/Windows callers and already-canonical macOS roots see zero behavior change), and hardens every platform against projects with symlinked ancestor directories.

npm install -g @sdsrs/code-graph@0.16.3

Full Changelog: v0.16.2...v0.16.3

v0.16.2 — Windows path normalization + macOS watcher flake fix

23 Apr 00:52

Choose a tag to compare

Follow-up to v0.16.1. That release fixed Clippy on the 1.95 toolchain, which let the Test step run for the first time on macOS and Windows in CI — and immediately surfaced pre-existing cross-platform bugs that the previous Clippy-red baseline had been hiding. v0.16.2 addresses them.

Windows — path normalization (user-visible)

  • All relative paths in the DB, MCP/CLI output, and gitignore-prefix checks now use / on every platform. Before v0.16.2, Windows users saw pkg\scripts\foo.js in every tool response and starts_with(\"src/\") filters silently missed files with \ separators.
  • New internal normalize_rel_path(&Path) in src/indexer/merkle.rs converts \/ on Windows; no-op on Unix so no Unix behavior change.
  • Fixes 4 pipeline tests + 2 merkle tests that were red on windows-latest.

macOS — watcher test stability

  • test_watcher_detects_file_changes recv_timeout 5s → 15s (FSEvents coalescing was exceeding 5s on GH runners).
  • test_watcher_detects_changes_and_reindexes replaced 300ms fixed sleep with bounded polling (40 × 200ms).

CI

  • release.yml post-publish smoke now reads map.json via fs.readFileSync rather than require('$tmpdir/map.json') — Node.js on Win32 can't resolve Git Bash's POSIX-style mktemp -d output.

Upgrade

npm install -g @sdsrs/code-graph@0.16.2

or via plugin auto-updater.

Full Changelog: v0.16.1...v0.16.2

v0.16.1 — JS edge resolution precision + CI clippy fix

23 Apr 00:33

Choose a tag to compare

Highlights

Two correctness bugs in the JS/TS edge pipeline were surfaced by driving the tool set against this project as a user:

Parser / indexer (JS/TS)

  • Anonymous arrow scope fallbackwalk_for_relations no longer labels unnamed arrow functions <anonymous> (a name nothing resolves to). Arrows without a variable_declarator parent inherit the enclosing scope; JS/TS call_expressions at module top-level fall back to <module> so same-file edges resolve.
    • Test-file helpers like writeJson, mkHome, readCargoVersion that are referenced only from inside test(() => {...}) callbacks are no longer reported as orphan dead code.
  • Cross-file same-language resolutionrefine_ambiguous_targets now disambiguates by (a) non-test-file preference when the caller is non-test code and (b) longest byte-common path prefix with the caller. Pool is preserved on true ties so Rust bare-name scoped calls like crate::x::foo() don't get dropped and misreported as dead.

Measured on this project's own index

Metric v0.16.0 v0.16.1
JS cross-file calls fan-out edges 28 7
syncLifecycleConfig → readJson targets 4 (3 wrong) 1
refs writeJson callers found 2 5
Dead-code orphan false positives 5 0 (JS)*

* Remaining 3 orphans are bench_full_index / bench_call_graph / bench_fts5_search, registered via the criterion_group! macro — a separate follow-up.

CI

  • .github/workflows/ci.yml explicitly requests components: clippy for the 1.95 toolchain. v0.16.0 CI was red across every matrix cell with 'cargo-clippy' is not installed for the toolchain '1.95.0'.

Upgrade

npm install -g code-graph-mcp@0.16.1

or via the plugin auto-updater.

Full Changelog: v0.16.0...v0.16.1

v0.16.0 — production hardening pass

22 Apr 23:24

Choose a tag to compare

Summary

Architecture audit surfaced nine correctness / safety gaps — this release addresses all of them plus four items from a follow-up code review. Schema auto-migrates; no user action required.

Algorithmic correctness

  • RRF blend factor fixSCORE_BLEND_FACTOR = 0.1 silently dominated RRF by ~100× at k=30 (rank-0 RRF ≈ 0.0164 vs. max blend = 0.1), inverting the docstring's own "doesn't override rank ordering" contract and effectively converting RRF into per-source-raw-score ranking. Replaced with adaptive blend_scale = 0.5 / ((k+1)(k+2)) — mathematically half the smallest adjacent-rank RRF gap. Semantic search results shift (for the better) on queries where one source returns a high-raw-score item at a late rank.

Data safety — schema v7 embedding-dim guard

  • SCHEMA_VERSION 6 → 7. New meta table records embedding_dim; mismatch on open → atomic DROP + rebuild node_vectors at current EMBEDDING_DIM. Prevents silent crash-on-INSERT when a user rebuilds the binary at a different dim (e.g., swaps embedding model).
  • v6 → v7 upgrade path introspects on-disk vec0 DDL via sqlite_master.sql (float[N]) and rebuilds if the existing table's dim ≠ current.

Concurrency hardening

  • Bounded watcher channel (sync_channel(4096)) — no more unbounded memory growth on bulk fs events; merkle rescan covers any dropped events.
  • Secondary MCP instances (flock denied) open DB strictly SQLITE_OPEN_READ_ONLY | query_only=ON; eliminates the race where a secondary could run migrations / the INDEX_VERSION DELETE sweep on the primary's DB.

Contract strengthening

  • ParsedRelation.source_language is now stamped by the parser; resolver hard-errors on mismatch (bail!, not debug_assert!) so parser regressions fail in release builds too.
  • Phase-3 startup auto-repair (repair_null_context_strings) now actually fires on every session start — previously only via explicit pipeline paths, contradicting the README's own claim.

Documentation accuracy

  • README HTTP route tracing now correctly lists only the frameworks extract_route_pattern actually implements (Express, Flask/FastAPI, Go net/http). ASP.NET/Rails/Laravel/Vapor claims removed.

CI + release

  • ci.yml — matrix {ubuntu, macos, windows} × {no-embed, with-embed} (was ubuntu-only); toolchain pinned to @1.95.0.
  • release.yml — new smoke-verify job runs after publish on all 3 OSes: npm install with retry-backoff, --version exact match, incremental-index + map --json on a tmp git repo. Catches missing platform binaries, find-binary.js regressions, and version-sync drift before users hit them.

Testing

  • +18 unit tests: RRF invariants ×4, schema v7 paths ×5, readonly ×2, source_language stamp ×1.
  • 382 tests pass total: 250 unit + 56 integration + 44 hardening + 19 parser + 6 CLI + 6 plugin + 1 routing.
  • Clippy 1.95 clean on both feature modes.

Deferred

L3 refactor — splitting tools.rs (2236 LOC), relations.rs (2174), queries.rs (2783) into focused modules — requires a dedicated session with plan-mode review and ships separately.

Install

```bash
npm install -g @sdsrs/code-graph@0.16.0

or

npx -y @sdsrs/code-graph@0.16.0
```

Claude Code users see the update via the plugin's auto-update hook.

Full Changelog: v0.15.2...v0.16.0

v0.15.2 — ast_search ranking + dead-code --json empty contract

22 Apr 22:10

Choose a tag to compare

User-driven QA pass exercising every MCP tool + CLI subcommand surfaced two bugs whose contract violations were silent — both regressions guard against recurrence.

Fixes

  • src/storage/queries.rsget_nodes_with_files_by_filters (the SQL backing ast_search / ast-search) ordered by f.path ASC only, so the LIMIT clause silently truncated alphabetically-late files (src/storage/queries.rs itself, with 54 Result-returning fns) out of the top-N. New ordering is caller_count DESC, path ASC, line ASC so high-value symbols surface first regardless of file path.
  • src/cli.rs:2655dead-code --json returned only stderr (no stdout) when all results were filtered by --ignore, breaking JSON consumers piping stdout. Now emits [] to stdout before the human stderr message, matching the established empty-result contract used by search / grep / callgraph / show / trace / overview.

New regression tests

  • test_get_nodes_with_files_by_filters_ranks_by_caller_count (src/storage/queries.rs) — alphabetically-first low-caller fn must not outrank alphabetically-last high-caller fn at any LIMIT.
  • test_cli_json_empty_dead_code (tests/cli_e2e.rs) — stdout must be [] and stderr must still surface "No dead code" when --ignore filters all results.

371 tests pass (was 369). Clippy 1.95 clean on both feature combos.

Full Changelog: v0.15.1...v0.15.2