Skip to content

Commit c43fabd

Browse files
authored
Merge branch 'main' into agent/fix-mcp-structured-content
2 parents cabece1 + 71cd27f commit c43fabd

13 files changed

Lines changed: 965 additions & 61 deletions

File tree

‎Makefile.cbm‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ TEST_REPRO_SRCS = \
450450
tests/repro/repro_issue581.c \
451451
tests/repro/repro_issue787.c \
452452
tests/repro/repro_issue842.c \
453+
tests/repro/repro_issue964.c \
453454
tests/repro/repro_invariant_calls.c \
454455
tests/repro/repro_invariant_graph.c \
455456
tests/repro/repro_invariant_breadth.c \

‎internal/cbm/cbm.c‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,38 @@ static bool cbm_region_is_recovered(uint32_t rs, uint32_t re, const CBMDefArray
817817
return covered_to >= re;
818818
}
819819

820+
/* #961: true when 1-based `line` of `src` contains `name` (used to verify a
821+
* def recovered from EXPANDED source really lives on that ORIGINAL line —
822+
* rejects header-inlined defs whose physical expanded lines alias unrelated
823+
* raw lines when compile_commands include paths are present). */
824+
static bool cbm_line_contains(const char *src, int src_len, uint32_t line, const char *name) {
825+
if (!src || !name || !name[0] || line == 0) {
826+
return false;
827+
}
828+
uint32_t cur = 1;
829+
int i = 0;
830+
while (i < src_len && cur < line) {
831+
if (src[i] == '\n') {
832+
cur++;
833+
}
834+
i++;
835+
}
836+
if (cur != line) {
837+
return false;
838+
}
839+
int end = i;
840+
while (end < src_len && src[end] != '\n') {
841+
end++;
842+
}
843+
size_t nlen = strlen(name);
844+
for (int j = i; j + (int)nlen <= end; j++) {
845+
if (strncmp(src + j, name, nlen) == 0) {
846+
return true;
847+
}
848+
}
849+
return false;
850+
}
851+
820852
static void cbm_subtract_recovered_regions(cbm_error_regions_t *regs, const CBMDefArray *defs) {
821853
int kept = 0;
822854
for (int i = 0; i < regs->count; i++) {
@@ -1103,6 +1135,52 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len,
11031135
cbm_run_c_lsp(a, result, expanded, expanded_len, pp_root,
11041136
language != CBM_LANG_C);
11051137

1138+
/* #961: a def whose body braces are split across
1139+
* #ifdef/#else branches parses as an ERROR region on the
1140+
* RAW source (both branches present at once -> unbalanced
1141+
* braces), so the raw defs walk silently dropped it. The
1142+
* expanded tree parses clean (simplecpp picked one
1143+
* branch) and same-file token lines stay aligned, so
1144+
* recover defs from it — adopting ONLY those that
1145+
* intersect a raw ERROR region, whose name is visible on
1146+
* the raw source line, and whose QN the raw pass did not
1147+
* already extract. */
1148+
if (ts_node_has_error(root)) {
1149+
cbm_error_regions_t raw_regs = {{0}, {0}, 0};
1150+
cbm_collect_error_regions(root, &raw_regs);
1151+
if (raw_regs.count > 0) {
1152+
int defs_before = result->defs.count;
1153+
cbm_extract_definitions(&pp_ctx);
1154+
int w = defs_before;
1155+
for (int i = defs_before; i < result->defs.count; i++) {
1156+
CBMDefinition *d = &result->defs.items[i];
1157+
bool adopt = false;
1158+
for (int rj = 0; rj < raw_regs.count && !adopt; rj++) {
1159+
if (d->start_line <= raw_regs.ends[rj] &&
1160+
d->end_line >= raw_regs.starts[rj]) {
1161+
adopt = true;
1162+
}
1163+
}
1164+
if (adopt &&
1165+
(!d->name || !cbm_line_contains(source, source_len,
1166+
d->start_line, d->name))) {
1167+
adopt = false;
1168+
}
1169+
for (int j = 0; j < defs_before && adopt; j++) {
1170+
const char *q = result->defs.items[j].qualified_name;
1171+
if (q && d->qualified_name &&
1172+
strcmp(q, d->qualified_name) == 0) {
1173+
adopt = false;
1174+
}
1175+
}
1176+
if (adopt) {
1177+
result->defs.items[w++] = *d;
1178+
}
1179+
}
1180+
result->defs.count = w;
1181+
}
1182+
}
1183+
11061184
ts_tree_delete(pp_tree);
11071185
}
11081186
}

