Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
146 changes: 145 additions & 1 deletion src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ enum {
#include <sys/stat.h>
#include <time.h>
#include <errno.h>
#include <ctype.h>

/* ── Constants ────────────────────────────────────────────────── */

Expand Down Expand Up @@ -345,9 +346,26 @@ static const tool_def_t TOOLS[] = {
"are normalized.\"},"
"\"persistence\":{\"type\":\"boolean\",\"default\":false,\"description\":"
"\"Write compressed artifact to .codebase-memory/graph.db.zst for team sharing. "
"Teammates can bootstrap from the artifact instead of full re-indexing.\"}"
"Teammates can bootstrap from the artifact instead of full re-indexing.\"},"
"\"version\":{\"type\":\"string\",\"description\":"
"\"Version tag stamped on every extracted node (e.g. '28.0', '30.0'). "
"Enables cross-version diff queries: MATCH (n:Class {version:'30.0'}) ... "
"Auto-detected from trailing path segment when the segment is numeric (e.g. "
"/exports/MyApp/28.0 → version='28.0'). Versioned runs always do a full index.\"}"
"},\"required\":[\"repo_path\"]}"},

{"diff_versions", "Diff versions",
"Compare two indexed versions of the same project to find added, removed, and "
"changed nodes. Requires version tags set via index_repository version= parameter.",
"{\"type\":\"object\",\"properties\":{"
"\"project\":{\"type\":\"string\",\"description\":\"Project name\"},"
"\"from_version\":{\"type\":\"string\",\"description\":\"Baseline version tag (e.g. "
"'28.0')\"},"
"\"to_version\":{\"type\":\"string\",\"description\":\"Comparison version tag (e.g. "
"'30.0')\"},"
"\"label\":{\"type\":\"string\",\"description\":\"Node label to compare (default: Class)\"}"
"},\"required\":[\"project\",\"from_version\",\"to_version\"]}"},

{"search_graph", "Search graph",
"Search the code knowledge graph for functions, classes, routes, and variables. Use INSTEAD "
"OF grep/glob when finding code definitions, implementations, or relationships. Three search "
Expand Down Expand Up @@ -611,6 +629,7 @@ static const tool_annotation_def_t TOOL_ANNOTATIONS[] = {
{"detect_changes", false, true, true, false},
{"manage_adr", false, true, false, false},
{"ingest_traces", false, false, false, false},
{"diff_versions", true, false, true, false},
};

static const tool_annotation_def_t *mcp_tool_annotations(const char *name) {
Expand Down Expand Up @@ -5686,6 +5705,112 @@ char *cbm_mcp_index_run_supervised_path(const char *root_path) {

bool cbm_path_within_root(const char *root_path, const char *abs_path); /* defined below */

static char *handle_diff_versions(cbm_mcp_server_t *srv, const char *args) {
char *project = cbm_mcp_get_string_arg(args, "project");
char *from_ver = cbm_mcp_get_string_arg(args, "from_version");
char *to_ver = cbm_mcp_get_string_arg(args, "to_version");
char *label_arg = cbm_mcp_get_string_arg(args, "label");
const char *label = (label_arg && label_arg[0]) ? label_arg : "Class";

if (!project || !from_ver || !to_ver) {
free(project);
free(from_ver);
free(to_ver);
free(label_arg);
return cbm_mcp_text_result("project, from_version, and to_version required", true);
}

cbm_store_t *store = resolve_store(srv, project);
if (!store) {
free(project);
free(from_ver);
free(to_ver);
free(label_arg);
return cbm_mcp_text_result("project not found", true);
}

char added_q[512], removed_q[512];
snprintf(added_q, sizeof(added_q),
"MATCH (n:%s {version:'%s'}) "
"OPTIONAL MATCH (m:%s {name:n.name,version:'%s'}) "
"WHERE m IS NULL RETURN n.name LIMIT 500",
label, to_ver, label, from_ver);
snprintf(removed_q, sizeof(removed_q),
"MATCH (n:%s {version:'%s'}) "
"OPTIONAL MATCH (m:%s {name:n.name,version:'%s'}) "
"WHERE m IS NULL RETURN n.name LIMIT 500",
label, from_ver, label, to_ver);

cbm_cypher_result_t added = {0}, removed = {0};
cbm_cypher_execute(store, added_q, project, 500, &added);
cbm_cypher_execute(store, removed_q, project, 500, &removed);

yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
yyjson_mut_val *root = yyjson_mut_obj(doc);
yyjson_mut_doc_set_root(doc, root);

yyjson_mut_val *added_arr = yyjson_mut_arr(doc);
for (int r = 0; r < added.row_count; r++) {
if (added.rows[r][0]) {
yyjson_mut_arr_add_strcpy(doc, added_arr, added.rows[r][0]);
}
}
yyjson_mut_val *removed_arr = yyjson_mut_arr(doc);
for (int r = 0; r < removed.row_count; r++) {
if (removed.rows[r][0]) {
yyjson_mut_arr_add_strcpy(doc, removed_arr, removed.rows[r][0]);
}
}

yyjson_mut_obj_add_val(doc, root, "added", added_arr);
yyjson_mut_obj_add_val(doc, root, "removed", removed_arr);
yyjson_mut_obj_add_strcpy(doc, root, "from_version", from_ver);
yyjson_mut_obj_add_strcpy(doc, root, "to_version", to_ver);

char *json = yyjson_mut_write(doc, 0, NULL);
char *result = cbm_mcp_text_result(json, false);
free(json);
yyjson_mut_doc_free(doc);
cbm_cypher_result_free(&added);
cbm_cypher_result_free(&removed);
free(project);
free(from_ver);
free(to_ver);
free(label_arg);
return result;
}

/* Derive a version tag from the last path segment when it looks like a
* version number (e.g. "/repos/MyApp/28.0" → "28.0").
* Writes to out[0..out_sz-1]; sets out[0]='\0' when nothing found. */
static void cbm_derive_version_from_path(const char *path, char *out, size_t out_sz) {
if (!path || !out || out_sz == 0) {
return;
}
out[0] = '\0';
const char *p = path + strlen(path);
while (p > path) {
p--;
if (*p == '/' || *p == '\\') {
const char *seg = p + 1;
size_t slen = strlen(seg);
if (slen > 0 && slen < 32 && isdigit((unsigned char)seg[0])) {
bool ok = true;
for (size_t i = 0; i < slen; i++) {
if (!isdigit((unsigned char)seg[i]) && seg[i] != '.') {
ok = false;
break;
}
}
if (ok) {
snprintf(out, out_sz, "%s", seg);
return;
}
}
}
}
}

static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
/* Supervisor gate: run the index in a crash/hang-isolating worker subprocess
* unless this process IS the worker or the kill switch (CBM_INDEX_SUPERVISOR=0)
Expand Down Expand Up @@ -5757,6 +5882,21 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
free(name_override);
cbm_pipeline_set_persistence(p, persistence);

/* Optional version tag: stamp every node with the given version string.
* Falls back to auto-detecting a version number from the trailing path segment
* (e.g. /exports/MyApp/28.0 → version="28.0"). */
char *version_tag = cbm_mcp_get_string_arg(args, "version");
if (version_tag && version_tag[0]) {
cbm_pipeline_set_version(p, version_tag);
} else {
char derived[64] = {0};
cbm_derive_version_from_path(repo_path, derived, sizeof(derived));
if (derived[0]) {
cbm_pipeline_set_version(p, derived);
}
}
free(version_tag);

char *project_name = heap_strdup(cbm_pipeline_project_name(p));

/* Bootstrap from artifact if no local DB exists */
Expand Down Expand Up @@ -7835,6 +7975,10 @@ char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const ch
return handle_get_architecture(srv, args_json);
}

if (strcmp(tool_name, "diff_versions") == 0) {
return handle_diff_versions(srv, args_json);
}

/* Pipeline-dependent tools */
if (strcmp(tool_name, "index_repository") == 0) {
return handle_index_repository(srv, args_json);
Expand Down
10 changes: 8 additions & 2 deletions src/pipeline/pass_definitions.c
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ static void append_json_str_array(char *buf, size_t bufsize, size_t *pos, const
}

/* Build properties JSON for a definition node. */
static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) {
static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def,
const char *version_tag) {
/* The complexity/loop/recursion metrics are only meaningful for executable
* units (Function/Method). Emitting them on the millions of Macro/Field/
* Variable/Class/Enum nodes — where they are always zero — bloats every
Expand Down Expand Up @@ -311,6 +312,11 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
append_json_string(buf, bufsize, &pos, "bt", def->body_tokens);
}

/* Version tag for cross-version diff queries */
if (version_tag && version_tag[0] && pos + CBM_SZ_256 < bufsize) {
append_json_string(buf, bufsize, &pos, "version", version_tag);
}

if (pos < bufsize - SKIP_ONE) {
buf[pos] = '}';
buf[pos + SKIP_ONE] = '\0';
Expand All @@ -323,7 +329,7 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
return;
}
char props[CBM_SZ_2K];
build_def_props(props, sizeof(props), def);
build_def_props(props, sizeof(props), def, ctx->version_tag);
int64_t node_id = cbm_gbuf_upsert_node(
ctx->gbuf, def->label ? def->label : "Function", def->name, def->qualified_name,
def->file_path ? def->file_path : rel, (int)def->start_line, (int)def->end_line, props);
Expand Down
18 changes: 13 additions & 5 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,8 @@ static void append_json_str_array(char *buf, size_t bufsize, size_t *pos, const
*pos = p;
}

static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) {
static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def,
const char *version_tag) {
/* Complexity/loop/recursion metrics are meaningful only for Function/Method.
* Gate the block so the millions of Macro/Field/Variable/Class/Enum nodes
* keep a lean properties blob (lossless — those fields are always zero for
Expand Down Expand Up @@ -522,6 +523,11 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
append_json_string(buf, bufsize, &pos, "bt", def->body_tokens);
}

/* Version tag for cross-version diff queries */
if (version_tag && version_tag[0] && pos + CBM_SZ_256 < bufsize) {
append_json_string(buf, bufsize, &pos, "version", version_tag);
}

if (pos < bufsize - SKIP_ONE) {
buf[pos] = '}';
buf[pos + SKIP_ONE] = '\0';
Expand Down Expand Up @@ -714,6 +720,7 @@ typedef struct {

const CBMMacroTable *macro_table; /* ObjectScript $$$macros (NULL if none) */
const CBMReturnTypeTable *return_type_table; /* ObjectScript return types (NULL if none) */
const char *version_tag; /* borrowed from pipeline ctx — version label for nodes */
} extract_ctx_t;

/* Cap on the number of index.file_oversized WARN lines (the full list still goes
Expand All @@ -722,9 +729,9 @@ enum { PP_OVERSIZED_WARN_MAX = 32 };

/* Insert one definition node (and its route if present) into the local gbuf. */
static void insert_def_into_gbuf(extract_worker_state_t *ws, const cbm_file_info_t *fi,
CBMDefinition *def) {
CBMDefinition *def, const char *version_tag) {
char props[CBM_SZ_2K];
build_def_props(props, sizeof(props), def);
build_def_props(props, sizeof(props), def, version_tag);
int64_t func_id =
cbm_gbuf_upsert_node(ws->local_gbuf, def->label ? def->label : "Function", def->name,
def->qualified_name, def->file_path ? def->file_path : fi->rel_path,
Expand Down Expand Up @@ -905,7 +912,7 @@ static void extract_worker(int worker_id, void *ctx_ptr) {
for (int d = 0; d < xr->defs.count; d++) {
CBMDefinition *def = &xr->defs.items[d];
if (def->qualified_name && def->name) {
insert_def_into_gbuf(ws, fi, def);
insert_def_into_gbuf(ws, fi, def, ec->version_tag);
}
}
cbm_free_result(xr);
Expand Down Expand Up @@ -952,7 +959,7 @@ static void extract_worker(int worker_id, void *ctx_ptr) {
for (int d = 0; d < result->defs.count; d++) {
CBMDefinition *def = &result->defs.items[d];
if (def->qualified_name && def->name) {
insert_def_into_gbuf(ws, fi, def);
insert_def_into_gbuf(ws, fi, def, ec->version_tag);
}
}

Expand Down Expand Up @@ -1173,6 +1180,7 @@ int cbm_parallel_extract_ex(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
.retain_per_file_max_bytes = resolved_opts.retain_per_file_max_bytes,
.macro_table = pp_macro_table,
.return_type_table = ctx->return_type_table,
.version_tag = ctx->version_tag,
};
atomic_init(&ec.next_worker_id, 0);
atomic_init(&ec.next_file_idx, 0);
Expand Down
16 changes: 14 additions & 2 deletions src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ struct cbm_pipeline {
char *branch_qn;
cbm_index_mode_t mode;
atomic_int cancelled;
bool persistence; /* write .codebase-memory/graph.db.zst after indexing */
bool persistence; /* write .codebase-memory/graph.db.zst after indexing */
char *version_tag; /* version label stamped on every node (e.g. "28.0") */

/* Indexing state (set during run) */
cbm_gbuf_t *gbuf;
Expand Down Expand Up @@ -194,6 +195,14 @@ void cbm_pipeline_set_persistence(cbm_pipeline_t *p, bool enabled) {
}
}

void cbm_pipeline_set_version(cbm_pipeline_t *p, const char *version_tag) {
if (!p) {
return;
}
free(p->version_tag);
p->version_tag = version_tag ? strdup(version_tag) : NULL;
}

bool cbm_pipeline_set_project_name(cbm_pipeline_t *p, const char *name) {
if (!p || !name || !name[0]) {
return false;
Expand Down Expand Up @@ -239,6 +248,7 @@ void cbm_pipeline_free(cbm_pipeline_t *p) {
p->file_errors_count = 0;
p->file_errors_cap = 0;
free(p->branch_qn);
free(p->version_tag);
free(p->saved_adr); /* freed here too: error paths can exit before the
* restore in dump_and_persist_hashes runs. Issue #516. */
p->saved_adr = NULL;
Expand Down Expand Up @@ -1077,7 +1087,8 @@ static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *file
cbm_store_get_file_hashes(check_store, p->project_name, &hashes, &hash_count);
cbm_store_free_file_hashes(hashes, hash_count);
cbm_store_close(check_store);
if (hash_count > 0 && file_count <= hash_count + (hash_count / PAIR_LEN)) {
if (hash_count > 0 && file_count <= hash_count + (hash_count / PAIR_LEN) &&
!p->version_tag) {
cbm_log_info("pipeline.route", "path", "incremental", "stored_hashes",
itoa_buf(hash_count));
int rc = cbm_pipeline_run_incremental(p, db_path, files, file_count);
Expand Down Expand Up @@ -1547,6 +1558,7 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
.path_aliases = path_aliases,
.excluded_dirs = p->excluded_dirs,
.excluded_count = p->excluded_count,
.version_tag = p->version_tag,
};

rc = run_extraction_phase(p, &ctx, files, file_count);
Expand Down
5 changes: 5 additions & 0 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path, cbm
* When enabled, the pipeline writes a compressed artifact after indexing. */
void cbm_pipeline_set_persistence(cbm_pipeline_t *p, bool enabled);

/* Tag every extracted node with a version string (e.g. "28.0", "30.0").
* Enables cross-version diff queries without extra Cypher syntax.
* When set, the incremental path is bypassed to guarantee a clean full index. */
void cbm_pipeline_set_version(cbm_pipeline_t *p, const char *version_tag);

/* Free a pipeline and all its internal state. NULL-safe. */
void cbm_pipeline_free(cbm_pipeline_t *p);

Expand Down
4 changes: 4 additions & 0 deletions src/pipeline/pipeline_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ typedef struct {
/* ObjectScript method-return-type table built from extracted definitions
* (NULL until pass_calls builds it). Owned by pipeline.c. */
const CBMReturnTypeTable *return_type_table;

/* Version tag for cross-version diff queries (e.g. "28.0", "30.0").
* When set, appended to every extracted node's properties JSON. */
const char *version_tag;
} cbm_pipeline_ctx_t;

static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *const *excluded_dirs,
Expand Down
1 change: 1 addition & 0 deletions tests/test_mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ TEST(mcp_tools_have_behavior_annotations) {
{"detect_changes", false, true, true, false},
{"manage_adr", false, true, false, false},
{"ingest_traces", false, false, false, false},
{"diff_versions", true, false, true, false},
};

char *json = cbm_mcp_tools_list();
Expand Down
Loading