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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,17 @@ codebase-memory-mcp cli --raw search_graph '{"label": "Function"}' | jq '.result
| `delete_project` | Remove a project and all its graph data. |
| `index_status` | Check indexing status of a project. |

`index_status` includes an additive `evidence` object. `evidence.index_snapshot`
reports the timestamp and Git HEAD captured when the last successful graph
snapshot was finalized, the current HEAD, tracked working-tree state, and a
`freshness` value: `current` means the indexed HEAD equals current HEAD and
tracked files are clean; `head_changed` means a later checkout/commit changed
HEAD; `working_tree_changed` means HEAD matches but tracked files are modified;
`unknown` means Git or comparison data is unavailable. Untracked files are not
compared. `evidence.coverage` reports discovered/indexed/excluded/failed file
counters when the stored index contains them; older indexes may report
`unknown` rather than guessing.

### Querying

| Tool | Description |
Expand All @@ -426,6 +437,14 @@ codebase-memory-mcp cli --raw search_graph '{"label": "Function"}' | jq '.result
| `get_graph_schema` | Node/edge counts, relationship patterns, property definitions per label. Run this first. |
| `get_code_snippet` | Read source code for a function by qualified name. |
| `get_architecture` | Codebase overview: languages, packages, routes, hotspots, clusters, ADR. |

`trace_path`/`trace_call_path` preserve their existing `callers`/`callees`
arrays and add `edge_evidence` for traversed relations when stored edge
properties contain provenance. Relation `confidence` is source-resolution
confidence from the indexer, not a probability of runtime correctness and not
BM25/semantic search relevance. Dynamic behavior such as reflection,
dependency injection, framework wiring, generated code, configuration, HTTP,
async messaging, and cross-repo links may remain inferred or unavailable.
| `search_code` | Grep-like text search within indexed project files. |
| `manage_adr` | CRUD for Architecture Decision Records. |
| `ingest_traces` | Ingest runtime traces to validate HTTP_CALLS edges. |
Expand Down
6 changes: 4 additions & 2 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -5540,8 +5540,10 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
* For the nested case, the child has no "type" field directly. Detect by
* walking named children for a variable_declaration. */
TSNode type_node = ts_node_child_by_field_name(child, TS_FIELD("type"));
TSNode name_node =
ts_node_is_null(type_node) ? (TSNode){0} : resolve_field_name_node(child);
TSNode name_node = {0};
if (!ts_node_is_null(type_node)) {
name_node = resolve_field_name_node(child);
}

if (ts_node_is_null(type_node)) {
uint32_t cnc = ts_node_named_child_count(child);
Expand Down
14 changes: 12 additions & 2 deletions internal/cbm/sqlite_writer.c
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,7 @@ static uint8_t *build_token_vec_record(const CBMDumpTokenVec *tv, int *out_len)
return data;
}

