Skip to content

Commit bd6bbc2

Browse files
committed
Add Go LSP hybrid type resolver, config store, and supporting infrastructure
- Go LSP cross-file type resolution engine (internal/cbm/lsp/) with C-based type registry, scope tracking, and Go stdlib data (30K+ definitions) - lsp_bridge.go: CrossFileDef struct and RunGoLSPCrossFile CGo bridge - config.go: persistent ConfigStore (SQLite) for auto_index, mem_limit settings - CLI config subcommand for get/set/list/delete operations - Store: edge batch upsert, project config columns, goleak test harness - Pipeline: prefetch parallelism, resolver improvements, memleak tests - CI: dry-run workflow improvements, vendor-grammar.sh enhancements
1 parent c4848a8 commit bd6bbc2

33 files changed

+37113
-59
lines changed

.github/workflows/dry-run.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,25 @@ jobs:
5858
- name: Smoke test
5959
run: bash scripts/smoke-test.sh ./codebase-memory-mcp
6060

61+
test-asan:
62+
needs: [lint]
63+
runs-on: ubuntu-latest
64+
steps:
65+
- uses: actions/checkout@v4
66+
67+
- uses: actions/setup-go@v5
68+
with:
69+
go-version: "1.26"
70+
71+
- name: Test with AddressSanitizer
72+
env:
73+
CGO_CFLAGS: "-fsanitize=address -fno-omit-frame-pointer"
74+
CGO_LDFLAGS: "-fsanitize=address"
75+
run: go test -count=1 -timeout=10m ./...
76+
77+
- name: Memory stability check
78+
run: go test -run TestMemoryStability -v -count=1 -timeout=5m ./internal/pipeline/
79+
6180
test-windows:
6281
needs: [lint]
6382
runs-on: windows-latest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ Thumbs.db
3232

3333
# Local project memory (Claude Code auto-memory)
3434
memory/
35+
reference/

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build test lint clean install check
1+
.PHONY: build test lint clean install check bench-memory
22

33
BINARY=codebase-memory-mcp
44
MODULE=github.com/DeusData/codebase-memory-mcp
@@ -19,3 +19,6 @@ clean:
1919

2020
install:
2121
go install ./cmd/codebase-memory-mcp/
22+
23+
bench-memory: ## Run memory stability benchmark
24+
go test -run TestMemoryStability -v -count=1 -timeout=5m ./internal/pipeline/

