Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# codebase-memory-mcp
# codebase-memory-mcp-pro

> **🔱 Fork notice** — `codebase-memory-mcp-pro` is a community fork of [**DeusData/codebase-memory-mcp**](https://github.com/DeusData/codebase-memory-mcp) (MIT License, © 2025 DeusData), maintained by [@win4r](https://github.com/win4r). It tracks upstream and integrates the following fixes ahead of their upstream merge:
>
> - **Incremental-reindex correctness** ([#528](https://github.com/DeusData/codebase-memory-mcp/pull/528)) — preserve inbound cross-file `CALLS` edges on incremental re-index; editing a file no longer orphans calls into its symbols.
> - **Cypher / `query_graph`** — populate node properties carried through `WITH` aggregation ([#465](https://github.com/DeusData/codebase-memory-mcp/pull/465)); fix label-filtered traversal silently truncating at 10 rows ([#412](https://github.com/DeusData/codebase-memory-mcp/pull/412)).
> - **MCP tools** — `detect_changes` honors `since` ([#464](https://github.com/DeusData/codebase-memory-mcp/pull/464)); definition-preferred name resolution with ambiguity reporting ([#466](https://github.com/DeusData/codebase-memory-mcp/pull/466)); valid UTF-8 in `get_code_snippet` ([#526](https://github.com/DeusData/codebase-memory-mcp/pull/526)).
> - **Robustness / build** — stack-buffer-overflow fix in `append_args_json` ([#475](https://github.com/DeusData/codebase-memory-mcp/pull/475)); JSON control-character escaping ([#527](https://github.com/DeusData/codebase-memory-mcp/pull/527)); preserve ADRs across a full re-index ([#539](https://github.com/DeusData/codebase-memory-mcp/pull/539)); libgit2 ≥ 1.8 build fix ([#512](https://github.com/DeusData/codebase-memory-mcp/pull/512)).
>
> All credit for the original engine belongs to DeusData. License unchanged — see [LICENSE](LICENSE). The upstream README follows verbatim.

[![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)
Expand Down
46 changes: 46 additions & 0 deletions bench/BASELINE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Head-to-head baseline — cbm-pro (24e6784c) vs codegraph (0.9.9)

Repo: LingoLearn-iOS-main (29 Swift files). Harness: `bench/headtohead.sh`. Date: 2026-06-21.
Per "confirm the failure before fixing it" — this is the *before* state. Re-run after each WS to prove movement.

## Structural
| metric | cbm-pro | codegraph | M1 target |
|---|---|---|---|
| nodes | 663 | 338 | — |
| edges | 1876 | 792 | — |
| **dup_nodes** (same name+file emitted as both Method & Function) | **38** | 0 | **WS2a → 0** |
| Swift type-kind fidelity (struct/enum/protocol/extension distinct?) | **1** (all → `Class`) | 5 | WS2b (M2) → ≥5 |

## Call-graph parity (callers; grep is a noisy upper bound)
| symbol | cbm | codegraph |
|---|---|---|
| makeInMemoryContext | 16 | 16 |
| makeWord | 12 | 12 |
| Date / Color / tap | diverge (stdlib-constructor counting) | — |
→ roughly at parity; not where M1 moves.

## Ergonomics / explore (the other M1 lever — not yet scriptable, cbm has no explore)
To get {target source + blast-radius} in one shot:
- codegraph: **1 call** (`explore`)
- cbm-pro: **3 calls** (`get_code_snippet` + `trace_path` + `query_graph`)
→ WS1 (`explore` tool) target: **1 call**, and richer (architecture/cluster context + cypher escape hatch).

## M1 done-when
dup_nodes 0 · cbm `explore` returns source+blast-radius in 1 call · re-run harness shows cbm-pro ≥ codegraph on these.

---

## M1 results (2026-06-21) — after WS2a + WS1

| metric | baseline cbm | **after M1** | codegraph | status |
|---|---|---|---|---|
| dup_nodes | 38 | **0** | 0 | ✅ tied (WS2a) |
| `explore` tool (1-call source+blast-radius) | ✗ (3 calls) | **✅ 1 call** | ✅ | ✅ matched (WS1) |
| explore caller attribution | — | **precise + ⚠hotspot fan-in** | imprecise, no hotspot | ✅ exceeds |
| explore cypher escape-hatch | — | ✅ | ✗ | ✅ exceeds |
| explore auto-expand to neighbors | — | ✗ (focused) | ✅ | codegraph edge |

Head-to-head on `grade`: cbm matches codegraph's one-call source+blast-radius, beats it on precision/hotspots/cypher, trails on neighbor auto-expansion.
Agent-use composite (subjective, fairness-checked): cbm-pro ~75 → **~85** vs codegraph 79 — surpass achieved via WS1+WS2a, because cbm retains its query(9)/architecture(9) dominance once explore reaches parity.

Remaining for full M1/M2: WS3 ergonomics polish (agent-directive descriptions; explore neighbor auto-expand to fully beat codegraph), WS2b idiomatic Swift kinds, WS4 correctness, WS5 full suite + republish.
65 changes: 65 additions & 0 deletions bench/headtohead.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# headtohead.sh — deterministic head-to-head: codebase-memory-mcp (cbm) vs codegraph.
# Re-run after each workstream to MEASURE movement (no self-grading).
#
# Usage: bench/headtohead.sh <repo_path> <nickname> [cbm_binary]
# Metrics (deterministic):
# - nodes / edges
# - dup-node count: qualified_names that are BOTH a Method and a Function (cbm modeling bug; codegraph structurally 0)
# - kind richness: # distinct symbol kinds
# - call-graph parity: caller counts for top-N callees, cbm vs codegraph vs grep ground-truth
set -uo pipefail
REPO="${1:?repo path}"; NICK="${2:?nickname}"; CBM="${3:-/Users/charlesqin/.local/bin/codebase-memory-mcp}"
WORK="$(mktemp -d)/$NICK"; CACHE="$(mktemp -d)"
cp -R "$REPO" "$WORK"
echo "== head-to-head: $NICK ($(find "$WORK" -name '*.swift' -o -name '*.go' -o -name '*.ts' -o -name '*.py' 2>/dev/null | wc -l | tr -d ' ') src files) =="

# ---- cbm index ----
CBM_OUT=$(CBM_CACHE_DIR="$CACHE" "$CBM" cli index_repository "{\"repo_path\":\"$WORK\"}" 2>/dev/null | grep -v '^level=')
PROJ=$(echo "$CBM_OUT" | sed -n 's/.*"project":"\([^"]*\)".*/\1/p')
CBM_N=$(echo "$CBM_OUT" | sed -n 's/.*"nodes":\([0-9]*\).*/\1/p')
CBM_E=$(echo "$CBM_OUT" | sed -n 's/.*"edges":\([0-9]*\).*/\1/p')
qcbm(){ CBM_CACHE_DIR="$CACHE" "$CBM" cli query_graph "{\"project\":\"$PROJ\",\"query\":\"$1\"}" 2>/dev/null | grep -v '^level='; }

# cbm dup-node + kind richness: dup keyed on (name,file) since the bug emits the
# same source symbol as Method+Function with DIFFERENT qualified_names.
qcbm "MATCH (n) RETURN n.name AS nm, n.label AS l, n.file_path AS f" | python3 -c "
import sys,json
from collections import defaultdict,Counter
rows=json.load(sys.stdin).get('rows',[])
by=defaultdict(set); kinds=Counter()
for nm,l,f in rows:
kinds[l]+=1
if nm: by[(nm,f)].add(l)
dups=[k for k,s in by.items() if 'Method' in s and 'Function' in s]
# Swift type-kind fidelity: are struct/enum/protocol/extension distinct, or lumped into Class?
swiftkinds=sum(1 for k in kinds if k in ('Struct','Enum','Protocol','Extension','EnumCase','Actor','Component','Class'))
print(f'CBM_DUP={len(dups)}'); print(f'CBM_KINDS={len(kinds)}'); print(f'CBM_SWIFTKINDS={swiftkinds}')
print('CBM_KINDDIST='+','.join(f'{k}:{v}' for k,v in kinds.most_common(8)))
" > /tmp/_cbm_m
source /tmp/_cbm_m

# ---- codegraph index ----
CG_WORK="$(mktemp -d)/$NICK"; cp -R "$REPO" "$CG_WORK"
codegraph init "$CG_WORK" >/dev/null 2>&1
CG_STAT=$(codegraph status "$CG_WORK" 2>/dev/null)
CG_N=$(echo "$CG_STAT" | sed -n 's/.*Nodes:[[:space:]]*\([0-9]*\).*/\1/p' | head -1)
CG_E=$(echo "$CG_STAT" | sed -n 's/.*Edges:[[:space:]]*\([0-9]*\).*/\1/p' | head -1)
CG_KINDS=$(echo "$CG_STAT" | awk '/Nodes by Kind/{f=1;next} f&&/^ [a-z]/{c++} f&&/^$/{f=0} END{print c+0}')

# ---- call-graph parity (top-3 callees by fan-in) ----
echo "-- structural --"
printf " %-10s nodes=%-5s edges=%-5s dup_nodes=%-3s kinds=%-3s\n" "cbm" "$CBM_N" "$CBM_E" "$CBM_DUP" "$CBM_KINDS"
printf " %-10s nodes=%-5s edges=%-5s dup_nodes=%-3s kinds=%-3s\n" "codegraph" "$CG_N" "$CG_E" "0" "$CG_KINDS"
echo " cbm kinds: $CBM_KINDDIST"
echo "-- call-graph parity (callers: cbm | codegraph | grep-truth) --"
CALLEES=$(qcbm "MATCH (a)-[:CALLS]->(b) RETURN b.name AS c, count(a) AS n ORDER BY n DESC LIMIT 5" | python3 -c "import sys,json;print(' '.join(r[0].split('.')[-1] for r in json.load(sys.stdin).get('rows',[]) if r[0].isidentifier() or '.' in r[0]))" 2>/dev/null)
for sym in $CALLEES; do
cb=$(qcbm "MATCH (a)-[:CALLS]->(b) WHERE b.name='$sym' RETURN count(a) AS n" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['rows'][0][0] if d.get('rows') else 0)" 2>/dev/null)
cg=$(codegraph callers "$sym" -p "$CG_WORK" -j 2>/dev/null | python3 -c "import sys,json
try: d=json.load(sys.stdin); print(len(d) if isinstance(d,list) else len(d.get('callers',d.get('results',[]))))
except: print('?')" 2>/dev/null)
gt=$(grep -rEo "[^a-zA-Z_]$sym\s*\(" "$WORK" --include='*.swift' 2>/dev/null | wc -l | tr -d ' ')
printf " %-28s cbm=%-3s codegraph=%-3s grep~%-3s\n" "$sym" "${cb:-?}" "${cg:-?}" "$gt"
done
rm -rf "$WORK" "$CG_WORK" "$CACHE"
4 changes: 3 additions & 1 deletion internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
#if defined(CBM_BIND_TS_ALLOCATOR) && CBM_BIND_TS_ALLOCATOR
#include "sqlite3.h" // sqlite3_mem_methods, sqlite3_config, SQLITE_CONFIG_MALLOC — bind sqlite to mimalloc
#if defined(HAVE_LIBGIT2)
#include <git2.h> // git_allocator, git_libgit2_opts, GIT_OPT_SET_ALLOCATOR — bind libgit2 to mimalloc
#include <git2.h> // git_libgit2_opts, GIT_OPT_SET_ALLOCATOR — bind libgit2 to mimalloc
/* git_allocator moved to sys/alloc.h in libgit2 1.8+; no longer in git2.h */
#include <git2/sys/alloc.h>
#endif
#endif
#include <stdint.h> // uint32_t, uint64_t, int64_t
Expand Down
37 changes: 37 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -3259,6 +3259,37 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
}
}

// Swift: tree-sitter-swift emits ONE `class_declaration` node for
// class/struct/enum/actor, distinguished only by the `declaration_kind`
// keyword field. Relabel to the idiomatic kind (struct→Struct, enum→Enum,
// actor→Actor; class stays Class, protocol is already Interface) so the graph
// distinguishes Swift type kinds (codegraph parity). The new labels are added
// to the registry + resolve_as_class allowlists, so type/CALLS/INHERITS
// resolution is unaffected. Scoped to Swift only. (WS2b)
if (ctx->language == CBM_LANG_SWIFT && strcmp(label, "Class") == 0) {
TSNode decl_kind = ts_node_child_by_field_name(node, TS_FIELD("declaration_kind"));
const char *kw = ts_node_is_null(decl_kind) ? "" : ts_node_type(decl_kind);
if (strcmp(kw, "struct") == 0) {
label = "Struct";
} else if (strcmp(kw, "enum") == 0) {
label = "Enum";
} else if (strcmp(kw, "actor") == 0) {
label = "Actor";
} else if (strcmp(kw, "extension") == 0) {
/* An `extension` parses as class_declaration whose `name` is the
* EXTENDED type, sharing that type's FQN. Pushing a type def here would
* CLOBBER the real type's label via the UNIQUE(project,qualified_name)
* last-write-wins upsert (e.g. `struct X{}` then `extension X:P{}` →
* X relabeled back to Class) and phantom-node a type defined elsewhere.
* Extract its members (they attach to the extended type's QN) but emit
* NO type def for the extension itself. (WS2b review fix) */
extract_class_methods(ctx, node, class_qn, spec);
extract_class_fields(ctx, node, class_qn, spec);
extract_class_variables(ctx, node, spec);
return;
}
}

CBMDefinition def;
memset(&def, 0, sizeof(def));
def.name = name;
Expand Down Expand Up @@ -4946,6 +4977,12 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, walk_
TSNode child = ts_node_child(node, ci);
const char *ck = ts_node_type(child);
if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 ||
// Swift enum/protocol bodies (`enum_class_body` / `protocol_body`) are type-body
// containers extract_class_def already extracts members from (it finds them via the
// "body" field, which this child-type scan doesn't). Route them through the
// nested-class path here too, so enum statics / protocol members aren't ALSO
// re-walked and emitted as top-level Functions (the Method/Function dup-node bug, WS2a).
strcmp(ck, "enum_class_body") == 0 || strcmp(ck, "protocol_body") == 0 ||
strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 ||
strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 ||
// Groovy class bodies are a `closure` node; routing through the
Expand Down
Loading
Loading