// Build a projects table record: (name, indexed_at, root_path)
// Build a projects table record.
static uint8_t *build_project_record(const char *name, const char *indexed_at,
const char *root_path, int *out_len) {
RecordBuilder r;
Expand All @@ -797,6 +797,13 @@ static uint8_t *build_project_record(const char *name, const char *indexed_at,
rec_add_text(&r, name);
rec_add_text(&r, indexed_at);
rec_add_text(&r, root_path);
/* indexed_git_head: unavailable in the direct writer path */
rec_add_null(&r);
/* files_discovered, files_indexed, files_excluded, files_failed */
rec_add_int(&r, 0);
rec_add_int(&r, 0);
rec_add_int(&r, 0);
rec_add_int(&r, 0);

uint8_t *data = rec_finalize(&r, out_len);
rec_free(&r);
Expand Down Expand Up @@ -2118,7 +2125,10 @@ static int write_db_after_nodes(write_db_ctx_t *w, uint32_t nodes_root) {
MasterEntry master[] = {
{"table", "projects", "projects", projects_root,
"CREATE TABLE projects (\n\t\tname TEXT PRIMARY KEY,\n\t\tindexed_at TEXT NOT "
"NULL,\n\t\troot_path TEXT NOT NULL\n\t)"},
"NULL,\n\t\troot_path TEXT NOT NULL,\n\t\tindexed_git_head TEXT,\n\t\tfiles_discovered "
"INTEGER NOT NULL DEFAULT 0,\n\t\tfiles_indexed INTEGER NOT NULL DEFAULT 0,\n\t\t"
"files_excluded INTEGER NOT NULL DEFAULT 0,\n\t\tfiles_failed INTEGER NOT NULL DEFAULT "
"0\n\t)"},
{"index", "sqlite_autoindex_projects_1", "projects", autoindex_projects_root, NULL},
{"table", "file_hashes", "file_hashes", file_hashes_root,
"CREATE TABLE file_hashes (\n\t\tproject TEXT NOT NULL REFERENCES projects(name) ON "
Expand Down
24 changes: 12 additions & 12 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -2503,14 +2503,14 @@ enum {
};
static const size_t ZIP_MAX_UNCOMP = 500U * 1024U * 1024U;

static uint16_t zip_read_u16le(const unsigned char *p) {
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << BYTE_SHIFT));
static uint16_t zip_read_le16(const unsigned char *p) {
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[CLI_SKIP_ONE] << BYTE_SHIFT));
}

static uint32_t zip_read_u32le(const unsigned char *p) {
return ((uint32_t)p[0]) | ((uint32_t)p[1] << BYTE_SHIFT) |
((uint32_t)p[2] << (BYTE_SHIFT * CLI_PAIR_LEN)) |
((uint32_t)p[3] << (BYTE_SHIFT * CLI_JSON_INDENT));
static uint32_t zip_read_le32(const unsigned char *p) {
return (uint32_t)p[0] | ((uint32_t)p[CLI_SKIP_ONE] << BYTE_SHIFT) |
((uint32_t)p[CLI_PAIR_LEN] << (BYTE_SHIFT * CLI_PAIR_LEN)) |
((uint32_t)p[CLI_JSON_INDENT] << (BYTE_SHIFT * CLI_JSON_INDENT));
}

/* Decompress a single zip entry (stored or deflated). Returns malloc'd buffer
Expand Down Expand Up @@ -2574,14 +2574,14 @@ unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_l
break;
}

uint16_t method = zip_read_u16le(data + pos + ZIP_OFF_METHOD);
uint32_t comp_size = zip_read_u32le(data + pos + ZIP_OFF_COMP);
uint32_t uncomp_size = zip_read_u32le(data + pos + ZIP_OFF_UNCOMP);
uint16_t name_len = zip_read_u16le(data + pos + ZIP_OFF_NAMELEN);
uint16_t extra_len = zip_read_u16le(data + pos + ZIP_OFF_EXTRALEN);
uint16_t method = zip_read_le16(data + pos + ZIP_OFF_METHOD);
size_t comp_size = zip_read_le32(data + pos + ZIP_OFF_COMP);
size_t uncomp_size = zip_read_le32(data + pos + ZIP_OFF_UNCOMP);
uint16_t name_len = zip_read_le16(data + pos + ZIP_OFF_NAMELEN);
uint16_t extra_len = zip_read_le16(data + pos + ZIP_OFF_EXTRALEN);

int header_end = pos + ZIP_HDR_SZ + name_len + extra_len;
if (header_end > data_len || comp_size > (uint32_t)(data_len - header_end)) {
if (header_end > data_len || comp_size > (size_t)(data_len - header_end)) {
break;
}

Expand Down
10 changes: 2 additions & 8 deletions src/cypher/cypher.c
Original file line number Diff line number Diff line change
Expand Up @@ -2131,12 +2131,6 @@ static const char *node_prop(const cbm_node_t *n, const char *prop, cbm_store_t
if (rv && rv[0]) {
snprintf(out, CBM_SZ_512, "%s", rv);
res = out;
} else if (strcmp(prop, "start_line") == 0) {
snprintf(out, CBM_SZ_512, "%d", full.start_line);
res = out;
} else if (strcmp(prop, "end_line") == 0) {
snprintf(out, CBM_SZ_512, "%d", full.end_line);
res = out;
} else if (full.properties_json && full.properties_json[0] == '{') {
const char *jv = json_extract_prop(full.properties_json, prop, out, CBM_SZ_512);
if (jv && jv[0]) {
Expand Down Expand Up @@ -2442,11 +2436,11 @@ static bool eval_condition(const cbm_condition_t *c, binding_t *b) {

/* IS NULL / IS NOT NULL */
if (strcmp(c->op, "IS NULL") == 0) {
result = (actual[0] == '\0');
result = (!actual || actual[0] == '\0');
return c->negated ? !result : result;
}
if (strcmp(c->op, "IS NOT NULL") == 0) {
result = (actual[0] != '\0');
result = (actual && actual[0] != '\0');
return c->negated ? !result : result;
}

Expand Down
36 changes: 36 additions & 0 deletions src/git/git_context.c
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,42 @@ int cbm_git_context_resolve(const char *path, cbm_git_context_t *out) {
return 0;
}

int cbm_git_tracked_dirty(const char *path, bool *out_dirty) {
if (!out_dirty) {
return CBM_NOT_FOUND;
}
*out_dirty = false;

if (!path || !git_validate_repo_path(path)) {
return CBM_NOT_FOUND;
}

char cmd[GIT_CMD_MAX];
#ifdef _WIN32
const char *null_dev = "NUL";
#else
const char *null_dev = "/dev/null";
#endif
int n = snprintf(cmd, sizeof(cmd), "git -C \"%s\" status --porcelain --untracked-files=no 2>%s",
path, null_dev);
if (n < 0 || n >= (int)sizeof(cmd)) {
return CBM_NOT_FOUND;
}

FILE *fp = cbm_popen(cmd, "r");
if (!fp) {
return CBM_NOT_FOUND;
}
char buf[GIT_OUTPUT_MAX];
bool has_output = fgets(buf, sizeof(buf), fp) != NULL;
int rc = cbm_pclose(fp);
if (rc != 0) {
return CBM_NOT_FOUND;
}
*out_dirty = has_output;
return 0;
}

char *cbm_git_context_branch_qn(const char *project_name, const cbm_git_context_t *ctx) {
const char *project = project_name && project_name[0] ? project_name : "project";
const char *slug = "working-tree";
Expand Down
3 changes: 3 additions & 0 deletions src/git/git_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ typedef struct {

int cbm_git_context_resolve(const char *path, cbm_git_context_t *out);
void cbm_git_context_free(cbm_git_context_t *ctx);
/* Returns 0 when tracked working-tree dirtiness was determined, non-zero when
* unavailable. Untracked files are intentionally not compared. */
int cbm_git_tracked_dirty(const char *path, bool *out_dirty);
char *cbm_git_context_branch_qn(const char *project_name, const cbm_git_context_t *ctx);
int cbm_git_context_props_json(const cbm_git_context_t *ctx, char *buf, int buf_size);

Expand Down
168 changes: 165 additions & 3 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,86 @@ static void add_git_context_json(yyjson_mut_doc *doc, yyjson_mut_val *obj, const
cbm_git_context_free(&ctx);
}

static void add_index_evidence_json(yyjson_mut_doc *doc, yyjson_mut_val *root,
const cbm_project_t *proj) {
yyjson_mut_val *evidence = yyjson_mut_obj(doc);
yyjson_mut_val *snap = yyjson_mut_obj(doc);
const char *indexed_head = (proj && proj->indexed_git_head && proj->indexed_git_head[0])
? proj->indexed_git_head
: NULL;
const char *project_root = proj ? proj->root_path : NULL;
cbm_git_context_t ctx = {0};
int git_rc = cbm_git_context_resolve(project_root, &ctx);
bool dirty = false;
int dirty_rc = (project_root && git_rc == 0 && ctx.is_git)
? cbm_git_tracked_dirty(project_root, &dirty)
: CBM_NOT_FOUND;

add_git_context_string(doc, snap, "indexed_at", proj ? proj->indexed_at : NULL);
add_git_context_string(doc, snap, "indexed_git_head", indexed_head);
add_git_context_string(
doc, snap, "current_git_head",
(git_rc == 0 && ctx.is_git && ctx.head_sha && ctx.head_sha[0]) ? ctx.head_sha : NULL);
const char *repo_state = "unavailable";
if (git_rc == 0 && ctx.root_exists && !ctx.is_git) {
repo_state = "not_git";
} else if (git_rc == 0 && ctx.is_git && dirty_rc == 0) {
repo_state = dirty ? "dirty" : "clean";
}
yyjson_mut_obj_add_str(doc, snap, "repository_state", repo_state);
if (indexed_head && git_rc == 0 && ctx.is_git && ctx.head_sha && ctx.head_sha[0]) {
yyjson_mut_obj_add_bool(doc, snap, "snapshot_matches_current_head",
strcmp(indexed_head, ctx.head_sha) == 0);
} else {
yyjson_mut_obj_add_null(doc, snap, "snapshot_matches_current_head");
}
if (indexed_head && git_rc == 0 && ctx.is_git && ctx.head_sha && ctx.head_sha[0] &&
dirty_rc == 0) {
yyjson_mut_obj_add_bool(doc, snap, "snapshot_matches_working_tree",
strcmp(indexed_head, ctx.head_sha) == 0 && !dirty);
} else {
yyjson_mut_obj_add_null(doc, snap, "snapshot_matches_working_tree");
}
const char *freshness = "unknown";
if (indexed_head && git_rc == 0 && ctx.is_git && ctx.head_sha && ctx.head_sha[0] &&
dirty_rc == 0) {
if (strcmp(indexed_head, ctx.head_sha) != 0) {
freshness = "head_changed";
} else {
freshness = dirty ? "working_tree_changed" : "current";
}
} else if (git_rc == 0 && ctx.root_exists && !ctx.is_git) {
freshness = "unknown";
}
yyjson_mut_obj_add_str(doc, snap, "freshness", freshness);
yyjson_mut_obj_add_val(doc, evidence, "index_snapshot", snap);

yyjson_mut_val *cov = yyjson_mut_obj(doc);
int discovered = proj ? proj->files_discovered : 0;
int indexed = proj ? proj->files_indexed : 0;
int excluded = proj ? proj->files_excluded : 0;
int failed = proj ? proj->files_failed : 0;
yyjson_mut_obj_add_int(doc, cov, "files_discovered", discovered);
yyjson_mut_obj_add_int(doc, cov, "files_indexed", indexed);
yyjson_mut_obj_add_int(doc, cov, "files_excluded", excluded);
yyjson_mut_obj_add_int(doc, cov, "files_failed", failed);
yyjson_mut_obj_add_str(doc, cov, "coverage_status",
failed > 0 || excluded > 0 ? "partial"
: discovered > 0 && indexed == discovered ? "complete"
: "unknown");
yyjson_mut_obj_add_val(doc, evidence, "coverage", cov);
yyjson_mut_val *limits = yyjson_mut_arr(doc);
yyjson_mut_val *lim = yyjson_mut_obj(doc);
yyjson_mut_obj_add_str(doc, lim, "code", "UNTRACKED_FILES_NOT_COMPARED");
yyjson_mut_obj_add_str(doc, lim, "message",
"Working-tree freshness compares current HEAD and tracked "
"modifications; untracked files are not compared.");
yyjson_mut_arr_add_val(limits, lim);
yyjson_mut_obj_add_val(doc, evidence, "limitations", limits);
yyjson_mut_obj_add_val(doc, root, "evidence", evidence);
cbm_git_context_free(&ctx);
}

/* Build a helpful error listing available projects. Caller must free() result. */
static char *build_project_list_error(const char *reason) {
char dir_path[CBM_SZ_1K];
Expand Down Expand Up @@ -2152,9 +2232,8 @@ static char *handle_index_status(cbm_mcp_server_t *srv, const char *args) {
yyjson_mut_obj_add_strcpy(doc, root, "root_path",
proj_info.root_path ? proj_info.root_path : "");
add_git_context_json(doc, root, proj_info.root_path);
safe_str_free(&proj_info.name);
safe_str_free(&proj_info.indexed_at);
safe_str_free(&proj_info.root_path);
add_index_evidence_json(doc, root, &proj_info);
cbm_project_free_fields(&proj_info);
}
if (nodes == 0) {
yyjson_mut_obj_add_str(
Expand Down Expand Up @@ -2949,6 +3028,79 @@ static int clamp_mcp_depth(int depth, const char *tool) {
return depth;
}

static const char *edge_evidence_status(const char *strategy, double confidence, int candidates) {
if (!strategy || !strategy[0]) {
return "unavailable";
}
if (candidates > 1) {
return "ambiguous";
}
if (strstr(strategy, "heur") || strstr(strategy, "fuzzy") || confidence < 0.8) {
return "inferred";
}
return "verified";
}

static const char *edge_resolution_strategy(const char *strategy) {
if (!strategy || !strategy[0]) {
return "unknown";
}
if (strstr(strategy, "lsp")) {
return "hybrid_lsp";
}
if (strstr(strategy, "import") || strstr(strategy, "same_module") ||
strstr(strategy, "receiver")) {
return "direct_ast";
}
if (strstr(strategy, "fuzzy") || strstr(strategy, "heur")) {
return "heuristic";
}
return strategy;
}

static yyjson_mut_val *trace_edges_to_json_array(yyjson_mut_doc *doc, cbm_traverse_result_t *tr) {
yyjson_mut_val *arr = yyjson_mut_arr(doc);
for (int i = 0; i < tr->edge_count; i++) {
yyjson_mut_val *item = yyjson_mut_obj(doc);
yyjson_mut_obj_add_str(doc, item, "from",
tr->edges[i].from_name ? tr->edges[i].from_name : "");
yyjson_mut_obj_add_str(doc, item, "to", tr->edges[i].to_name ? tr->edges[i].to_name : "");
yyjson_mut_obj_add_str(doc, item, "type", tr->edges[i].type ? tr->edges[i].type : "");
yyjson_mut_val *edge = yyjson_mut_obj(doc);
const char *props = tr->edges[i].properties_json;
yyjson_doc *pdoc = props ? yyjson_read(props, strlen(props), 0) : NULL;
yyjson_val *proot = pdoc ? yyjson_doc_get_root(pdoc) : NULL;
yyjson_val *v = proot ? yyjson_obj_get(proot, "strategy") : NULL;
const char *strategy = yyjson_is_str(v) ? yyjson_get_str(v) : NULL;
v = proot ? yyjson_obj_get(proot, "confidence") : NULL;
bool has_conf = yyjson_is_num(v);
double conf = has_conf ? yyjson_get_num(v) : 0.0;
v = proot ? yyjson_obj_get(proot, "candidates") : NULL;
int candidates = yyjson_is_int(v) ? (int)yyjson_get_int(v) : 0;
yyjson_mut_obj_add_str(doc, edge, "resolution_strategy",
edge_resolution_strategy(strategy));
if (has_conf) {
yyjson_mut_obj_add_real(doc, edge, "confidence", conf);
} else {
yyjson_mut_obj_add_null(doc, edge, "confidence");
}
if (candidates > 0) {
yyjson_mut_obj_add_int(doc, edge, "candidate_count", candidates);
} else {
yyjson_mut_obj_add_null(doc, edge, "candidate_count");
}
yyjson_mut_obj_add_null(doc, edge, "source_location");
yyjson_mut_obj_add_str(doc, edge, "evidence_status",
edge_evidence_status(strategy, conf, candidates));
yyjson_mut_obj_add_val(doc, item, "edge", edge);
yyjson_mut_arr_add_val(arr, item);
if (pdoc) {
yyjson_doc_free(pdoc);
}
}
return arr;
}

static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
char *func_name = cbm_mcp_get_string_arg(args, "function_name");
char *project = get_project_arg(args);
Expand Down Expand Up @@ -3093,6 +3245,16 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
doc, root, "callers",
bfs_to_json_array(doc, &tr_in, risk_labels, include_tests, data_flow));
}
yyjson_mut_val *edge_evidence = yyjson_mut_obj(doc);
if (do_outbound) {
yyjson_mut_obj_add_val(doc, edge_evidence, "outbound",
trace_edges_to_json_array(doc, &tr_out));
}
if (do_inbound) {
yyjson_mut_obj_add_val(doc, edge_evidence, "inbound",
trace_edges_to_json_array(doc, &tr_in));
}
yyjson_mut_obj_add_val(doc, root, "edge_evidence", edge_evidence);

/* Serialize BEFORE freeing traversal results (yyjson borrows strings) */
char *json = yy_doc_to_str(doc);
Expand Down
Loading
Loading