cmd/codebase-memory-mcp/config.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"sort"
7+
"strings"
8+
9+
"github.com/DeusData/codebase-memory-mcp/internal/store"
10+
)
11+
12+
// configDefaults documents all known config keys, their defaults, and descriptions.
13+
var configDefaults = []struct {
14+
Key string
15+
Default string
16+
Description string
17+
}{
18+
{store.ConfigAutoIndex, "false", "Enable background auto-indexing on MCP session start"},
19+
{store.ConfigAutoIndexLimit, "50000", "Max files for auto-indexing new (never-indexed) projects"},
20+
{store.ConfigMemLimit, "", "GOMEMLIMIT for the server process (e.g. 2G, 512M). Empty = no limit"},
21+
}
22+
23+
func runConfig(args []string) int {
24+
if len(args) == 0 {
25+
printConfigHelp()
26+
return 0
27+
}
28+
29+
switch args[0] {
30+
case "list", "ls":
31+
return configList()
32+
case "get":
33+
if len(args) < 2 {
34+
fmt.Fprintln(os.Stderr, "Usage: codebase-memory-mcp config get <key>")
35+
return 1
36+
}
37+
return configGet(args[1])
38+
case "set":
39+
if len(args) < 3 {
40+
fmt.Fprintln(os.Stderr, "Usage: codebase-memory-mcp config set <key> <value>")
41+
return 1
42+
}
43+
return configSet(args[1], args[2])
44+
case "reset":
45+
if len(args) < 2 {
46+
fmt.Fprintln(os.Stderr, "Usage: codebase-memory-mcp config reset <key>")
47+
return 1
48+
}
49+
return configReset(args[1])
50+
case "--help", "-h", "help":
51+
printConfigHelp()
52+
return 0
53+
default:
54+
fmt.Fprintf(os.Stderr, "Unknown config command: %s\n", args[0])
55+
printConfigHelp()
56+
return 1
57+
}
58+
}
59+
60+
func configList() int {
61+
cfg, err := store.OpenConfig()
62+
if err != nil {
63+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
64+
return 1
65+
}
66+
defer cfg.Close()
67+
68+
all, err := cfg.All()
69+
if err != nil {
70+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
71+
return 1
72+
}
73+
74+
// Merge with defaults to show all known keys
75+
merged := make(map[string]string)
76+
for _, d := range configDefaults {
77+
merged[d.Key] = d.Default
78+
}
79+
for k, v := range all {
80+
merged[k] = v
81+
}
82+
83+
keys := make([]string, 0, len(merged))
84+
for k := range merged {
85+
keys = append(keys, k)
86+
}
87+
sort.Strings(keys)
88+
89+
fmt.Println("Configuration:")
90+
for _, k := range keys {
91+
v := merged[k]
92+
source := "default"
93+
if _, ok := all[k]; ok {
94+
source = "set"
95+
}
96+
desc := configDescription(k)
97+
fmt.Printf(" %-25s = %-10s (%s) %s\n", k, v, source, desc)
98+
}
99+
return 0
100+
}
101+
102+
func configGet(key string) int {
103+
cfg, err := store.OpenConfig()
104+
if err != nil {
105+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
106+
return 1
107+
}
108+
defer cfg.Close()
109+
110+
defVal := configDefaultValue(key)
111+
val := cfg.Get(key, defVal)
112+
fmt.Println(val)
113+
return 0
114+
}
115+
116+
func configSet(key, value string) int {
117+
// Validate known keys
118+
if !isKnownConfigKey(key) {
119+
fmt.Fprintf(os.Stderr, "Unknown config key: %s\n", key)
120+
fmt.Fprintf(os.Stderr, "Known keys: %s\n", knownConfigKeys())
121+
return 1
122+
}
123+
124+
// Validate mem_limit
125+
if key == store.ConfigMemLimit && value != "" {
126+
if _, err := parseByteSize(value); err != nil {
127+
fmt.Fprintf(os.Stderr, "Invalid value for %s: %q (expected size like 2G, 512M, 4096M)\n", key, value)
128+
return 1
129+
}
130+
}
131+
132+
// Validate bool keys
133+
if key == store.ConfigAutoIndex {
134+
v := strings.ToLower(value)
135+
if v != "true" && v != "false" && v != "on" && v != "off" && v != "1" && v != "0" {
136+
fmt.Fprintf(os.Stderr, "Invalid value for %s: %q (expected true/false)\n", key, value)
137+
return 1
138+
}
139+
// Normalize
140+
switch v {
141+
case "on", "1":
142+
value = "true"
143+
case "off", "0":
144+
value = "false"
145+
default:
146+
value = v
147+
}
148+
}
149+
150+
cfg, err := store.OpenConfig()
151+
if err != nil {
152+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
153+
return 1
154+
}
155+
defer cfg.Close()
156+
157+
if err := cfg.Set(key, value); err != nil {
158+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
159+
return 1
160+
}
161+
fmt.Printf("%s = %s\n", key, value)
162+
return 0
163+
}
164+
165+
func configReset(key string) int {
166+
cfg, err := store.OpenConfig()
167+
if err != nil {
168+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
169+
return 1
170+
}
171+
defer cfg.Close()
172+
173+
if err := cfg.Delete(key); err != nil {
174+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
175+
return 1
176+
}
177+
fmt.Printf("%s reset to default (%s)\n", key, configDefaultValue(key))
178+
return 0
179+
}
180+
181+
func printConfigHelp() {
182+
fmt.Fprintf(os.Stderr, `Usage: codebase-memory-mcp config <command> [args]
183+
184+
Commands:
185+
list Show all config values (with defaults)
186+
get <key> Get a config value
187+
set <key> <val> Set a config value
188+
reset <key> Reset a key to its default
189+
190+
Config keys:
191+
`)
192+
for _, d := range configDefaults {
193+
fmt.Fprintf(os.Stderr, " %-25s default=%-10s %s\n", d.Key, d.Default, d.Description)
194+
}
195+
fmt.Fprintf(os.Stderr, `
196+
Examples:
197+
codebase-memory-mcp config set auto_index true Enable auto-indexing on session start
198+
codebase-memory-mcp config set auto_index false Disable auto-indexing (default)
199+
codebase-memory-mcp config set auto_index_limit 20000
200+
codebase-memory-mcp config list Show all settings
201+
`)
202+
}
203+
204+
func configDefaultValue(key string) string {
205+
for _, d := range configDefaults {
206+
if d.Key == key {
207+
return d.Default
208+
}
209+
}
210+
return ""
211+
}
212+
213+
func configDescription(key string) string {
214+
for _, d := range configDefaults {
215+
if d.Key == key {
216+
return d.Description
217+
}
218+
}
219+
return ""
220+
}
221+
222+
func isKnownConfigKey(key string) bool {
223+
for _, d := range configDefaults {
224+
if d.Key == key {
225+
return true
226+
}
227+
}
228+
return false
229+
}
230+
231+
func knownConfigKeys() string {
232+
keys := make([]string, len(configDefaults))
233+
for i, d := range configDefaults {
234+
keys[i] = d.Key
235+
}
236+
return strings.Join(keys, ", ")
237+
}