‎internal/cbm/extract_defs.c‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4646,6 +4646,37 @@ static void extract_destructured_vars(CBMExtractCtx *ctx, TSNode pattern, TSNode
46464646
}
46474647
}
46484648

4649+
/* True for `require("...")` call_expressions with a string-literal path — the
4650+
* CommonJS import form that extract_imports records with local_name = the
4651+
* enclosing declarator's identifier. Same string-argument kinds as
4652+
* process_commonjs_require so the two stay in lockstep. */
4653+
static bool is_require_import_call(TSNode value, const char *source, CBMArena *a) {
4654+
if (strcmp(ts_node_type(value), "call_expression") != 0) {
4655+
return false;
4656+
}
4657+
TSNode fn = ts_node_child_by_field_name(value, TS_FIELD("function"));
4658+
if (ts_node_is_null(fn) || strcmp(ts_node_type(fn), "identifier") != 0) {
4659+
return false;
4660+
}
4661+
char *fn_name = cbm_node_text(a, fn, source);
4662+
if (!fn_name || strcmp(fn_name, "require") != 0) {
4663+
return false;
4664+
}
4665+
TSNode args = ts_node_child_by_field_name(value, TS_FIELD("arguments"));
4666+
if (ts_node_is_null(args)) {
4667+
return false;
4668+
}
4669+
uint32_t argc = ts_node_named_child_count(args);
4670+
for (uint32_t i = 0; i < argc; i++) {
4671+
const char *ak = ts_node_type(ts_node_named_child(args, i));
4672+
if (strcmp(ak, "string") == 0 || strcmp(ak, "string_literal") == 0 ||
4673+
strcmp(ak, "template_string") == 0) {
4674+
return true;
4675+
}
4676+
}
4677+
return false;
4678+
}
4679+
46494680
// JS/TS variable extraction: skip function-assigned declarators.
46504681
static void extract_js_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) {
46514682
uint32_t n = ts_node_named_child_count(node);
@@ -4654,13 +4685,15 @@ static void extract_js_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) {
46544685
if (strcmp(ts_node_type(child), "variable_declarator") != 0) {
46554686
continue;
46564687
}
4688+
bool is_require = false;
46574689
TSNode value = ts_node_child_by_field_name(child, TS_FIELD("value"));
46584690
if (!ts_node_is_null(value)) {
46594691
const char *vk = ts_node_type(value);
46604692
if (strcmp(vk, "arrow_function") == 0 || strcmp(vk, "function_expression") == 0 ||
46614693
strcmp(vk, "generator_function") == 0) {
46624694
continue;
46634695
}
4696+
is_require = is_require_import_call(value, ctx->source, a);
46644697
}
46654698
TSNode vname = ts_node_child_by_field_name(child, TS_FIELD("name"));
46664699
if (!ts_node_is_null(vname)) {
@@ -4670,6 +4703,15 @@ static void extract_js_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) {
46704703
if (strcmp(nk, "object_pattern") == 0 || strcmp(nk, "array_pattern") == 0) {
46714704
extract_destructured_vars(ctx, vname, child, a);
46724705
} else {
4706+
/* `const foo = require('./foo')` is an import binding, not a
4707+
* definition — extract_imports records local_name="foo". A
4708+
* Variable node here shadows call resolution onto the alias
4709+
* and orphans the imported callee (#871); ESM `import`
4710+
* bindings emit no Variable either. Destructured requires
4711+
* keep theirs: the import row only records the module. */
4712+
if (is_require) {
4713+
continue;
4714+
}
46734715
push_var_def(ctx, cbm_node_text(a, vname, ctx->source), child);
46744716
}
46754717
}

‎src/main.c‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,18 @@ static void *http_thread(void *arg) {
159159
static int watcher_index_fn(const char *project_name, const char *root_path, void *user_data) {
160160
(void)user_data;
161161

162-
/* Skip indexing if shutdown is in progress */
162+
/* Skip indexing if shutdown is in progress (skipped, not indexed) */
163163
if (atomic_load(&g_shutdown)) {
164-
return 0;
164+
return 1;
165165
}
166166

167-
/* Non-blocking: skip if another pipeline is already running.
168-
* Watcher will retry on next poll cycle (5-60s). */
167+
/* Non-blocking: skip if another pipeline is already running. The
168+
* positive return keeps the watcher's baselines uncommitted so the
169+
* change is retried on the next poll cycle (5-60s) instead of being
170+
* recorded as seen and lost (#937). */
169171
if (!cbm_pipeline_try_lock()) {
170172
cbm_log_info("watcher.skip", "project", project_name, "reason", "pipeline_busy");
171-
return 0;
173+
return 1;
172174
}
173175

174176
cbm_log_info("watcher.reindex", "project", project_name, "path", root_path);

‎src/mcp/mcp.c‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,67 @@ static char *normalize_project_arg(char *project) {
853853
return project;
854854
}
855855

856+
/* Forward decls — defined below alongside store resolution. */
857+
static const char *cache_dir(char *buf, size_t bufsz);
858+
static bool is_project_db_file(const char *name, size_t len);
859+
bool cbm_validate_project_name(const char *project);
860+
861+
/* #1025: agents naturally pass the repo FOLDER name ("codebase-memory-mcp"),
862+
* but indexed project names derive from the full path
863+
* (E:\project\graph\x -> "E-project-graph-x"), so the exact lookup fails
864+
* while list_projects clearly shows the project. When no <project>.db exists,
865+
* scan cache-dir FILENAMES for a segment-aligned tail match ("-<project>.db"):
866+
* exactly one match adopts the full name; zero or several keep the original so
867+
* the existing not-found error (which lists all candidates) fires. Filename-
868+
* level only — internal-name drift stays #704's fallback in resolve_store. */
869+
static char *resolve_project_tail(char *project) {
870+
if (!project || !cbm_validate_project_name(project)) {
871+
return project;
872+
}
873+
char dir[CBM_SZ_1K];
874+
cache_dir(dir, sizeof(dir));
875+
char exact[CBM_SZ_2K];
876+
snprintf(exact, sizeof(exact), "%s/%s.db", dir, project);
877+
if (cbm_file_exists(exact)) {
878+
return project; /* exact name — untouched fast path */
879+
}
880+
size_t plen = strlen(project);
881+
char match[CBM_SZ_1K] = "";
882+
int matches = 0;
883+
cbm_dir_t *d = cbm_opendir(dir);
884+
if (!d) {
885+
return project;
886+
}
887+
cbm_dirent_t *entry;
888+
while ((entry = cbm_readdir(d)) != NULL) {
889+
const char *n = entry->name;
890+
size_t len = strlen(n);
891+
if (!is_project_db_file(n, len)) {
892+
continue;
893+
}
894+
size_t stem_len = len - MCP_DB_EXT; /* strip ".db" */
895+
if (stem_len <= plen + 1 || stem_len >= sizeof(match)) {
896+
continue;
897+
}
898+
if (n[stem_len - plen - 1] != '-' || strncmp(n + stem_len - plen, project, plen) != 0) {
899+
continue;
900+
}
901+
matches++;
902+
if (matches > 1) {
903+
break; /* ambiguous — keep the original name */
904+
}
905+
memcpy(match, n, stem_len);
906+
match[stem_len] = '\0';
907+
}
908+
cbm_closedir(d);
909+
if (matches == 1) {
910+
cbm_log_info("mcp.project_tail_resolved", "passed", project, "resolved", match);
911+
free(project);
912+
return heap_strdup(match);
913+
}
914+
return project;
915+
}
916+
856917
/* Resolve the project argument, accepting the canonical "project" key plus the
857918
* aliases a caller naturally reaches for (#640): list_projects surfaces the
858919
* field as "name" and the not-found hint says "pass the project name", so
@@ -870,7 +931,7 @@ static char *get_project_arg(const char *args_json) {
870931
if (!p) {
871932
p = cbm_mcp_get_string_arg(args_json, "projectName");
872933
}
873-
return normalize_project_arg(p);
934+
return resolve_project_tail(normalize_project_arg(p));
874935
}
875936

876937
int cbm_mcp_get_int_arg(const char *args_json, const char *key, int default_val) {

0 commit comments

Comments
 (0)