internal/cbm/cbm.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "helpers.h"
33
#include "lang_specs.h"
44
#include "extract_unified.h"
5+
#include "lsp/go_lsp.h"
56
#include <stdlib.h>
67
#include <string.h>
78
#include <time.h>
@@ -98,6 +99,11 @@ void cbm_impltrait_push(CBMImplTraitArray* arr, CBMArena* a, CBMImplTrait it) {
9899
arr->items[arr->count++] = it;
99100
}
100101

102+
void cbm_resolvedcall_push(CBMResolvedCallArray* arr, CBMArena* a, CBMResolvedCall rc) {
103+
GROW_ARRAY(arr, a, CBMResolvedCall);
104+
arr->items[arr->count++] = rc;
105+
}
106+
101107
// --- String input reader (for parse_with_options) ---
102108

103109
typedef struct {
@@ -261,6 +267,11 @@ CBMFileResult* cbm_extract_file(
261267
cbm_extract_imports(&ctx);
262268
cbm_extract_unified(&ctx);
263269

270+
// LSP type-aware call resolution (Go only for now)
271+
if (language == CBM_LANG_GO) {
272+
cbm_run_go_lsp(a, result, source, source_len, root);
273+
}
274+
264275
uint64_t t2 = now_ns();
265276

266277
result->imports_count = result->imports.count;

internal/cbm/cbm.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,28 @@ import (
1616
"github.com/DeusData/codebase-memory-mcp/internal/lang"
1717
)
1818

19+
// ResolvedCall represents a high-confidence type-aware call resolution from LSP.
20+
type ResolvedCall struct {
21+
CallerQN string
22+
CalleeQN string
23+
Strategy string
24+
Confidence float32
25+
Reason string // diagnostic label for unresolved calls (empty if resolved)
26+
}
27+
1928
// FileResult holds the extraction results from one file.
2029
type FileResult struct {
21-
Definitions []Definition
22-
Calls []Call
23-
Imports []Import
24-
Usages []Usage
25-
Throws []Throw
26-
ReadWrites []ReadWrite
27-
TypeRefs []TypeRef
28-
EnvAccesses []EnvAccess
29-
TypeAssigns []TypeAssign
30-
ImplTraits []ImplTrait
30+
Definitions []Definition
31+
Calls []Call
32+
Imports []Import
33+
Usages []Usage
34+
Throws []Throw
35+
ReadWrites []ReadWrite
36+
TypeRefs []TypeRef
37+
EnvAccesses []EnvAccess
38+
TypeAssigns []TypeAssign
39+
ImplTraits []ImplTrait
40+
ResolvedCalls []ResolvedCall
3141

3242
ModuleQN string
3343
IsTestFile bool
@@ -55,7 +65,9 @@ type Definition struct {
5565
ParentClass string
5666
Decorators []string
5767
BaseClasses []string
68+
ParamNames []string
5869
ParamTypes []string
70+
ReturnTypes []string
5971
Complexity int
6072
Lines int
6173
IsExported bool
@@ -279,7 +291,9 @@ func convertResult(r *C.CBMFileResult) *FileResult {
279291
ParentClass: goStringOrEmpty(d.parent_class),
280292
Decorators: goStringSlice(d.decorators),
281293
BaseClasses: goStringSlice(d.base_classes),
282-
ParamTypes: goStringSlice(d.param_types),
294+
ParamNames: goStringSlice(d.param_names),
295+
ParamTypes: goStringSlice(d.param_types),
296+
ReturnTypes: goStringSlice(d.return_types),
283297
Complexity: int(d.complexity),
284298
Lines: int(d.lines),
285299
IsExported: bool(d.is_exported),
@@ -400,6 +414,21 @@ func convertResult(r *C.CBMFileResult) *FileResult {
400414
}
401415
}
402416

417+
// ResolvedCalls (LSP)
418+
if r.resolved_calls.count > 0 {
419+
fr.ResolvedCalls = make([]ResolvedCall, r.resolved_calls.count)
420+
rcs := unsafe.Slice(r.resolved_calls.items, r.resolved_calls.count)
421+
for i, rc := range rcs {
422+
fr.ResolvedCalls[i] = ResolvedCall{
423+
CallerQN: C.GoString(rc.caller_qn),
424+
CalleeQN: C.GoString(rc.callee_qn),
425+
Strategy: C.GoString(rc.strategy),
426+
Confidence: float32(rc.confidence),
427+
Reason: goStringOrEmpty(rc.reason),
428+
}
429+
}
430+
}
431+
403432
return fr
404433
}
405434

0 commit comments

Comments
 (0)