mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-12 09:47:20 +08:00
fix: harden index persistence and MCP outputs
Make index writes and artifact refreshes use verified temp files plus atomic replacement, preserving Windows replace semantics and avoiding fixed temp-path collisions. Thread the remaining pipeline confidence knobs through config while keeping zero-valued defaults backward compatible with existing upstream behavior. Tighten route-node gates and architecture route output so infra URL nodes stay queryable in the graph but do not pollute get_architecture route summaries; preserve code-created absolute URL routes for all language extractors. Harden MCP and CLI path handling around cache paths, grep scratch files, update downloads, checksum validation, stale checks, and project DB discovery without writing protocol noise to stdout. Add regression coverage for corrupt-DB recovery, concurrent atomic writes, tmp-prefixed project DBs, duplicate search result fields, route output filtering, config registry entries, and focused suite selectors. Validated with: git diff --check; make -f Makefile.cbm build/c/test-runner; focused suites security, artifact, graph_buffer, sqlite_writer, store_search, store_bulk, store_pragmas, store_checkpoint, watcher, infrascan, pipeline, store_nodes, mcp, cli, parallel, tool_consolidation, incremental, and store_arch; make -f Makefile.cbm cbm; isolated dogfood index/query run. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
@@ -649,8 +649,9 @@ static bool has_filesystem_extension(const char *path) {
|
||||
|
||||
static const char *const hard_file_exts[] = {
|
||||
".cfg", ".conf", ".credentials", ".crt", ".db", ".env",
|
||||
".ini", ".key", ".pem", ".pid", ".properties", ".service",
|
||||
".sock", ".socket", ".sqlite", ".toml", NULL};
|
||||
".ini", ".key", ".log", ".md", ".pdf", ".pem",
|
||||
".pid", ".properties", ".rst", ".service", ".sock", ".socket",
|
||||
".sqlite", ".toml", ".txt", NULL};
|
||||
for (int i = 0; hard_file_exts[i]; i++) {
|
||||
if (path_ext_matches(ext, hard_file_exts[i])) {
|
||||
return true;
|
||||
@@ -716,6 +717,11 @@ bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *
|
||||
if (!path || !path[0]) {
|
||||
return false;
|
||||
}
|
||||
/* Routes never contain whitespace; reject command/description strings
|
||||
* (e.g. "/autorun test task description") that start with '/'. */
|
||||
if (strpbrk(path, " \t\r\n")) {
|
||||
return false;
|
||||
}
|
||||
if (strncmp(path, "http://", 7) == 0 || strncmp(path, "https://", 8) == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -725,6 +731,16 @@ bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *
|
||||
if (path[0] != '/') {
|
||||
return false;
|
||||
}
|
||||
/* Reject CLI slash-command syntax ("/ar:allow", "/gh:pr") without blocking
|
||||
* ordinary route parameters in later segments ("/teams/:team/users/:id"). */
|
||||
const char *first_slash = strchr(path + 1, '/');
|
||||
size_t first_segment_len = first_slash ? (size_t)(first_slash - (path + 1)) : strlen(path + 1);
|
||||
if (first_segment_len > 0) {
|
||||
const char *colon = memchr(path + 1, ':', first_segment_len);
|
||||
if (colon && path[1] != ':') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (callee_is_delimiter_or_filesystem_builder(callee_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#ifndef CBM_SERVICE_PATTERNS_H
|
||||
#define CBM_SERVICE_PATTERNS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Edge type returned by pattern match. */
|
||||
typedef enum {
|
||||
CBM_SVC_NONE = 0, /* Not a service pattern — use normal CALLS */
|
||||
@@ -51,6 +53,15 @@ const char *cbm_service_pattern_http_method(const char *callee_name);
|
||||
* Returns NULL if not a known route registration method. */
|
||||
const char *cbm_service_pattern_route_method(const char *callee_name);
|
||||
|
||||
/* Classify a string literal as a genuine HTTP route path. Returns true for
|
||||
* real routes ("/api/orders", "/users/:id", "https://..."); false for file
|
||||
* paths ("/tmp/foo.md"), CLI slash-commands ("/ar:allow"), description strings
|
||||
* (contain whitespace), and other non-route args that extraction may surface.
|
||||
* Used to gate Route-node creation/emit sites so non-routes don't pollute the
|
||||
* graph. callee_name is used to reject string-builder callees (str.split,
|
||||
* os.path.join, ...). May be NULL. */
|
||||
bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name);
|
||||
|
||||
/* Get the broker name for an async QN (e.g., "pubsub" from a Pub/Sub QN).
|
||||
* Returns NULL if not an async pattern. */
|
||||
const char *cbm_service_pattern_broker(const char *resolved_qn);
|
||||
|
||||
+288
-59
@@ -449,8 +449,17 @@ typedef struct {
|
||||
PageRef *leaves;
|
||||
int leaf_count;
|
||||
int leaf_cap;
|
||||
bool ok;
|
||||
} PageBuilder;
|
||||
|
||||
static bool write_bytes_at(FILE *fp, long offset, const void *data, size_t len) {
|
||||
return fp && data && fseek(fp, offset, SEEK_SET) == 0 && fwrite(data, SKIP_ONE, len, fp) == len;
|
||||
}
|
||||
|
||||
static bool write_page_at(FILE *fp, uint32_t page_num, const uint8_t page[CBM_PAGE_SIZE]) {
|
||||
return write_bytes_at(fp, (long)(page_num - SKIP_ONE) * CBM_PAGE_SIZE, page, CBM_PAGE_SIZE);
|
||||
}
|
||||
|
||||
static void pb_init(PageBuilder *pb, FILE *fp, uint32_t start_page, bool is_index) {
|
||||
pb->fp = fp;
|
||||
pb->next_page = start_page;
|
||||
@@ -464,6 +473,7 @@ static void pb_init(PageBuilder *pb, FILE *fp, uint32_t start_page, bool is_inde
|
||||
pb->leaves = NULL;
|
||||
pb->leaf_count = 0;
|
||||
pb->leaf_cap = 0;
|
||||
pb->ok = true;
|
||||
}
|
||||
|
||||
static void pb_free(PageBuilder *pb) {
|
||||
@@ -473,6 +483,9 @@ static void pb_free(PageBuilder *pb) {
|
||||
}
|
||||
free(pb->leaves);
|
||||
}
|
||||
pb->leaves = NULL;
|
||||
pb->leaf_count = 0;
|
||||
pb->leaf_cap = 0;
|
||||
}
|
||||
|
||||
// Flush current leaf page to file
|
||||
@@ -480,6 +493,9 @@ static void pb_flush_leaf(PageBuilder *pb) {
|
||||
if (pb->cell_count == 0) {
|
||||
return;
|
||||
}
|
||||
if (!pb->ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
int hdr = pb->page1_offset;
|
||||
// Write leaf page header
|
||||
@@ -493,8 +509,10 @@ static void pb_flush_leaf(PageBuilder *pb) {
|
||||
pb->next_page = cbm_skip_pending_byte(pb->next_page);
|
||||
uint32_t page_num = pb->next_page;
|
||||
long offset = (long)(page_num - SKIP_ONE) * CBM_PAGE_SIZE;
|
||||
(void)fseek(pb->fp, offset, SEEK_SET);
|
||||
(void)fwrite(pb->page, SKIP_ONE, CBM_PAGE_SIZE, pb->fp);
|
||||
if (!write_bytes_at(pb->fp, offset, pb->page, CBM_PAGE_SIZE)) {
|
||||
pb->ok = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Record this leaf for interior page building
|
||||
if (pb->leaf_count >= pb->leaf_cap) {
|
||||
@@ -502,8 +520,8 @@ static void pb_flush_leaf(PageBuilder *pb) {
|
||||
pb->leaf_cap = old_cap == 0 ? INITIAL_LEAF_CAP : old_cap * GROWTH_FACTOR;
|
||||
void *tmp = realloc(pb->leaves, (size_t)pb->leaf_cap * sizeof(PageRef));
|
||||
if (!tmp) {
|
||||
free(pb->leaves);
|
||||
pb->leaves = NULL;
|
||||
pb_free(pb);
|
||||
pb->ok = false;
|
||||
return;
|
||||
}
|
||||
pb->leaves = (PageRef *)tmp;
|
||||
@@ -534,6 +552,10 @@ static bool pb_cell_fits(PageBuilder *pb, int cell_len) {
|
||||
// For table leaves: varint(payload_len) + varint(rowid) + payload
|
||||
// For index leaves: varint(payload_len) + payload
|
||||
static void pb_add_cell(PageBuilder *pb, const uint8_t *cell, int cell_len) {
|
||||
if (!pb->ok || !cell || cell_len <= 0 || !pb_cell_fits(pb, cell_len)) {
|
||||
pb->ok = false;
|
||||
return;
|
||||
}
|
||||
// Write cell content (grows down)
|
||||
pb->content_offset -= cell_len;
|
||||
memcpy(pb->page + pb->content_offset, cell, cell_len);
|
||||
@@ -568,8 +590,14 @@ static int build_interior_cell(const PageRef *child, bool is_index, uint8_t *cel
|
||||
put_u32(cell_buf, child->page_num);
|
||||
return BTREE_PTR_SIZE + put_varint(cell_buf + BTREE_PTR_SIZE, child->max_key);
|
||||
}
|
||||
if (!child->sep_cell || child->sep_cell_len <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int clen = BTREE_PTR_SIZE + child->sep_cell_len;
|
||||
uint8_t *data = (uint8_t *)malloc(clen);
|
||||
if (!data) {
|
||||
return 0;
|
||||
}
|
||||
put_u32(data, child->page_num);
|
||||
memcpy(data + 4, child->sep_cell, child->sep_cell_len);
|
||||
*out_heap = data;
|
||||
@@ -591,16 +619,23 @@ static int write_interior_page(PageBuilder *pb, uint8_t *page, int cell_count, i
|
||||
page[HDR_FRAGBYTES_OFF] = 0;
|
||||
put_u32(page + HDR_RIGHTCHILD_OFF, right_child_page);
|
||||
|
||||
(void)fseek(pb->fp, (long)(pnum - SKIP_ONE) * CBM_PAGE_SIZE, SEEK_SET);
|
||||
(void)fwrite(page, SKIP_ONE, CBM_PAGE_SIZE, pb->fp);
|
||||
if (!write_page_at(pb->fp, pnum, page)) {
|
||||
pb->ok = false;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (parent_count >= *parent_cap) {
|
||||
int old_pcap = *parent_cap;
|
||||
*parent_cap = old_pcap == 0 ? INITIAL_PARENT_CAP : old_pcap * GROWTH_FACTOR;
|
||||
PageRef *tmp = (PageRef *)realloc(*parents, *parent_cap * sizeof(PageRef));
|
||||
if (!tmp) {
|
||||
for (int j = 0; j < parent_count; j++) {
|
||||
free((*parents)[j].sep_cell);
|
||||
}
|
||||
free(*parents);
|
||||
*parents = NULL;
|
||||
*parent_cap = 0;
|
||||
pb->ok = false;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
*parents = tmp;
|
||||
@@ -612,6 +647,10 @@ static int write_interior_page(PageBuilder *pb, uint8_t *page, int cell_count, i
|
||||
if (is_index && children[right_child_idx].sep_cell) {
|
||||
int slen = children[right_child_idx].sep_cell_len;
|
||||
(*parents)[parent_count].sep_cell = (uint8_t *)malloc(slen);
|
||||
if (!(*parents)[parent_count].sep_cell) {
|
||||
pb->ok = false;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
memcpy((*parents)[parent_count].sep_cell, children[right_child_idx].sep_cell, slen);
|
||||
(*parents)[parent_count].sep_cell_len = slen;
|
||||
} else {
|
||||
@@ -633,13 +672,16 @@ static void free_children(PageRef *children, int child_count, const PageRef *lea
|
||||
|
||||
// Fill an interior page with cells from children[*idx..child_count-2].
|
||||
// Updates cell_count, content_offset, ptr_offset, and *idx.
|
||||
static void fill_interior_page(uint8_t *page, const PageRef *children, int child_count,
|
||||
static bool fill_interior_page(uint8_t *page, const PageRef *children, int child_count,
|
||||
bool is_index, int *idx, int *cell_count, int *content_offset,
|
||||
int *ptr_offset) {
|
||||
while (*idx < child_count - SKIP_ONE) {
|
||||
uint8_t tbuf[INTERIOR_CELL_BUF];
|
||||
uint8_t *heap_cell = NULL;
|
||||
int clen = build_interior_cell(&children[*idx], is_index, tbuf, &heap_cell);
|
||||
if (clen <= 0) {
|
||||
return false;
|
||||
}
|
||||
uint8_t *cell_data = heap_cell ? heap_cell : tbuf;
|
||||
|
||||
int available = *content_offset - *ptr_offset - CELL_PTR_SIZE;
|
||||
@@ -656,6 +698,7 @@ static void fill_interior_page(uint8_t *page, const PageRef *children, int child
|
||||
free(heap_cell);
|
||||
(*idx)++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t pb_build_interior(PageBuilder *pb, bool is_index) {
|
||||
@@ -682,8 +725,11 @@ static uint32_t pb_build_interior(PageBuilder *pb, bool is_index) {
|
||||
int content_offset = CBM_PAGE_SIZE;
|
||||
int ptr_offset = BTREE_INTERIOR_HDR;
|
||||
|
||||
fill_interior_page(page, children, child_count, is_index, &i, &cell_count,
|
||||
&content_offset, &ptr_offset);
|
||||
if (!fill_interior_page(page, children, child_count, is_index, &i, &cell_count,
|
||||
&content_offset, &ptr_offset)) {
|
||||
pb->ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
int right_child_idx = (i < child_count - SKIP_ONE) ? i : child_count - SKIP_ONE;
|
||||
uint32_t right_child_page = 0;
|
||||
@@ -700,11 +746,16 @@ static uint32_t pb_build_interior(PageBuilder *pb, bool is_index) {
|
||||
right_child_page, children, right_child_idx,
|
||||
is_index, &parents, parent_count, &parent_cap);
|
||||
if (parent_count < 0) {
|
||||
pb->ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free_children(children, child_count, pb->leaves);
|
||||
if (!pb->ok) {
|
||||
free_children(parents, parent_count > 0 ? parent_count : 0, pb->leaves);
|
||||
return 0;
|
||||
}
|
||||
children = parents;
|
||||
child_count = parent_count;
|
||||
}
|
||||
@@ -861,6 +912,7 @@ static uint32_t write_overflow_pages(FILE *fp, uint32_t *next_page, const uint8_
|
||||
|
||||
int offset = 0;
|
||||
while (offset < data_len) {
|
||||
*next_page = cbm_skip_pending_byte(*next_page);
|
||||
uint32_t pnum = (*next_page)++;
|
||||
if (first_page == 0) {
|
||||
first_page = pnum;
|
||||
@@ -870,8 +922,9 @@ static uint32_t write_overflow_pages(FILE *fp, uint32_t *next_page, const uint8_
|
||||
if (prev_next_ptr_offset >= 0) {
|
||||
uint8_t ptr[BTREE_PTR_SIZE];
|
||||
put_u32(ptr, pnum);
|
||||
(void)fseek(fp, prev_next_ptr_offset, SEEK_SET);
|
||||
(void)fwrite(ptr, SKIP_ONE, BTREE_PTR_SIZE, fp);
|
||||
if (!write_bytes_at(fp, prev_next_ptr_offset, ptr, BTREE_PTR_SIZE)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int chunk = data_len - offset;
|
||||
@@ -886,8 +939,9 @@ static uint32_t write_overflow_pages(FILE *fp, uint32_t *next_page, const uint8_
|
||||
|
||||
long page_offset = (long)(pnum - SKIP_ONE) * CBM_PAGE_SIZE;
|
||||
prev_next_ptr_offset = page_offset;
|
||||
(void)fseek(fp, page_offset, SEEK_SET);
|
||||
(void)fwrite(page, SKIP_ONE, CBM_PAGE_SIZE, fp);
|
||||
if (!write_bytes_at(fp, page_offset, page, CBM_PAGE_SIZE)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
offset += chunk;
|
||||
}
|
||||
@@ -1038,11 +1092,13 @@ static bool pb_ensure_leaf_cap(PageBuilder *pb) {
|
||||
pb->leaf_cap = pb->leaf_cap == 0 ? INITIAL_LEAF_CAP : pb->leaf_cap * GROWTH_FACTOR;
|
||||
void *tmp = realloc(pb->leaves, (size_t)pb->leaf_cap * sizeof(PageRef));
|
||||
if (!tmp) {
|
||||
free(pb->leaves);
|
||||
pb->leaves = NULL;
|
||||
pb_free(pb);
|
||||
pb->ok = false;
|
||||
return false;
|
||||
}
|
||||
pb->leaves = (PageRef *)tmp;
|
||||
memset(&pb->leaves[pb->leaf_count], 0,
|
||||
((size_t)pb->leaf_cap - (size_t)pb->leaf_count) * sizeof(PageRef));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1081,7 +1137,10 @@ static int get_varint(const uint8_t *buf, uint64_t *out) {
|
||||
// overflow pages: varint(payload_len) + payload[0..local) + u32(first_ovfl).
|
||||
// Returns the (possibly new, malloc'd) cell; frees the original when replaced.
|
||||
static uint8_t *overflowize_index_cell(FILE *fp, uint32_t *next_page, uint8_t *cell,
|
||||
int *cell_len) {
|
||||
int *cell_len, bool *ok) {
|
||||
if (!*ok) {
|
||||
return cell;
|
||||
}
|
||||
uint64_t plen = 0;
|
||||
int vlen = get_varint(cell, &plen);
|
||||
if ((int64_t)plen <= INDEX_OVERFLOW_MAX_LOCAL) {
|
||||
@@ -1092,10 +1151,15 @@ static uint8_t *overflowize_index_cell(FILE *fp, uint32_t *next_page, uint8_t *c
|
||||
int local = (k <= INDEX_OVERFLOW_MAX_LOCAL) ? (int)k : INDEX_OVERFLOW_MIN_LOCAL;
|
||||
uint32_t first_ovfl =
|
||||
write_overflow_pages(fp, next_page, cell + vlen + local, (int)plen - local);
|
||||
if (first_ovfl == 0) {
|
||||
*ok = false;
|
||||
return cell;
|
||||
}
|
||||
int nlen = vlen + local + BTREE_PTR_SIZE;
|
||||
uint8_t *data = (uint8_t *)malloc((size_t)nlen);
|
||||
if (!data) {
|
||||
return cell; /* fall back to the (broken) inline form on OOM */
|
||||
*ok = false;
|
||||
return cell;
|
||||
}
|
||||
memcpy(data, cell, (size_t)(vlen + local));
|
||||
put_u32(data + vlen + local, first_ovfl);
|
||||
@@ -1126,6 +1190,7 @@ static void pb_add_table_cell_with_flush(PageBuilder *pb, int64_t rowid, const u
|
||||
uint32_t overflow_page = write_overflow_pages(pb->fp, &pb->next_page, payload + local_len,
|
||||
payload_len - local_len);
|
||||
if (overflow_page == 0) {
|
||||
pb->ok = false;
|
||||
return; // overflow write failed
|
||||
}
|
||||
|
||||
@@ -1136,11 +1201,13 @@ static void pb_add_table_cell_with_flush(PageBuilder *pb, int64_t rowid, const u
|
||||
}
|
||||
|
||||
if (!cell) {
|
||||
pb->ok = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pb_cell_fits(pb, cell_len) && pb->cell_count > 0) {
|
||||
if (!pb_ensure_leaf_cap(pb)) {
|
||||
pb->ok = false;
|
||||
free(cell);
|
||||
return;
|
||||
}
|
||||
@@ -1148,17 +1215,35 @@ static void pb_add_table_cell_with_flush(PageBuilder *pb, int64_t rowid, const u
|
||||
pb->leaves[pb->leaf_count].sep_cell = NULL;
|
||||
pb->leaves[pb->leaf_count].sep_cell_len = 0;
|
||||
pb_flush_leaf(pb);
|
||||
if (!pb->ok) {
|
||||
free(cell);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pb_cell_fits(pb, cell_len)) {
|
||||
pb->ok = false;
|
||||
free(cell);
|
||||
return;
|
||||
}
|
||||
pb_add_cell(pb, cell, cell_len);
|
||||
free(cell);
|
||||
}
|
||||
|
||||
// Finalize a table PageBuilder: flush last leaf and build interior pages.
|
||||
static uint32_t pb_finalize_table(PageBuilder *pb, uint32_t *next_page, int64_t last_rowid) {
|
||||
if (!pb->ok) {
|
||||
pb_free(pb);
|
||||
return 0;
|
||||
}
|
||||
if (pb->cell_count > 0) {
|
||||
pb_ensure_leaf_cap(pb);
|
||||
if (!pb_ensure_leaf_cap(pb)) {
|
||||
pb->ok = false;
|
||||
pb_free(pb);
|
||||
return 0;
|
||||
}
|
||||
if (!pb->leaves) {
|
||||
pb->ok = false;
|
||||
pb_free(pb);
|
||||
return 0;
|
||||
}
|
||||
@@ -1166,6 +1251,10 @@ static uint32_t pb_finalize_table(PageBuilder *pb, uint32_t *next_page, int64_t
|
||||
pb->leaves[pb->leaf_count].sep_cell = NULL;
|
||||
pb->leaves[pb->leaf_count].sep_cell_len = 0;
|
||||
pb_flush_leaf(pb);
|
||||
if (!pb->ok) {
|
||||
pb_free(pb);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
*next_page = pb->next_page;
|
||||
@@ -1199,9 +1288,7 @@ static uint32_t write_table_btree(FILE *fp, uint32_t *next_page, const uint8_t *
|
||||
put_u16(page + hdr + HDR_CELLCOUNT_OFF, 0); // 0 cells
|
||||
put_u16(page + hdr + HDR_CONTENT_OFF, (uint16_t)CBM_PAGE_SIZE); // content at end of page
|
||||
page[hdr + HDR_FRAGBYTES_OFF] = 0; // 0 fragmented bytes
|
||||
(void)fseek(fp, (long)(pnum - SKIP_ONE) * CBM_PAGE_SIZE, SEEK_SET);
|
||||
(void)fwrite(page, SKIP_ONE, CBM_PAGE_SIZE, fp);
|
||||
return pnum;
|
||||
return write_page_at(fp, pnum, page) ? pnum : 0;
|
||||
}
|
||||
|
||||
PageBuilder pb;
|
||||
@@ -1212,6 +1299,10 @@ static uint32_t write_table_btree(FILE *fp, uint32_t *next_page, const uint8_t *
|
||||
for (int i = 0; i < count; i++) {
|
||||
pb_add_table_cell_with_flush(&pb, rowids[i], records[i], record_lens[i],
|
||||
i > 0 ? rowids[i - SKIP_ONE] : 0);
|
||||
if (!pb.ok) {
|
||||
pb_free(&pb);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return pb_finalize_table(&pb, next_page, rowids[count - SKIP_ONE]);
|
||||
@@ -1224,6 +1315,10 @@ static bool pb_promote_and_flush(PageBuilder *pb, uint8_t **cells, int *cell_len
|
||||
}
|
||||
pb->leaves[pb->leaf_count].max_key = 0;
|
||||
pb->leaves[pb->leaf_count].sep_cell = (uint8_t *)malloc(cell_lens[prev_idx]);
|
||||
if (!pb->leaves[pb->leaf_count].sep_cell) {
|
||||
pb->ok = false;
|
||||
return false;
|
||||
}
|
||||
memcpy(pb->leaves[pb->leaf_count].sep_cell, cells[prev_idx], cell_lens[prev_idx]);
|
||||
pb->leaves[pb->leaf_count].sep_cell_len = cell_lens[prev_idx];
|
||||
|
||||
@@ -1249,9 +1344,7 @@ static uint32_t write_empty_index_leaf(FILE *fp, uint32_t *next_page) {
|
||||
put_u16(page + HDR_CELLCOUNT_OFF, 0);
|
||||
put_u16(page + HDR_CONTENT_OFF, (uint16_t)CBM_PAGE_SIZE);
|
||||
page[HDR_FRAGBYTES_OFF] = 0;
|
||||
(void)fseek(fp, (long)(pnum - SKIP_ONE) * CBM_PAGE_SIZE, SEEK_SET);
|
||||
(void)fwrite(page, SKIP_ONE, CBM_PAGE_SIZE, fp);
|
||||
return pnum;
|
||||
return write_page_at(fp, pnum, page) ? pnum : 0;
|
||||
}
|
||||
|
||||
// Write leaf pages for an index, returns root page.
|
||||
@@ -1265,8 +1358,12 @@ static uint32_t write_index_btree(FILE *fp, uint32_t *next_page, uint8_t **cells
|
||||
* every cell added below is within the local-payload limit (see
|
||||
* INDEX_OVERFLOW_MAX_LOCAL). Overflow pages are allocated from *next_page
|
||||
* ahead of the leaf pages, which is fine — page order is arbitrary. */
|
||||
bool overflow_ok = true;
|
||||
for (int i = 0; i < count; i++) {
|
||||
cells[i] = overflowize_index_cell(fp, next_page, cells[i], &cell_lens[i]);
|
||||
cells[i] = overflowize_index_cell(fp, next_page, cells[i], &cell_lens[i], &overflow_ok);
|
||||
if (!overflow_ok) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
PageBuilder pb;
|
||||
@@ -1276,6 +1373,11 @@ static uint32_t write_index_btree(FILE *fp, uint32_t *next_page, uint8_t **cells
|
||||
if (!pb_cell_fits(&pb, cell_lens[i])) {
|
||||
if (pb.cell_count > 0) {
|
||||
if (!pb_promote_and_flush(&pb, cells, cell_lens, i - SKIP_ONE)) {
|
||||
pb_free(&pb);
|
||||
return 0;
|
||||
}
|
||||
if (!pb.ok) {
|
||||
pb_free(&pb);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1288,18 +1390,33 @@ static uint32_t write_index_btree(FILE *fp, uint32_t *next_page, uint8_t **cells
|
||||
}
|
||||
}
|
||||
pb_add_cell(&pb, cells[i], cell_lens[i]);
|
||||
if (!pb.ok) {
|
||||
pb_free(&pb);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (pb.cell_count > 0) {
|
||||
if (!pb_ensure_leaf_cap(&pb)) {
|
||||
pb.ok = false;
|
||||
pb_free(&pb);
|
||||
return 0;
|
||||
}
|
||||
pb.leaves[pb.leaf_count].max_key = 0;
|
||||
int last = count - SKIP_ONE;
|
||||
pb.leaves[pb.leaf_count].sep_cell = (uint8_t *)malloc(cell_lens[last]);
|
||||
if (!pb.leaves[pb.leaf_count].sep_cell) {
|
||||
pb.ok = false;
|
||||
pb_free(&pb);
|
||||
return 0;
|
||||
}
|
||||
memcpy(pb.leaves[pb.leaf_count].sep_cell, cells[last], cell_lens[last]);
|
||||
pb.leaves[pb.leaf_count].sep_cell_len = cell_lens[last];
|
||||
pb_flush_leaf(&pb);
|
||||
if (!pb.ok) {
|
||||
pb_free(&pb);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
*next_page = pb.next_page;
|
||||
@@ -1723,7 +1840,7 @@ static int write_one_table(write_db_ctx_t *w, uint32_t *root, const void *items,
|
||||
build_record_fn build_rec, get_rowid_fn get_id) {
|
||||
if (count <= 0 || !items) {
|
||||
*root = write_table_btree(w->fp, &w->next_page, NULL, NULL, NULL, 0, false);
|
||||
return 0;
|
||||
return *root == 0 ? ERR_WRITE_FAILED : 0;
|
||||
}
|
||||
PageBuilder pb;
|
||||
pb_init(&pb, w->fp, w->next_page, false);
|
||||
@@ -1731,15 +1848,20 @@ static int write_one_table(write_db_ctx_t *w, uint32_t *root, const void *items,
|
||||
int rec_len;
|
||||
uint8_t *rec = build_rec(items, i, &rec_len);
|
||||
if (!rec) {
|
||||
pb_free(&pb);
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
int64_t rowid = get_id(items, i);
|
||||
int64_t prev_id = i > 0 ? get_id(items, i - SKIP_ONE) : 0;
|
||||
pb_add_table_cell_with_flush(&pb, rowid, rec, rec_len, prev_id);
|
||||
free(rec);
|
||||
if (!pb.ok) {
|
||||
pb_free(&pb);
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
}
|
||||
*root = pb_finalize_table(&pb, &w->next_page, get_id(items, count - SKIP_ONE));
|
||||
return 0;
|
||||
return *root == 0 ? ERR_WRITE_FAILED : 0;
|
||||
}
|
||||
|
||||
/* Adapter functions for write_one_table (nodes are written via the streaming
|
||||
@@ -1764,21 +1886,30 @@ static int64_t adapt_token_vec_id(const void *items, int i) {
|
||||
}
|
||||
|
||||
/* Phase 2: Write metadata tables (projects, file_hashes, summaries, sqlite_sequence). */
|
||||
static void write_metadata_tables(write_db_ctx_t *w, uint32_t *projects_root,
|
||||
uint32_t *file_hashes_root, uint32_t *summaries_root,
|
||||
uint32_t *sqlite_seq_root) {
|
||||
static int write_metadata_tables(write_db_ctx_t *w, uint32_t *projects_root,
|
||||
uint32_t *file_hashes_root, uint32_t *summaries_root,
|
||||
uint32_t *sqlite_seq_root) {
|
||||
int proj_rec_len;
|
||||
uint8_t *proj_rec =
|
||||
build_project_record(w->project, w->indexed_at, w->root_path, &proj_rec_len);
|
||||
if (!proj_rec) {
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
const uint8_t *proj_recs[] = {proj_rec};
|
||||
int proj_lens[] = {proj_rec_len};
|
||||
int64_t proj_rowids[] = {FIRST_ROWID};
|
||||
*projects_root =
|
||||
write_table_btree(w->fp, &w->next_page, proj_recs, proj_lens, proj_rowids, SKIP_ONE, false);
|
||||
free(proj_rec);
|
||||
if (*projects_root == 0) {
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
|
||||
*file_hashes_root = write_table_btree(w->fp, &w->next_page, NULL, NULL, NULL, 0, false);
|
||||
*summaries_root = write_table_btree(w->fp, &w->next_page, NULL, NULL, NULL, 0, false);
|
||||
if (*file_hashes_root == 0 || *summaries_root == 0) {
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
|
||||
RecordBuilder r1;
|
||||
RecordBuilder r2;
|
||||
@@ -1788,6 +1919,9 @@ static void write_metadata_tables(write_db_ctx_t *w, uint32_t *projects_root,
|
||||
int seq1_len;
|
||||
uint8_t *seq1 = rec_finalize(&r1, &seq1_len);
|
||||
rec_free(&r1);
|
||||
if (!seq1) {
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
|
||||
rec_init(&r2);
|
||||
rec_add_text(&r2, "edges");
|
||||
@@ -1795,6 +1929,10 @@ static void write_metadata_tables(write_db_ctx_t *w, uint32_t *projects_root,
|
||||
int seq2_len;
|
||||
uint8_t *seq2 = rec_finalize(&r2, &seq2_len);
|
||||
rec_free(&r2);
|
||||
if (!seq2) {
|
||||
free(seq1);
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
|
||||
const uint8_t *seq_recs[] = {seq1, seq2};
|
||||
int seq_lens[] = {seq1_len, seq2_len};
|
||||
@@ -1803,6 +1941,7 @@ static void write_metadata_tables(write_db_ctx_t *w, uint32_t *projects_root,
|
||||
write_table_btree(w->fp, &w->next_page, seq_recs, seq_lens, seq_rowids, PAIR_LEN, false);
|
||||
free(seq1);
|
||||
free(seq2);
|
||||
return *sqlite_seq_root == 0 ? ERR_WRITE_FAILED : 0;
|
||||
}
|
||||
|
||||
/* Write the SQLite file header on page 1 with master entries. */
|
||||
@@ -1834,12 +1973,21 @@ static void write_sqlite_file_header(uint8_t *page1, uint32_t total_pages) {
|
||||
|
||||
/* Build master records, write page 1 B-tree + file header. */
|
||||
static int write_master_page1(FILE *fp, MasterEntry *master, int master_count, uint32_t next_page) {
|
||||
const uint8_t **master_records = (const uint8_t **)malloc(master_count * sizeof(uint8_t *));
|
||||
int *master_lens = (int *)malloc(master_count * sizeof(int));
|
||||
int64_t *master_rowids = (int64_t *)malloc(master_count * sizeof(int64_t));
|
||||
const uint8_t **master_records = (const uint8_t **)calloc((size_t)master_count, sizeof(uint8_t *));
|
||||
int *master_lens = (int *)calloc((size_t)master_count, sizeof(int));
|
||||
int64_t *master_rowids = (int64_t *)calloc((size_t)master_count, sizeof(int64_t));
|
||||
int rc = 0;
|
||||
if (!master_records || !master_lens || !master_rowids) {
|
||||
rc = ERR_WRITE_FAILED;
|
||||
goto cleanup;
|
||||
}
|
||||
for (int i = 0; i < master_count; i++) {
|
||||
master_rowids[i] = i + SKIP_ONE;
|
||||
master_records[i] = build_master_record(&master[i], &master_lens[i]);
|
||||
if (!master_records[i]) {
|
||||
rc = ERR_WRITE_FAILED;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t page1[CBM_PAGE_SIZE];
|
||||
@@ -1857,13 +2005,8 @@ static int write_master_page1(FILE *fp, MasterEntry *master, int master_count, u
|
||||
int available = content_off - ptr_off - CELL_PTR_SIZE;
|
||||
if (!cell || cell_len > available) {
|
||||
free(cell);
|
||||
for (int j = 0; j < master_count; j++) {
|
||||
free((void *)master_records[j]);
|
||||
}
|
||||
free(master_records);
|
||||
free(master_lens);
|
||||
free(master_rowids);
|
||||
return ERR_MASTER_OVERFLOW;
|
||||
rc = ERR_MASTER_OVERFLOW;
|
||||
goto cleanup;
|
||||
}
|
||||
content_off -= cell_len;
|
||||
memcpy(page1 + content_off, cell, cell_len);
|
||||
@@ -1880,28 +2023,40 @@ static int write_master_page1(FILE *fp, MasterEntry *master, int master_count, u
|
||||
|
||||
write_sqlite_file_header(page1, next_page - SKIP_ONE);
|
||||
|
||||
(void)fseek(fp, 0, SEEK_SET);
|
||||
(void)fwrite(page1, SKIP_ONE, CBM_PAGE_SIZE, fp);
|
||||
if (!write_bytes_at(fp, 0, page1, CBM_PAGE_SIZE)) {
|
||||
rc = ERR_WRITE_FAILED;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
for (int i = 0; i < master_count; i++) {
|
||||
free((void *)master_records[i]);
|
||||
if (master_records) {
|
||||
free((void *)master_records[i]);
|
||||
}
|
||||
}
|
||||
free(master_records);
|
||||
free(master_lens);
|
||||
free(master_rowids);
|
||||
return 0;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Pad file to exact page boundary. */
|
||||
static void pad_file_to_page_boundary(FILE *fp, uint32_t next_page) {
|
||||
(void)fseek(fp, 0, SEEK_END);
|
||||
static int pad_file_to_page_boundary(FILE *fp, uint32_t next_page) {
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
long file_size = ftell(fp);
|
||||
if (file_size < 0) {
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
long expected_size = (long)(next_page - SKIP_ONE) * CBM_PAGE_SIZE;
|
||||
if (file_size < expected_size) {
|
||||
uint8_t zero = 0;
|
||||
(void)fseek(fp, expected_size - SKIP_ONE, SEEK_SET);
|
||||
(void)fwrite(&zero, SKIP_ONE, SKIP_ONE, fp);
|
||||
if (!write_bytes_at(fp, expected_size - SKIP_ONE, &zero, SKIP_ONE)) {
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Build all 4 node index B-trees. Returns 0 on success, ERR_SORT_FAILED on failure. */
|
||||
@@ -1916,7 +2071,7 @@ static int build_node_indexes(FILE *fp, uint32_t *next_page, CBMDumpNode *nodes,
|
||||
ncol_file);
|
||||
*qn_root =
|
||||
build_node_index_sorted(fp, next_page, nodes, node_count, nsorts[NSORT_QN].perm, ncol_qn);
|
||||
if (node_count > 0 && (!*label_root || !*name_root || !*file_root || !*qn_root)) {
|
||||
if (!*label_root || !*name_root || !*file_root || !*qn_root) {
|
||||
return ERR_SORT_FAILED;
|
||||
}
|
||||
return 0;
|
||||
@@ -1941,30 +2096,61 @@ static int build_edge_indexes(FILE *fp, uint32_t *next_page, CBMDumpEdge *edges,
|
||||
esorts[ESORT_URL_PATH].perm, ecell_url_path);
|
||||
*auto_root = build_edge_index_sorted(fp, next_page, edges, edge_count,
|
||||
esorts[ESORT_SRC_TGT_TYPE].perm, ecell_src_tgt_type);
|
||||
if (edge_count > 0 && (!*source_root || !*target_root || !*type_root || !*tgt_type_root ||
|
||||
!*src_type_root || !*url_path_root || !*auto_root)) {
|
||||
if (!*source_root || !*target_root || !*type_root || !*tgt_type_root || !*src_type_root ||
|
||||
!*url_path_root || !*auto_root) {
|
||||
return ERR_SORT_FAILED;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Launch parallel sort threads for all index permutations. */
|
||||
static void parallel_sort_indexes(SortJob *nsorts, int n_node, SortJob *esorts, int n_edge) {
|
||||
static int maybe_start_sort_job(SortJob *job, cbm_thread_t *threads, int *thread_count) {
|
||||
if (job->count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (cbm_thread_create(&threads[*thread_count], 0, sort_worker, job) == 0) {
|
||||
(*thread_count)++;
|
||||
return 0;
|
||||
}
|
||||
(void)sort_worker(job);
|
||||
return job->perm ? 0 : ERR_SORT_FAILED;
|
||||
}
|
||||
|
||||
static int parallel_sort_indexes(SortJob *nsorts, int n_node, SortJob *esorts, int n_edge) {
|
||||
cbm_thread_t st[TOTAL_SORT_THREADS];
|
||||
int nt = 0;
|
||||
int rc = 0;
|
||||
for (int i = 0; i < n_node; i++) {
|
||||
if (nsorts[i].count > 0) {
|
||||
cbm_thread_create(&st[nt++], 0, sort_worker, &nsorts[i]);
|
||||
if (maybe_start_sort_job(&nsorts[i], st, &nt) != 0) {
|
||||
rc = ERR_SORT_FAILED;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < n_edge; i++) {
|
||||
if (esorts[i].count > 0) {
|
||||
cbm_thread_create(&st[nt++], 0, sort_worker, &esorts[i]);
|
||||
if (maybe_start_sort_job(&esorts[i], st, &nt) != 0) {
|
||||
rc = ERR_SORT_FAILED;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < nt; i++) {
|
||||
cbm_thread_join(&st[i]);
|
||||
}
|
||||
for (int i = 0; i < n_node; i++) {
|
||||
if (nsorts[i].count > 0 && !nsorts[i].perm) {
|
||||
rc = ERR_SORT_FAILED;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < n_edge; i++) {
|
||||
if (esorts[i].count > 0 && !esorts[i].perm) {
|
||||
rc = ERR_SORT_FAILED;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void free_sort_perms(SortJob *jobs, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(jobs[i].perm);
|
||||
jobs[i].perm = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Write everything after the nodes table: the edges/vectors/token_vectors data
|
||||
@@ -2009,9 +2195,13 @@ static int write_db_after_nodes(write_db_ctx_t *w, uint32_t nodes_root) {
|
||||
uint32_t file_hashes_root;
|
||||
uint32_t summaries_root;
|
||||
uint32_t sqlite_seq_root;
|
||||
write_metadata_tables(w, &projects_root, &file_hashes_root, &summaries_root, &sqlite_seq_root);
|
||||
rc = write_metadata_tables(w, &projects_root, &file_hashes_root, &summaries_root, &sqlite_seq_root);
|
||||
uint32_t next_page = w->next_page;
|
||||
CBM_PROF_END("write_db", "2_metadata_tables", t_meta);
|
||||
if (rc != 0) {
|
||||
(void)fclose(fp);
|
||||
return rc;
|
||||
}
|
||||
|
||||
// --- Build indexes (all sorted by key columns before writing) ---
|
||||
|
||||
@@ -2039,8 +2229,14 @@ static int write_db_after_nodes(write_db_ctx_t *w, uint32_t nodes_root) {
|
||||
};
|
||||
|
||||
CBM_PROF_START(t_sort);
|
||||
parallel_sort_indexes(nsorts, NODE_SORT_THREADS, esorts, EDGE_SORT_THREADS);
|
||||
rc = parallel_sort_indexes(nsorts, NODE_SORT_THREADS, esorts, EDGE_SORT_THREADS);
|
||||
CBM_PROF_END_N("write_db", "3_parallel_sort_indexes", t_sort, node_count + edge_count);
|
||||
if (rc != 0) {
|
||||
free_sort_perms(nsorts, NODE_SORT_THREADS);
|
||||
free_sort_perms(esorts, EDGE_SORT_THREADS);
|
||||
(void)fclose(fp);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Phase 4-5: Build node + edge index B-trees */
|
||||
CBM_PROF_START(t_node_idx);
|
||||
@@ -2052,6 +2248,7 @@ static int write_db_after_nodes(write_db_ctx_t *w, uint32_t nodes_root) {
|
||||
&idx_nodes_name_root, &idx_nodes_file_root, &autoindex_nodes_root);
|
||||
CBM_PROF_END_N("write_db", "4_node_indexes_seq", t_node_idx, node_count * NODE_SORT_THREADS);
|
||||
if (nrc != 0) {
|
||||
free_sort_perms(esorts, EDGE_SORT_THREADS);
|
||||
(void)fclose(fp);
|
||||
return nrc;
|
||||
}
|
||||
@@ -2085,9 +2282,18 @@ static int write_db_after_nodes(write_db_ctx_t *w, uint32_t nodes_root) {
|
||||
int plen = 0;
|
||||
uint8_t *payload = rec_finalize(&r, &plen);
|
||||
rec_free(&r);
|
||||
if (!payload) {
|
||||
(void)fclose(fp);
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
int vl = varint_len(plen);
|
||||
int total = vl + plen;
|
||||
uint8_t *cell = (uint8_t *)malloc(total);
|
||||
if (!cell) {
|
||||
free(payload);
|
||||
(void)fclose(fp);
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
int pos = put_varint(cell, plen);
|
||||
memcpy(cell + pos, payload, plen);
|
||||
free(payload);
|
||||
@@ -2102,6 +2308,11 @@ static int write_db_after_nodes(write_db_ctx_t *w, uint32_t nodes_root) {
|
||||
|
||||
// Autoindex for project_summaries(project TEXT PK) — empty (0 rows)
|
||||
uint32_t autoindex_summaries_root = write_index_btree(fp, &next_page, NULL, NULL, 0);
|
||||
if (autoindex_projects_root == 0 || autoindex_file_hashes_root == 0 ||
|
||||
autoindex_summaries_root == 0) {
|
||||
(void)fclose(fp);
|
||||
return ERR_WRITE_FAILED;
|
||||
}
|
||||
|
||||
// --- sqlite_master table (page 1) ---
|
||||
// This must be written last because it references root pages of all other tables/indexes.
|
||||
@@ -2178,7 +2389,11 @@ static int write_db_after_nodes(write_db_ctx_t *w, uint32_t nodes_root) {
|
||||
(void)fclose(fp);
|
||||
return rc2;
|
||||
}
|
||||
pad_file_to_page_boundary(fp, next_page);
|
||||
rc2 = pad_file_to_page_boundary(fp, next_page);
|
||||
if (rc2 != 0) {
|
||||
(void)fclose(fp);
|
||||
return rc2;
|
||||
}
|
||||
(void)fclose(fp);
|
||||
return 0;
|
||||
}
|
||||
@@ -2228,6 +2443,10 @@ int cbm_writer_append_nodes(cbm_db_writer_t *w, const CBMDumpNode *nodes, int co
|
||||
* the one-shot write_one_table loop — so output is byte-identical. */
|
||||
pb_add_table_cell_with_flush(&w->nodes_pb, nodes[i].id, rec, rec_len, w->last_node_rowid);
|
||||
free(rec);
|
||||
if (!w->nodes_pb.ok) {
|
||||
w->err = ERR_WRITE_FAILED;
|
||||
return w->err;
|
||||
}
|
||||
w->last_node_rowid = nodes[i].id;
|
||||
w->node_rows_written++;
|
||||
}
|
||||
@@ -2243,13 +2462,23 @@ int cbm_writer_finalize(cbm_db_writer_t *w, const char *project, const char *roo
|
||||
}
|
||||
int err = w->err;
|
||||
uint32_t nodes_root = 0;
|
||||
bool nodes_pb_done = false;
|
||||
if (err == 0) {
|
||||
if (w->node_rows_written == 0) {
|
||||
pb_free(&w->nodes_pb);
|
||||
nodes_pb_done = true;
|
||||
nodes_root = write_table_btree(w->wc.fp, &w->wc.next_page, NULL, NULL, NULL, 0, false);
|
||||
} else {
|
||||
nodes_root = pb_finalize_table(&w->nodes_pb, &w->wc.next_page, w->last_node_rowid);
|
||||
nodes_pb_done = true;
|
||||
}
|
||||
if (nodes_root == 0) {
|
||||
err = ERR_WRITE_FAILED;
|
||||
}
|
||||
}
|
||||
if (err != 0 && !nodes_pb_done) {
|
||||
pb_free(&w->nodes_pb);
|
||||
nodes_pb_done = true;
|
||||
}
|
||||
w->wc.project = project;
|
||||
w->wc.root_path = root_path;
|
||||
|
||||
+143
-20
@@ -8,6 +8,7 @@
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/platform.h"
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/str_util.h"
|
||||
|
||||
/* CLI buffer size constants. */
|
||||
enum {
|
||||
@@ -409,7 +410,10 @@ int cbm_replace_binary(const char *path, const unsigned char *data, int len, int
|
||||
#ifdef _WIN32
|
||||
/* Windows: can't unlink running .exe — rename aside */
|
||||
char old_path[CLI_BUF_1K];
|
||||
snprintf(old_path, sizeof(old_path), "%s.old", path);
|
||||
int old_len = snprintf(old_path, sizeof(old_path), "%s.old", path);
|
||||
if (old_len < 0 || (size_t)old_len >= sizeof(old_path)) {
|
||||
return CLI_ERR;
|
||||
}
|
||||
(void)cbm_unlink(old_path);
|
||||
if (rename(path, old_path) != 0) {
|
||||
return CLI_ERR;
|
||||
@@ -2454,11 +2458,23 @@ int cbm_remove_indexes(const char *home_dir) {
|
||||
size_t len = strlen(ent->name);
|
||||
if (len > DB_EXT_LEN && strcmp(ent->name + len - DB_EXT_LEN, ".db") == 0) {
|
||||
char path[CLI_BUF_1K];
|
||||
snprintf(path, sizeof(path), "%s/%s", cache_dir, ent->name);
|
||||
int path_len = snprintf(path, sizeof(path), "%s/%s", cache_dir, ent->name);
|
||||
if (path_len < 0 || (size_t)path_len >= sizeof(path)) {
|
||||
(void)fprintf(stderr,
|
||||
"warning: skipping index cleanup entry with overlong path: %s/%s\n",
|
||||
cache_dir, ent->name);
|
||||
continue;
|
||||
}
|
||||
/* Also remove .db.tmp if present */
|
||||
char tmp_path[CLI_FIELD_1040];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);
|
||||
cbm_unlink(tmp_path);
|
||||
int tmp_len = snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);
|
||||
if (tmp_len >= 0 && (size_t)tmp_len < sizeof(tmp_path)) {
|
||||
cbm_unlink(tmp_path);
|
||||
} else {
|
||||
(void)fprintf(stderr,
|
||||
"warning: skipping overlong temporary sidecar cleanup for %s\n",
|
||||
path);
|
||||
}
|
||||
if (cbm_unlink(path) == 0) {
|
||||
count++;
|
||||
}
|
||||
@@ -2673,6 +2689,12 @@ const cbm_config_entry_t CBM_CONFIG_REGISTRY[] = {
|
||||
"The architecture resource ranks every symbol by PageRank importance and returns the top N. "
|
||||
"Use 25 for most projects. Raise to 50-100 for large multi-language codebases where "
|
||||
"important functions may not appear in the first 25. Lower to 10 when tokens are limited."},
|
||||
{"context_key_functions_limit", "10", NULL, "Search",
|
||||
"Max key functions PUSHED in the first-response _context header (0 = use built-in 10)",
|
||||
"0-100",
|
||||
"Separate from key_functions_count (the architecture query bound) — this governs only the "
|
||||
"auto-pushed summary that closes the codebase://architecture pull-only gap. Kept small (10) "
|
||||
"to keep first-response token cost modest; raise to 20-25 if you want richer upfront context."},
|
||||
/* ── Tools ── */
|
||||
{"tool_mode", "streamlined", "CBM_TOOL_MODE", "Tools",
|
||||
"Which set of tools the MCP server exposes: 5 default tools or all 15 individual tools",
|
||||
@@ -2719,6 +2741,18 @@ const cbm_config_entry_t CBM_CONFIG_REGISTRY[] = {
|
||||
"20 iterations converges in ~5ms for 16K-node codebases. Typical convergence is 10-15 iters. "
|
||||
"Raise to 50-100 for very large codebases (>100K nodes). "
|
||||
"Diminishing returns above convergence — set too high wastes CPU at reindex time."},
|
||||
{"pagerank_damping", "0.85", NULL, "PageRank",
|
||||
"Damping factor — fraction of importance that follows edges vs. teleports randomly (the 'bored surfer')",
|
||||
"0.0-1.0",
|
||||
"0.85 is the standard Google PageRank value. Higher (0.9) spreads importance further along long "
|
||||
"call chains; lower (0.7-0.8) keeps importance local to direct callers. Out-of-range and NaN "
|
||||
"values are clamped to 0.85 at compute time, so an invalid value never crashes indexing."},
|
||||
{"pagerank_epsilon", "0.000001", NULL, "PageRank",
|
||||
"Convergence threshold — PageRank stops early when the L2 change between iterations drops below this",
|
||||
"0.0-1.0",
|
||||
"1e-6 default. Lower (1e-8) iterates longer for marginally finer convergence (rarely needed); "
|
||||
"higher (1e-4) stops sooner with slightly less precise rankings. Must be > 0 — non-positive and "
|
||||
"NaN values are clamped to 1e-6. Rarely needs tuning; pair with pagerank_max_iter as the hard cap."},
|
||||
{"rank_scope", "project", NULL, "PageRank",
|
||||
"Whether PageRank importance is computed per-project or across all indexed projects",
|
||||
"project|full",
|
||||
@@ -2811,6 +2845,21 @@ const cbm_config_entry_t CBM_CONFIG_REGISTRY[] = {
|
||||
"100-3600000",
|
||||
"60 seconds for repos with 50K+ files. Lower to 10000 for faster detection in large repos "
|
||||
"if CPU allows. Formula: min(base + file_count/500 * 1000, max)."},
|
||||
{"store_idle_timeout_s", "60", NULL, "MCP",
|
||||
"Seconds an MCP server keeps an idle SQLite project store open",
|
||||
"1-65536",
|
||||
"60 seconds balances latency for repeated tool calls with memory release while idle. Lower for "
|
||||
"memory-constrained hosts; raise for long interactive sessions that repeatedly query one project."},
|
||||
{"db_validate_busy_timeout_ms", "1000", NULL, "MCP",
|
||||
"SQLite busy timeout for read-only cache database validation in MCP startup/discovery paths",
|
||||
"0-65536",
|
||||
"1 second avoids hanging JSON-RPC startup on locked databases. Raise on slow network filesystems; "
|
||||
"set 0 to fail immediately."},
|
||||
{"update_check_timeout_s", "5", NULL, "MCP",
|
||||
"Curl timeout for the optional MCP background latest-release check (0=disabled)",
|
||||
"0-256",
|
||||
"Default preserves the historical 5-second bound. Set 0 for offline, hermetic, or privacy-sensitive "
|
||||
"MCP deployments where the server must not make background network requests."},
|
||||
/* ── Architecture ── */
|
||||
{"arch_hotspot_limit", "25", NULL, "Architecture",
|
||||
"Max hotspot functions shown in the classic get_architecture tool's hotspots section",
|
||||
@@ -2819,6 +2868,40 @@ const cbm_config_entry_t CBM_CONFIG_REGISTRY[] = {
|
||||
"They identify the most-invoked code — good candidates for optimization and risk assessment. "
|
||||
"25 is enough for orientation; raise to 100 for exhaustive call-density analysis. "
|
||||
"Only applies to the classic 'get_architecture' tool (tool_mode=classic)."},
|
||||
{"architecture_resolution", "1.0", NULL, "Architecture",
|
||||
"Leiden community-detection resolution for architecture clusters",
|
||||
"0.0001-10.0",
|
||||
"1.0 is the standard Leiden default. Higher (2.0-5.0) splits code into more, finer-grained "
|
||||
"clusters; lower (0.3-0.5) merges related clusters into coarse subsystems. Non-positive and "
|
||||
"NaN values are clamped to 1.0. Drives the 'clusters' section of get_architecture."},
|
||||
/* ── Similarity ── */
|
||||
{"similarity_threshold", "0.0", NULL, "Similarity",
|
||||
"MinHash Jaccard threshold for semantic SIMILAR edges (0.0 = use the built-in 0.95 default)",
|
||||
"0.0-1.0",
|
||||
"Two symbols get a SIMILAR edge when their estimated Jaccard similarity is at least this. "
|
||||
"0.0 (default) uses the built-in 0.95, so only near-duplicates are linked. Lower to 0.7-0.8 to "
|
||||
"surface more semantic duplicates (refactoring candidates); too low adds noisy edges that inflate "
|
||||
"PageRank rankings. Effective only when indexing creates similarity edges (full/moderate modes)."},
|
||||
{"semantic_threshold", "0.0", NULL, "Similarity",
|
||||
"Combined semantic score threshold for SEMANTICALLY_RELATED edges (0.0 = built-in 0.75)",
|
||||
"0.0-1.0",
|
||||
"Controls the algorithmic semantic-edge pass. Higher values improve precision and reduce edge count; "
|
||||
"lower values increase recall and runtime output volume. 0.0 preserves the upstream-compatible default."},
|
||||
{"httplink_min_confidence", "0.0", NULL, "Similarity",
|
||||
"Minimum confidence for HTTP route-to-call linking (0.0 = built-in 0.25)",
|
||||
"0.0-1.0",
|
||||
"Raises or lowers the fork-only HTTP linker's match threshold. Higher values reduce speculative "
|
||||
"cross-service HTTP_CALLS edges; 0.0 keeps existing behavior."},
|
||||
{"githistory_min_coupling", "0.0", NULL, "Similarity",
|
||||
"Minimum file co-change coupling score for FILE_CHANGES_WITH edges (0.0 = built-in 0.3)",
|
||||
"0.0-1.0",
|
||||
"Controls how strongly two files must co-change before git-history coupling emits an edge. "
|
||||
"Higher values reduce noisy historical edges; 0.0 keeps existing behavior."},
|
||||
{"lsp_confidence_floor", "0.0", NULL, "Similarity",
|
||||
"Minimum LSP-resolved call confidence accepted by call resolution (0.0 = built-in 0.6)",
|
||||
"0.0-1.0",
|
||||
"Applies consistently to sequential and parallel call resolution. Raise to prefer registry matches "
|
||||
"over uncertain LSP hints; lower only when language-specific LSP coverage is known to be precise."},
|
||||
/* ── Degree / Sort ── */
|
||||
{"degree_mode", "weighted", NULL, "Degree",
|
||||
"What 'degree' means for min_degree/max_degree filters and sort_by=degree ranking",
|
||||
@@ -3061,15 +3144,19 @@ static bool prompt_yn(const char *question) {
|
||||
/* Compute SHA-CBM_SZ_256 of a file using platform tools (sha256sum/shasum).
|
||||
* Writes CBM_SZ_64-char hex digest + NUL to out. Returns 0 on success. */
|
||||
static int sha256_file(const char *path, char *out, size_t out_size) {
|
||||
if (out_size < SHA256_BUF_SIZE) {
|
||||
if (!path || !cbm_validate_shell_arg(path) || out_size < SHA256_BUF_SIZE) {
|
||||
return CLI_ERR;
|
||||
}
|
||||
char cmd[CLI_BUF_1K];
|
||||
int cmd_len;
|
||||
#ifdef __APPLE__
|
||||
snprintf(cmd, sizeof(cmd), "shasum -a CBM_SZ_256 '%s' 2>/dev/null", path);
|
||||
cmd_len = snprintf(cmd, sizeof(cmd), "shasum -a CBM_SZ_256 '%s' 2>/dev/null", path);
|
||||
#else
|
||||
snprintf(cmd, sizeof(cmd), "sha256sum '%s' 2>/dev/null", path);
|
||||
cmd_len = snprintf(cmd, sizeof(cmd), "sha256sum '%s' 2>/dev/null", path);
|
||||
#endif
|
||||
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) {
|
||||
return CLI_ERR;
|
||||
}
|
||||
FILE *fp = cbm_popen(cmd, "r");
|
||||
if (!fp) {
|
||||
return CLI_ERR;
|
||||
@@ -3101,6 +3188,24 @@ static int cbm_download_to_file_quiet(const char *url, const char *dest) {
|
||||
return cbm_exec_no_shell(argv);
|
||||
}
|
||||
|
||||
static int cbm_cli_make_temp_file(char *out, size_t out_sz, const char *prefix) {
|
||||
if (!out || out_sz == 0 || !prefix || !prefix[0]) {
|
||||
errno = EINVAL;
|
||||
return CLI_ERR;
|
||||
}
|
||||
int n = snprintf(out, out_sz, "%s/%s_XXXXXX", cbm_tmpdir(), prefix);
|
||||
if (n < 0 || (size_t)n >= out_sz) {
|
||||
errno = ENAMETOOLONG;
|
||||
return CLI_ERR;
|
||||
}
|
||||
int fd = cbm_mkstemp_s(out, out_sz);
|
||||
if (fd < 0) {
|
||||
return CLI_ERR;
|
||||
}
|
||||
cbm_close_fd(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── macOS ad-hoc signing ─────────────────────────────────────── */
|
||||
|
||||
#ifdef __APPLE__
|
||||
@@ -3151,19 +3256,29 @@ static int cbm_kill_other_instances(void) {
|
||||
/* Download checksums.txt and verify the archive integrity.
|
||||
* Returns: 0 = verified OK, 1 = mismatch (FAIL), -1 = could not verify (warning). */
|
||||
static int verify_download_checksum(const char *archive_path, const char *archive_name) {
|
||||
char checksum_file[CLI_BUF_256];
|
||||
snprintf(checksum_file, sizeof(checksum_file), "%s/cbm-checksums.txt", cbm_tmpdir());
|
||||
char checksum_file[CBM_PATH_MAX];
|
||||
if (cbm_cli_make_temp_file(checksum_file, sizeof(checksum_file), "cbm-checksums") != 0) {
|
||||
(void)fprintf(stderr,
|
||||
"warning: could not create temporary checksum file — skipping verification\n");
|
||||
return CLI_ERR;
|
||||
}
|
||||
|
||||
char dl_base_buf[CLI_BUF_512];
|
||||
const char *dl_base =
|
||||
cbm_safe_getenv("CBM_DOWNLOAD_URL", dl_base_buf, sizeof(dl_base_buf), NULL);
|
||||
char checksum_url[CLI_BUF_512];
|
||||
int url_len;
|
||||
if (dl_base && dl_base[0]) {
|
||||
snprintf(checksum_url, sizeof(checksum_url), "%s/checksums.txt", dl_base);
|
||||
url_len = snprintf(checksum_url, sizeof(checksum_url), "%s/checksums.txt", dl_base);
|
||||
} else {
|
||||
snprintf(checksum_url, sizeof(checksum_url), "%s",
|
||||
"https://github.com/DeusData/codebase-memory-mcp/releases/latest/download/"
|
||||
"checksums.txt");
|
||||
url_len = snprintf(checksum_url, sizeof(checksum_url), "%s",
|
||||
"https://github.com/DeusData/codebase-memory-mcp/releases/latest/download/"
|
||||
"checksums.txt");
|
||||
}
|
||||
if (url_len < 0 || (size_t)url_len >= sizeof(checksum_url)) {
|
||||
(void)fprintf(stderr, "warning: checksum URL too long — skipping verification\n");
|
||||
cbm_unlink(checksum_file);
|
||||
return CLI_ERR;
|
||||
}
|
||||
int rc = cbm_download_to_file_quiet(checksum_url, checksum_file);
|
||||
if (rc != 0) {
|
||||
@@ -3174,8 +3289,8 @@ static int verify_download_checksum(const char *archive_path, const char *archiv
|
||||
}
|
||||
|
||||
FILE *fp = fopen(checksum_file, "r");
|
||||
cbm_unlink(checksum_file);
|
||||
if (!fp) {
|
||||
cbm_unlink(checksum_file);
|
||||
return CLI_ERR;
|
||||
}
|
||||
|
||||
@@ -3190,6 +3305,7 @@ static int verify_download_checksum(const char *archive_path, const char *archiv
|
||||
}
|
||||
}
|
||||
(void)fclose(fp);
|
||||
cbm_unlink(checksum_file);
|
||||
|
||||
if (expected[0] == '\0') {
|
||||
(void)fprintf(stderr, "warning: %s not found in checksums.txt\n", archive_name);
|
||||
@@ -4158,7 +4274,7 @@ static int extract_and_install_binary(extract_install_args_t args) {
|
||||
}
|
||||
|
||||
/* Build the download URL for the update command. */
|
||||
static void build_update_url(char *url, int url_sz, const char *os, const char *arch,
|
||||
static int build_update_url(char *url, int url_sz, const char *os, const char *arch,
|
||||
const char *ext, bool want_ui) {
|
||||
char base_url_buf[CLI_BUF_512];
|
||||
const char *base_url =
|
||||
@@ -4171,8 +4287,9 @@ static void build_update_url(char *url, int url_sz, const char *os, const char *
|
||||
* have no such variant. Keep in sync with install.sh / install.js / pypi
|
||||
* _cli.py. */
|
||||
const char *portable = (strcmp(os, "linux") == 0) ? "-portable" : "";
|
||||
snprintf(url, url_sz, "%s/codebase-memory-mcp-%s%s-%s%s.%s", base_url, want_ui ? "ui-" : "", os,
|
||||
arch, portable, ext);
|
||||
int n = snprintf(url, (size_t)url_sz, "%s/codebase-memory-mcp-%s%s-%s%s.%s", base_url,
|
||||
want_ui ? "ui-" : "", os, arch, portable, ext);
|
||||
return (n >= 0 && n < url_sz) ? 0 : CLI_ERR;
|
||||
}
|
||||
|
||||
/* Prompt to delete existing indexes. Returns 0 to continue, 1 to abort. */
|
||||
@@ -4200,8 +4317,11 @@ static int update_clear_indexes(const char *home, bool dry_run) {
|
||||
/* Download, verify checksum, kill old instances, and install binary. Returns 0 on success. */
|
||||
static int download_verify_install(const char *url, const char *ext, const char *os,
|
||||
const char *arch, bool want_ui, const char *bin_dest) {
|
||||
char tmp_archive[CLI_BUF_256];
|
||||
snprintf(tmp_archive, sizeof(tmp_archive), "%s/cbm-update.%s", cbm_tmpdir(), ext);
|
||||
char tmp_archive[CBM_PATH_MAX];
|
||||
if (cbm_cli_make_temp_file(tmp_archive, sizeof(tmp_archive), "cbm-update") != 0) {
|
||||
(void)fprintf(stderr, "error: cannot create temporary update archive path\n");
|
||||
return CLI_TRUE;
|
||||
}
|
||||
|
||||
int rc = cbm_download_to_file(url, tmp_archive);
|
||||
if (rc != 0) {
|
||||
@@ -4384,7 +4504,10 @@ int cbm_cmd_update(int argc, char **argv) {
|
||||
const char *ext = strcmp(os, "windows") == 0 ? "zip" : "tar.gz";
|
||||
|
||||
char url[CLI_BUF_512];
|
||||
build_update_url(url, sizeof(url), os, arch, ext, want_ui);
|
||||
if (build_update_url(url, sizeof(url), os, arch, ext, want_ui) != 0) {
|
||||
(void)fprintf(stderr, "error: update download URL is too long\n");
|
||||
return CLI_TRUE;
|
||||
}
|
||||
|
||||
if (dry_run) {
|
||||
printf("\nWould download %s binary for %s/%s ...\n", variant_label, os, arch);
|
||||
|
||||
+87
-21
@@ -7,6 +7,7 @@
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/constants.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
@@ -54,20 +55,55 @@ char *cbm_strcasestr(const char *haystack, const char *needle) {
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
char *cbm_mkdtemp(char *tmpl) {
|
||||
/* Build path in static buffer, then copy back to caller.
|
||||
* Callers must provide buffers >= CBM_SZ_256 bytes (all test code does). */
|
||||
static char buf[CBM_SZ_512];
|
||||
if (strncmp(tmpl, "/tmp/", 5) == 0) {
|
||||
|
||||
static int rewrite_tmp_template(char *tmpl, size_t tmpl_sz) {
|
||||
if (!tmpl || tmpl_sz == 0) {
|
||||
errno = EINVAL;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
while (len < tmpl_sz && tmpl[len]) {
|
||||
len++;
|
||||
}
|
||||
if (len == tmpl_sz || len >= CBM_PATH_MAX) {
|
||||
errno = ENAMETOOLONG;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
char original[CBM_PATH_MAX];
|
||||
memcpy(original, tmpl, len + SKIP_ONE);
|
||||
|
||||
enum { TMP_PREFIX_LEN = sizeof("/tmp/") - SKIP_ONE };
|
||||
int n;
|
||||
if (strncmp(original, "/tmp/", TMP_PREFIX_LEN) == 0) {
|
||||
const char *tmp = getenv("TEMP");
|
||||
if (!tmp)
|
||||
tmp = getenv("TMP");
|
||||
if (!tmp)
|
||||
tmp = ".";
|
||||
snprintf(buf, sizeof(buf), "%s\\%s", tmp, tmpl + 5);
|
||||
n = snprintf(tmpl, tmpl_sz, "%s\\%s", tmp, original + TMP_PREFIX_LEN);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%s", tmpl);
|
||||
n = snprintf(tmpl, tmpl_sz, "%s", original);
|
||||
}
|
||||
if (n < 0 || (size_t)n >= tmpl_sz) {
|
||||
errno = ENAMETOOLONG;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *cbm_mkdtemp(char *tmpl) {
|
||||
/* Build path in thread-local storage, then copy back to caller.
|
||||
* Callers must provide buffers >= CBM_SZ_256 bytes (all test code does). */
|
||||
static CBM_TLS char buf[CBM_PATH_MAX];
|
||||
int n = snprintf(buf, sizeof(buf), "%s", tmpl ? tmpl : "");
|
||||
if (n < 0 || (size_t)n >= sizeof(buf)) {
|
||||
errno = ENAMETOOLONG;
|
||||
return NULL;
|
||||
}
|
||||
if (rewrite_tmp_template(buf, sizeof(buf)) != 0)
|
||||
return NULL;
|
||||
if (!_mktemp(buf))
|
||||
return NULL;
|
||||
if (_mkdir(buf) != 0)
|
||||
@@ -90,25 +126,55 @@ char *cbm_mkdtemp(char *tmpl) {
|
||||
|
||||
#ifdef _WIN32
|
||||
int cbm_mkstemp(char *tmpl) {
|
||||
/* Rewrite /tmp/ to %TEMP%\ like cbm_mkdtemp */
|
||||
static char buf[CBM_SZ_512];
|
||||
if (strncmp(tmpl, "/tmp/", 5) == 0) {
|
||||
const char *tmp = getenv("TEMP");
|
||||
if (!tmp)
|
||||
tmp = getenv("TMP");
|
||||
if (!tmp)
|
||||
tmp = ".";
|
||||
snprintf(buf, sizeof(buf), "%s\\%s", tmp, tmpl + 5);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%s", tmpl);
|
||||
}
|
||||
if (!_mktemp(buf))
|
||||
/* Legacy ABI: caller owns an unsized template buffer. New shared code should
|
||||
* use cbm_mkstemp_s() so long paths fail instead of copying back blindly. */
|
||||
static CBM_TLS char buf[CBM_PATH_MAX];
|
||||
int n = snprintf(buf, sizeof(buf), "%s", tmpl ? tmpl : "");
|
||||
if (n < 0 || (size_t)n >= sizeof(buf)) {
|
||||
errno = ENAMETOOLONG;
|
||||
return CBM_NOT_FOUND;
|
||||
int fd = _open(buf, _O_CREAT | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
|
||||
}
|
||||
int fd = cbm_mkstemp_s(buf, sizeof(buf));
|
||||
if (fd >= 0)
|
||||
strcpy(tmpl, buf);
|
||||
return fd;
|
||||
}
|
||||
|
||||
int cbm_mkstemp_s(char *tmpl, size_t tmpl_sz) {
|
||||
if (rewrite_tmp_template(tmpl, tmpl_sz) != 0)
|
||||
return CBM_NOT_FOUND;
|
||||
|
||||
char *pattern = cbm_strdup(tmpl);
|
||||
if (!pattern) {
|
||||
errno = ENOMEM;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
enum { MKSTEMP_COLLISION_RETRIES = CBM_SZ_32 };
|
||||
for (int attempt = 0; attempt < MKSTEMP_COLLISION_RETRIES; attempt++) {
|
||||
int n = snprintf(tmpl, tmpl_sz, "%s", pattern);
|
||||
if (n < 0 || (size_t)n >= tmpl_sz) {
|
||||
free(pattern);
|
||||
errno = ENAMETOOLONG;
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
if (!_mktemp(tmpl)) {
|
||||
free(pattern);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
int fd = _open(tmpl, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
|
||||
if (fd >= 0) {
|
||||
free(pattern);
|
||||
return fd;
|
||||
}
|
||||
if (errno != EEXIST) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(pattern);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── clock_gettime (Windows lacks it) ─────────────────────────── */
|
||||
|
||||
@@ -48,9 +48,12 @@ ssize_t cbm_getline(char **lineptr, size_t *n, FILE *stream);
|
||||
|
||||
/* ── fileno ───────────────────────────────────────────────────── */
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#define cbm_fileno _fileno
|
||||
#define cbm_close_fd _close
|
||||
#else
|
||||
#define cbm_fileno fileno
|
||||
#define cbm_close_fd close
|
||||
#endif
|
||||
|
||||
/* ── strcasestr (Windows lacks it) ────────────────────────────── */
|
||||
@@ -109,8 +112,13 @@ char *cbm_mkdtemp(char *tmpl);
|
||||
/* ── mkstemp (Windows lacks it) ──────────────────────────────── */
|
||||
#ifdef _WIN32
|
||||
int cbm_mkstemp(char *tmpl);
|
||||
int cbm_mkstemp_s(char *tmpl, size_t tmpl_sz);
|
||||
#else
|
||||
#define cbm_mkstemp mkstemp
|
||||
static inline int cbm_mkstemp_s(char *tmpl, size_t tmpl_sz) {
|
||||
(void)tmpl_sz;
|
||||
return mkstemp(tmpl);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── setenv / unsetenv (Windows lacks them) ──────────────────── */
|
||||
@@ -129,7 +137,6 @@ static inline int cbm_unsetenv(const char *name) {
|
||||
|
||||
/* ── pipe (Windows uses _pipe) ───────────────────────────────── */
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#define cbm_pipe(fds) _pipe(fds, 4096, _O_BINARY)
|
||||
#else
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
* Windows: FindFirstFile/FindNextFile, _popen/_pclose, _mkdir, _unlink.
|
||||
*/
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/compat_fs.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -181,6 +183,40 @@ int cbm_rmdir(const char *path) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
int cbm_replace_file_ex(const char *tmp_path, const char *dest_path, int *platform_error) {
|
||||
if (platform_error) {
|
||||
*platform_error = 0;
|
||||
}
|
||||
if (!tmp_path || !dest_path) {
|
||||
if (platform_error) {
|
||||
*platform_error = ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
wchar_t *wtmp = cbm_utf8_to_wide(tmp_path);
|
||||
wchar_t *wdest = cbm_utf8_to_wide(dest_path);
|
||||
if (!wtmp || !wdest) {
|
||||
if (platform_error) {
|
||||
*platform_error = ERROR_NOT_ENOUGH_MEMORY;
|
||||
}
|
||||
free(wtmp);
|
||||
free(wdest);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
BOOL ok = MoveFileExW(wtmp, wdest, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
|
||||
DWORD err = ok ? 0 : GetLastError();
|
||||
free(wtmp);
|
||||
free(wdest);
|
||||
if (!ok && platform_error) {
|
||||
*platform_error = (int)err;
|
||||
}
|
||||
return ok ? 0 : CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
int cbm_replace_file(const char *tmp_path, const char *dest_path) {
|
||||
return cbm_replace_file_ex(tmp_path, dest_path, NULL);
|
||||
}
|
||||
|
||||
int cbm_exec_no_shell(const char *const *argv) {
|
||||
if (!argv || !argv[0]) {
|
||||
return CBM_NOT_FOUND;
|
||||
@@ -293,6 +329,29 @@ int cbm_rmdir(const char *path) {
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
int cbm_replace_file_ex(const char *tmp_path, const char *dest_path, int *platform_error) {
|
||||
if (platform_error) {
|
||||
*platform_error = 0;
|
||||
}
|
||||
if (!tmp_path || !dest_path) {
|
||||
if (platform_error) {
|
||||
*platform_error = EINVAL;
|
||||
}
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
if (rename(tmp_path, dest_path) != 0) {
|
||||
if (platform_error) {
|
||||
*platform_error = errno;
|
||||
}
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cbm_replace_file(const char *tmp_path, const char *dest_path) {
|
||||
return cbm_replace_file_ex(tmp_path, dest_path, NULL);
|
||||
}
|
||||
|
||||
int cbm_exec_no_shell(const char *const *argv) {
|
||||
if (!argv || !argv[0]) {
|
||||
return CBM_NOT_FOUND;
|
||||
@@ -331,3 +390,68 @@ int cbm_exec_no_shell(const char *const *argv) {
|
||||
}
|
||||
|
||||
#endif /* _WIN32 */
|
||||
|
||||
static void set_file_error(cbm_file_error_t *out, const char *stage, int code) {
|
||||
if (out) {
|
||||
out->stage = stage;
|
||||
out->code = code;
|
||||
}
|
||||
}
|
||||
|
||||
int cbm_write_file_atomic(const char *dest_path, const void *data, size_t len,
|
||||
cbm_file_error_t *out_err) {
|
||||
set_file_error(out_err, NULL, 0);
|
||||
if (!dest_path || (!data && len > 0)) {
|
||||
set_file_error(out_err, "invalid_argument", EINVAL);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
char tmp_path[CBM_PATH_MAX];
|
||||
int n = snprintf(tmp_path, sizeof(tmp_path), "%s.tmp.XXXXXX", dest_path);
|
||||
if (n < 0 || (size_t)n >= sizeof(tmp_path)) {
|
||||
set_file_error(out_err, "path_too_long", 0);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
int fd = cbm_mkstemp_s(tmp_path, sizeof(tmp_path));
|
||||
if (fd < 0) {
|
||||
set_file_error(out_err, "open_temp", errno);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
FILE *fp = _fdopen(fd, "wb");
|
||||
#else
|
||||
FILE *fp = fdopen(fd, "wb");
|
||||
#endif
|
||||
if (!fp) {
|
||||
int code = errno;
|
||||
cbm_close_fd(fd);
|
||||
cbm_unlink(tmp_path);
|
||||
set_file_error(out_err, "open_temp", code);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (len > 0 && fwrite(data, 1, len, fp) != len) {
|
||||
int code = ferror(fp) ? errno : 0;
|
||||
(void)fclose(fp);
|
||||
cbm_unlink(tmp_path);
|
||||
set_file_error(out_err, "write_temp", code);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (fclose(fp) != 0) {
|
||||
int code = errno;
|
||||
cbm_unlink(tmp_path);
|
||||
set_file_error(out_err, "close_temp", code);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
int replace_error = 0;
|
||||
if (cbm_replace_file_ex(tmp_path, dest_path, &replace_error) != 0) {
|
||||
cbm_unlink(tmp_path);
|
||||
set_file_error(out_err, "rename_temp", replace_error);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,25 @@ int cbm_unlink(const char *path);
|
||||
/* Delete an empty directory. Returns 0 on success. */
|
||||
int cbm_rmdir(const char *path);
|
||||
|
||||
/* Atomically replace dest_path with tmp_path when the platform supports it.
|
||||
* tmp_path must already contain the complete new file. Returns 0 on success.
|
||||
* POSIX: rename(). Windows: MoveFileExW(REPLACE_EXISTING | WRITE_THROUGH). */
|
||||
int cbm_replace_file(const char *tmp_path, const char *dest_path);
|
||||
|
||||
/* Same as cbm_replace_file(), but returns the platform-native failure code via
|
||||
* platform_error: errno on POSIX, GetLastError() on Windows. */
|
||||
int cbm_replace_file_ex(const char *tmp_path, const char *dest_path, int *platform_error);
|
||||
|
||||
typedef struct {
|
||||
const char *stage; /* path_too_long, open_temp, write_temp, close_temp, rename_temp */
|
||||
int code; /* errno/GetLastError() when available, or 0 for validation errors */
|
||||
} cbm_file_error_t;
|
||||
|
||||
/* Write data to a unique temp sibling, close it, then atomically replace dest_path.
|
||||
* Binary-safe. Returns 0 on success and fills out_err on failure when provided. */
|
||||
int cbm_write_file_atomic(const char *dest_path, const void *data, size_t len,
|
||||
cbm_file_error_t *out_err);
|
||||
|
||||
/* Execute a command without shell interpretation.
|
||||
* argv is a NULL-terminated array: {"cmd", "arg1", "arg2", NULL}.
|
||||
* Returns the process exit code, or -1 on fork/exec failure.
|
||||
|
||||
@@ -7,13 +7,16 @@
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/compat_thread.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
/* Default 8MB stack for all threads. macOS ARM64 default is only 512KB,
|
||||
* which is too small for deep pipeline passes (configlink, etc.). */
|
||||
#define CBM_DEFAULT_STACK_SIZE ((size_t)8 * CBM_SZ_1K * CBM_SZ_1K)
|
||||
#include <string.h>
|
||||
|
||||
/* ── Thread ───────────────────────────────────────────────────── */
|
||||
|
||||
|
||||
@@ -117,40 +117,29 @@ static void write_diagnostics(void) {
|
||||
long long qmax = atomic_load(&g_query_stats.max_us);
|
||||
long long qavg = qcount > 0 ? qtime / qcount : 0;
|
||||
|
||||
/* Write to .tmp then rename (atomic) */
|
||||
char tmp_path[sizeof(g_diag_path) + DIAG_PATH_EXTRA];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", g_diag_path);
|
||||
|
||||
FILE *f = fopen(tmp_path, "w");
|
||||
if (!f) {
|
||||
char json[CBM_SZ_2K];
|
||||
int n = snprintf(json, sizeof(json),
|
||||
"{\n"
|
||||
" \"uptime_s\": %ld,\n"
|
||||
" \"rss_bytes\": %zu,\n"
|
||||
" \"peak_rss_bytes\": %zu,\n"
|
||||
" \"heap_committed_bytes\": %zu,\n"
|
||||
" \"peak_committed_bytes\": %zu,\n"
|
||||
" \"page_faults\": %zu,\n"
|
||||
" \"fd_count\": %d,\n"
|
||||
" \"query_count\": %d,\n"
|
||||
" \"query_errors\": %d,\n"
|
||||
" \"query_total_us\": %lld,\n"
|
||||
" \"query_avg_us\": %lld,\n"
|
||||
" \"query_max_us\": %lld,\n"
|
||||
" \"pid\": %d\n"
|
||||
"}\n",
|
||||
uptime, current_rss, peak_rss, current_commit, peak_commit, page_faults, fds,
|
||||
qcount, qerrors, qtime, qavg, qmax, (int)getpid());
|
||||
if (n < 0 || (size_t)n >= sizeof(json)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fprintf(f,
|
||||
"{\n"
|
||||
" \"uptime_s\": %ld,\n"
|
||||
" \"rss_bytes\": %zu,\n"
|
||||
" \"peak_rss_bytes\": %zu,\n"
|
||||
" \"heap_committed_bytes\": %zu,\n"
|
||||
" \"peak_committed_bytes\": %zu,\n"
|
||||
" \"page_faults\": %zu,\n"
|
||||
" \"fd_count\": %d,\n"
|
||||
" \"query_count\": %d,\n"
|
||||
" \"query_errors\": %d,\n"
|
||||
" \"query_total_us\": %lld,\n"
|
||||
" \"query_avg_us\": %lld,\n"
|
||||
" \"query_max_us\": %lld,\n"
|
||||
" \"pid\": %d\n"
|
||||
"}\n",
|
||||
uptime, current_rss, peak_rss, current_commit, peak_commit, page_faults, fds,
|
||||
qcount, qerrors, qtime, qavg, qmax, (int)getpid()) < 0) {
|
||||
(void)fclose(f);
|
||||
return;
|
||||
}
|
||||
if (fclose(f) != 0) {
|
||||
return;
|
||||
}
|
||||
(void)rename(tmp_path, g_diag_path);
|
||||
(void)cbm_write_file_atomic(g_diag_path, json, (size_t)n, NULL);
|
||||
}
|
||||
|
||||
static void *diag_thread_fn(void *arg) {
|
||||
@@ -177,8 +166,12 @@ bool cbm_diag_start(void) {
|
||||
g_start_time = time(NULL);
|
||||
atomic_store(&g_diag_stop, 0);
|
||||
|
||||
snprintf(g_diag_path, sizeof(g_diag_path), "%s/cbm-diagnostics-%d.json", cbm_tmpdir(),
|
||||
(int)getpid());
|
||||
int path_len = snprintf(g_diag_path, sizeof(g_diag_path), "%s/cbm-diagnostics-%d.json",
|
||||
cbm_tmpdir(), (int)getpid());
|
||||
if (path_len < 0 || (size_t)path_len >= sizeof(g_diag_path)) {
|
||||
g_diag_path[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cbm_thread_create(&g_diag_thread, 0, diag_thread_fn, NULL) != 0) {
|
||||
return false;
|
||||
@@ -201,6 +194,8 @@ void cbm_diag_stop(void) {
|
||||
/* Clean up file */
|
||||
cbm_unlink(g_diag_path);
|
||||
char tmp_path[sizeof(g_diag_path) + DIAG_PATH_EXTRA];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", g_diag_path);
|
||||
cbm_unlink(tmp_path);
|
||||
int tmp_len = snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", g_diag_path);
|
||||
if (tmp_len >= 0 && (size_t)tmp_len < sizeof(tmp_path)) {
|
||||
cbm_unlink(tmp_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ enum {
|
||||
#include "sqlite_writer.h"
|
||||
#include "foundation/hash_table.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/dyn_array.h"
|
||||
#include "foundation/profile.h"
|
||||
@@ -1449,7 +1450,19 @@ int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) {
|
||||
if (!gb || !path) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
char tmp_path[CBM_SZ_1K];
|
||||
int tmp_len = snprintf(tmp_path, sizeof(tmp_path), "%s.tmp.XXXXXX", path);
|
||||
if (tmp_len <= 0 || (size_t)tmp_len >= sizeof(tmp_path)) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
char wal_path[CBM_SZ_1K];
|
||||
char shm_path[CBM_SZ_1K];
|
||||
int wal_len = snprintf(wal_path, sizeof(wal_path), "%s-wal", path);
|
||||
int shm_len = snprintf(shm_path, sizeof(shm_path), "%s-shm", path);
|
||||
if (wal_len <= 0 || (size_t)wal_len >= sizeof(wal_path) || shm_len <= 0 ||
|
||||
(size_t)shm_len >= sizeof(shm_path)) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
CBM_PROF_START(t_count);
|
||||
int live_count = count_live_nodes(gb);
|
||||
CBM_PROF_END_N("dump", "1_count_live_nodes", t_count, live_count);
|
||||
@@ -1470,14 +1483,24 @@ int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) {
|
||||
char indexed_at[CBM_SZ_64];
|
||||
generate_iso_timestamp(indexed_at, sizeof(indexed_at));
|
||||
|
||||
/* Stream node rows to the DB in partitions. Under memory pressure, free each
|
||||
int tmp_fd = cbm_mkstemp_s(tmp_path, sizeof(tmp_path));
|
||||
if (tmp_fd < 0) {
|
||||
free(src_nodes);
|
||||
free(dump_nodes);
|
||||
free(temp_to_final);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
cbm_close_fd(tmp_fd);
|
||||
|
||||
/* Stream node rows to the unique temp DB in partitions. Under memory pressure, free each
|
||||
* partition's heavy properties_json once persisted — the heavy column is
|
||||
* write-once and never read again, so this bounds the dump/finalize peak.
|
||||
* The DB output is identical whether or not freeing engages, so non-pressure
|
||||
* runs (and tests) leave the gbuf intact (the budget>0 guard keeps an
|
||||
* uninitialized budget from ever triggering the free). */
|
||||
cbm_db_writer_t *w = cbm_writer_open(path);
|
||||
cbm_db_writer_t *w = cbm_writer_open(tmp_path);
|
||||
if (!w) {
|
||||
cbm_unlink(tmp_path);
|
||||
free(src_nodes);
|
||||
free(dump_nodes);
|
||||
free(temp_to_final);
|
||||
@@ -1530,11 +1553,9 @@ int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) {
|
||||
rc = frc;
|
||||
}
|
||||
|
||||
/* Post-write integrity verification (B1 mitigation): the streaming dump can
|
||||
* intermittently emit a structurally-corrupt .db (gbuf-data corruption that
|
||||
* is ASan/TSan-invisible). Detect it with the fast projects-table check
|
||||
* (O(1), not a full integrity_check) and remove the corrupt DB so neither
|
||||
* queries nor tests ever read garbage — the next access re-indexes.
|
||||
/* Post-write integrity verification (B1 mitigation): verify the temp DB
|
||||
* before the atomic rename. A corrupt or unopenable temp file is deleted;
|
||||
* the previously published DB, if any, remains in place for readers.
|
||||
*
|
||||
* Use the _full variant so a path-only defect (root_path the check considers
|
||||
* non-absolute, e.g. a relative repo_path like ".") is RETAINED, not deleted
|
||||
@@ -1543,7 +1564,7 @@ int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) {
|
||||
* path is indexed (caught by self-indexing the repo with repo_path=".").
|
||||
* Consistent with #557 resolve_store. */
|
||||
if (rc == 0) {
|
||||
cbm_store_t *verify = cbm_store_open_path((const char *)path);
|
||||
cbm_store_t *verify = cbm_store_open_path((const char *)tmp_path);
|
||||
if (verify) {
|
||||
bool path_only = false;
|
||||
bool intact = cbm_store_check_integrity_full(verify, &path_only);
|
||||
@@ -1553,14 +1574,30 @@ int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) {
|
||||
char edges_str[CBM_SZ_16];
|
||||
snprintf(nodes_str, sizeof(nodes_str), "%d", node_idx);
|
||||
snprintf(edges_str, sizeof(edges_str), "%d", edge_idx);
|
||||
cbm_log_error("dump.verify_corrupt", "path", path, "action",
|
||||
"deleting corrupt db; re-index required", "nodes", nodes_str, "edges",
|
||||
edges_str);
|
||||
unlink(path);
|
||||
cbm_log_error("dump.verify_corrupt", "path", tmp_path, "action",
|
||||
"deleting corrupt temp db; re-index required", "nodes", nodes_str,
|
||||
"edges", edges_str);
|
||||
cbm_unlink(tmp_path);
|
||||
rc = GB_ERR;
|
||||
}
|
||||
} else {
|
||||
cbm_log_error("dump.verify_open_failed", "path", tmp_path, "action",
|
||||
"deleting unverified temp db");
|
||||
cbm_unlink(tmp_path);
|
||||
rc = GB_ERR;
|
||||
}
|
||||
}
|
||||
if (rc == 0) {
|
||||
cbm_unlink(wal_path);
|
||||
cbm_unlink(shm_path);
|
||||
if (cbm_replace_file(tmp_path, path) != 0) {
|
||||
cbm_log_error("dump.rename_failed", "tmp", tmp_path, "path", path);
|
||||
cbm_unlink(tmp_path);
|
||||
rc = GB_ERR;
|
||||
}
|
||||
} else {
|
||||
cbm_unlink(tmp_path);
|
||||
}
|
||||
|
||||
log_dump_summary(node_idx, edge_idx);
|
||||
free_dump_resources(url_paths, edge_idx, dump_edges, dump_nodes, temp_to_final);
|
||||
|
||||
+4
-4
@@ -31,6 +31,7 @@ enum {
|
||||
MAIN_PORT_OFF = 7, /* strlen("--port=") */
|
||||
MAIN_MAX_PORT = 65536,
|
||||
PARENT_WATCHDOG_STACK_SIZE = 64 * CBM_SZ_1K, /* watchdog only polls — tiny stack suffices */
|
||||
PARENT_WATCHDOG_POLL_US = CBM_USEC_PER_SEC / 2,
|
||||
};
|
||||
#define MAIN_RAM_FRACTION 0.5
|
||||
|
||||
@@ -112,10 +113,9 @@ static void signal_handler(int sig) {
|
||||
#ifndef _WIN32
|
||||
static void *parent_watchdog_thread(void *arg) {
|
||||
pid_t initial_ppid = *(pid_t *)arg;
|
||||
const unsigned int poll_interval_us = 500000; /* 500ms */
|
||||
|
||||
while (!atomic_load(&g_shutdown)) {
|
||||
cbm_usleep(poll_interval_us);
|
||||
cbm_usleep(PARENT_WATCHDOG_POLL_US);
|
||||
if (atomic_load(&g_shutdown)) {
|
||||
break;
|
||||
}
|
||||
@@ -327,10 +327,10 @@ static void print_help(void) {
|
||||
printf("\nSupported agents (auto-detected):\n");
|
||||
printf(" Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode,\n");
|
||||
printf(" Antigravity, Aider, KiloCode, Kiro\n");
|
||||
printf("\nTools: index_repository, search_graph, query_graph, trace_path,\n");
|
||||
printf("\nTools: index_repository, search_graph, query_graph, trace_call_path,\n");
|
||||
printf(" get_code_snippet, get_graph_schema, get_architecture, search_code,\n");
|
||||
printf(" list_projects, delete_project, index_status, detect_changes,\n");
|
||||
printf(" manage_adr, ingest_traces\n");
|
||||
printf(" manage_adr, ingest_traces, index_dependencies\n");
|
||||
}
|
||||
|
||||
/* ── Main ───────────────────────────────────────────────────────── */
|
||||
|
||||
+328
-111
@@ -96,7 +96,7 @@ static void add_pagerank_val(yyjson_mut_doc *doc, yyjson_mut_val *obj, double v)
|
||||
/* Default result limit for search_graph and search_code.
|
||||
* Prevents unbounded 500K-result responses. Callers can override.
|
||||
* Configurable via config key "search_limit". */
|
||||
#define CBM_DEFAULT_SEARCH_LIMIT 50
|
||||
#define CBM_MCP_DEFAULT_SEARCH_LIMIT 50
|
||||
#define CBM_CONFIG_SEARCH_LIMIT "search_limit"
|
||||
|
||||
/* Default: rank dependency sub-project symbols (proj.dep.*) LAST so a stdlib
|
||||
@@ -123,7 +123,16 @@ static void add_pagerank_val(yyjson_mut_doc *doc, yyjson_mut_val *obj, double v)
|
||||
|
||||
/* Idle store eviction: close cached project store after this many seconds
|
||||
* of inactivity to free SQLite memory during idle periods. */
|
||||
#define STORE_IDLE_TIMEOUT_S 60
|
||||
#define CBM_MCP_DEFAULT_STORE_IDLE_TIMEOUT_S 60
|
||||
#define CBM_CONFIG_STORE_IDLE_TIMEOUT_S "store_idle_timeout_s"
|
||||
/* Read-only DB validation should fail promptly when another process holds a
|
||||
* lock; this path is on startup/project discovery and must not hang MCP. */
|
||||
#define CBM_DB_VALIDATE_BUSY_TIMEOUT_MS 1000
|
||||
#define CBM_CONFIG_DB_VALIDATE_BUSY_TIMEOUT_MS "db_validate_busy_timeout_ms"
|
||||
/* Optional background release check. Default preserves the historical 5s curl
|
||||
* bound; config value 0 disables the network probe for offline/locked-down MCP. */
|
||||
#define CBM_MCP_UPDATE_CHECK_TIMEOUT_S 5
|
||||
#define CBM_CONFIG_UPDATE_CHECK_TIMEOUT_S "update_check_timeout_s"
|
||||
|
||||
/* Config key: comma-separated glob patterns to exclude from key_functions.
|
||||
* Set via: config set key_functions_exclude "scripts/,tools/,tests/" */
|
||||
@@ -133,9 +142,16 @@ static void add_pagerank_val(yyjson_mut_doc *doc, yyjson_mut_val *obj, double v)
|
||||
* header (closes the codebase://architecture pull-only gap). Smaller than the
|
||||
* get_architecture default (25) to keep first-response token cost modest. */
|
||||
#define CBM_CONTEXT_KEY_FUNCTIONS_LIMIT 10
|
||||
/* Config-tunable override for the _context key_functions push bound.
|
||||
* <=0 falls back to the CBM_CONTEXT_KEY_FUNCTIONS_LIMIT default above. */
|
||||
#define CBM_CONFIG_CONTEXT_KEY_FUNCTIONS_LIMIT "context_key_functions_limit"
|
||||
#define CBM_CONFIG_ARCH_HOTSPOT_LIMIT "arch_hotspot_limit"
|
||||
#define CBM_CONFIG_ARCH_RESOLUTION "architecture_resolution"
|
||||
#define CBM_CONFIG_SIMILARITY_THRESHOLD "similarity_threshold"
|
||||
#define CBM_CONFIG_HTTPLINK_MIN_CONFIDENCE "httplink_min_confidence"
|
||||
#define CBM_CONFIG_SEMANTIC_THRESHOLD "semantic_threshold"
|
||||
#define CBM_CONFIG_GITHISTORY_MIN_COUPLING "githistory_min_coupling"
|
||||
#define CBM_CONFIG_LSP_CONFIDENCE_FLOOR "lsp_confidence_floor"
|
||||
|
||||
/* Directory permissions: rwxr-xr-x */
|
||||
#define ADR_DIR_PERMS 0755
|
||||
@@ -797,6 +813,7 @@ struct cbm_mcp_server {
|
||||
char *current_project; /* which project store is open for (heap) */
|
||||
time_t store_last_used; /* last time resolve_store was called for a named project */
|
||||
char update_notice[CBM_SZ_256]; /* one-shot update notice, cleared after first injection */
|
||||
cbm_mutex_t update_notice_lock; /* protects update_notice across background check/request thread */
|
||||
bool update_checked; /* true after background check has been launched */
|
||||
cbm_thread_t update_tid; /* background update check thread */
|
||||
bool update_thread_active; /* true if update thread was started and needs joining */
|
||||
@@ -820,6 +837,36 @@ struct cbm_mcp_server {
|
||||
int64_t active_request_id; /* JSON-RPC id of the in-progress tool call */
|
||||
};
|
||||
|
||||
static int cbm_mcp_config_int_clamped(cbm_mcp_server_t *srv, const char *key, int default_val,
|
||||
int min_val, int max_val) {
|
||||
int value = srv && srv->config ? cbm_config_get_int(srv->config, key, default_val) : default_val;
|
||||
if (value < min_val) {
|
||||
value = min_val;
|
||||
}
|
||||
if (value > max_val) {
|
||||
value = max_val;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static int cbm_mcp_store_idle_timeout_s(cbm_mcp_server_t *srv) {
|
||||
return cbm_mcp_config_int_clamped(srv, CBM_CONFIG_STORE_IDLE_TIMEOUT_S,
|
||||
CBM_MCP_DEFAULT_STORE_IDLE_TIMEOUT_S, 1,
|
||||
CBM_SZ_64K);
|
||||
}
|
||||
|
||||
static int cbm_mcp_db_validate_busy_timeout_ms(cbm_mcp_server_t *srv) {
|
||||
return cbm_mcp_config_int_clamped(srv, CBM_CONFIG_DB_VALIDATE_BUSY_TIMEOUT_MS,
|
||||
CBM_DB_VALIDATE_BUSY_TIMEOUT_MS, 0,
|
||||
CBM_SZ_64K);
|
||||
}
|
||||
|
||||
static int cbm_mcp_update_check_timeout_s(cbm_mcp_server_t *srv) {
|
||||
return cbm_mcp_config_int_clamped(srv, CBM_CONFIG_UPDATE_CHECK_TIMEOUT_S,
|
||||
CBM_MCP_UPDATE_CHECK_TIMEOUT_S, 0,
|
||||
CBM_SZ_256);
|
||||
}
|
||||
|
||||
/* ── Tool list (needs full struct definition above) ──────────── */
|
||||
|
||||
char *cbm_mcp_tools_list(cbm_mcp_server_t *srv) {
|
||||
@@ -916,6 +963,7 @@ cbm_mcp_server_t *cbm_mcp_server_new(const char *store_path) {
|
||||
if (!srv) {
|
||||
return NULL;
|
||||
}
|
||||
cbm_mutex_init(&srv->update_notice_lock);
|
||||
|
||||
/* If a store_path is given, open that project directly.
|
||||
* Otherwise, create an in-memory store for test/embedded use. */
|
||||
@@ -969,6 +1017,7 @@ void cbm_mcp_server_free(cbm_mcp_server_t *srv) {
|
||||
if (srv->autoindex_active) {
|
||||
cbm_thread_join(&srv->autoindex_tid);
|
||||
}
|
||||
cbm_mutex_destroy(&srv->update_notice_lock);
|
||||
if (srv->owns_store && srv->store) {
|
||||
cbm_store_close(srv->store);
|
||||
}
|
||||
@@ -1018,7 +1067,12 @@ static const char *cache_dir(char *buf, size_t bufsz) {
|
||||
if (!dir) {
|
||||
dir = cbm_tmpdir();
|
||||
}
|
||||
snprintf(buf, bufsz, "%s", dir);
|
||||
int len = snprintf(buf, bufsz, "%s", dir);
|
||||
if (len <= 0 || (size_t)len >= bufsz) {
|
||||
if (bufsz > 0) {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -1030,7 +1084,12 @@ static const char *project_db_path(const char *project, char *buf, size_t bufsz)
|
||||
}
|
||||
char dir[CBM_SZ_1K];
|
||||
cache_dir(dir, sizeof(dir));
|
||||
snprintf(buf, bufsz, "%s/%s.db", dir, project);
|
||||
int len = snprintf(buf, bufsz, "%s/%s.db", dir, project);
|
||||
if (len <= 0 || (size_t)len >= bufsz) {
|
||||
if (bufsz > 0) {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -1042,8 +1101,8 @@ static const char *project_db_path(const char *project, char *buf, size_t bufsz)
|
||||
* matching DB is found. Cost: one access() call per dot in the QN (~5-10). */
|
||||
static char *extract_project_from_qn(const char *qn) {
|
||||
if (!qn) return NULL;
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) return NULL;
|
||||
const char *cdir = cbm_resolve_cache_dir();
|
||||
if (!cdir) return NULL;
|
||||
|
||||
/* Scan each dot-separated prefix of the QN and test if a matching DB file
|
||||
* exists. Walk left-to-right so the last hit is the longest (most
|
||||
@@ -1056,13 +1115,11 @@ static char *extract_project_from_qn(const char *qn) {
|
||||
|
||||
size_t best_end = 0; /* length of the longest matching prefix found */
|
||||
char db_path[1024];
|
||||
const char *home_val = home;
|
||||
|
||||
for (size_t i = 0; i < qn_len; i++) {
|
||||
if (candidate[i] == '.') {
|
||||
candidate[i] = '\0';
|
||||
snprintf(db_path, sizeof(db_path),
|
||||
"%s/.cache/codebase-memory-mcp/%s.db", home_val, candidate);
|
||||
snprintf(db_path, sizeof(db_path), "%s/%s.db", cdir, candidate);
|
||||
if (access(db_path, F_OK) == 0) {
|
||||
best_end = i; /* length of this prefix */
|
||||
}
|
||||
@@ -1131,15 +1188,18 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
|
||||
/* Open project's .db file — query-only open (no SQLITE_OPEN_CREATE) to
|
||||
* prevent ghost .db file creation for unknown/unindexed projects. */
|
||||
char path[CBM_SZ_1K];
|
||||
project_db_path(project, path, sizeof(path));
|
||||
project_db_path(db_project, path, sizeof(path));
|
||||
if (!path[0]) {
|
||||
return NULL;
|
||||
}
|
||||
srv->store = cbm_store_open_path_query(path);
|
||||
if (srv->store) {
|
||||
/* Check DB integrity — auto-clean corrupt databases. A bad project
|
||||
/* Check DB integrity — auto-clean corrupt cache databases. A bad project
|
||||
* root_path (with an otherwise-fine projects table) is cosmetic: the
|
||||
* indexed nodes/edges are intact and queries key off project name, not
|
||||
* root_path. Retain such DBs instead of deleting them, to avoid the
|
||||
* data loss reported in #557. Only genuine corruption (e.g. an
|
||||
* over-accumulated projects table) is auto-deleted. */
|
||||
* data loss reported in #557. Only genuine structural corruption is
|
||||
* removed, and only from the derived CBM cache. */
|
||||
bool path_only = false;
|
||||
if (!cbm_store_check_integrity_full(srv->store, &path_only)) {
|
||||
if (path_only) {
|
||||
@@ -1151,14 +1211,18 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
|
||||
"deleting corrupt db — re-index required");
|
||||
cbm_store_close(srv->store);
|
||||
srv->store = NULL;
|
||||
/* Delete the corrupt DB + WAL/SHM files */
|
||||
cbm_unlink(path);
|
||||
/* Delete the corrupt cache DB + WAL/SHM files. */
|
||||
(void)cbm_unlink(path);
|
||||
char wal_path[MCP_FIELD_SIZE];
|
||||
char shm_path[MCP_FIELD_SIZE];
|
||||
snprintf(wal_path, sizeof(wal_path), "%s-wal", path);
|
||||
snprintf(shm_path, sizeof(shm_path), "%s-shm", path);
|
||||
cbm_unlink(wal_path);
|
||||
cbm_unlink(shm_path);
|
||||
int wal_len = snprintf(wal_path, sizeof(wal_path), "%s-wal", path);
|
||||
int shm_len = snprintf(shm_path, sizeof(shm_path), "%s-shm", path);
|
||||
if (wal_len > 0 && (size_t)wal_len < sizeof(wal_path)) {
|
||||
(void)cbm_unlink(wal_path);
|
||||
}
|
||||
if (shm_len > 0 && (size_t)shm_len < sizeof(shm_path)) {
|
||||
(void)cbm_unlink(shm_path);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@@ -1219,7 +1283,8 @@ static int collect_db_project_names(const char *dir_path, char *out, size_t out_
|
||||
if (count > 0 && offset < (int)out_sz - MCP_SEPARATOR) {
|
||||
out[offset++] = ',';
|
||||
}
|
||||
int wrote = snprintf(out + offset, out_sz - (size_t)offset, "\"%.*s\"", (int)(len - 3), n);
|
||||
int wrote = snprintf(out + offset, out_sz - (size_t)offset, "\"%.*s\"",
|
||||
(int)(len - MCP_DB_EXT), n);
|
||||
if (wrote > 0) {
|
||||
offset += wrote;
|
||||
if ((size_t)offset >= out_sz) {
|
||||
@@ -1518,8 +1583,14 @@ static void inject_context_once(yyjson_mut_doc *doc, yyjson_mut_val *root,
|
||||
const char *kf_exclude = srv->config
|
||||
? cbm_config_get(srv->config, CBM_CONFIG_KEY_FUNCTIONS_EXCLUDE, "")
|
||||
: "";
|
||||
char *kf_sql = build_key_functions_sql(kf_exclude, NULL,
|
||||
CBM_CONTEXT_KEY_FUNCTIONS_LIMIT);
|
||||
int kf_cfg_limit = srv->config
|
||||
? cbm_config_get_int(srv->config, CBM_CONFIG_CONTEXT_KEY_FUNCTIONS_LIMIT,
|
||||
CBM_CONTEXT_KEY_FUNCTIONS_LIMIT)
|
||||
: CBM_CONTEXT_KEY_FUNCTIONS_LIMIT;
|
||||
if (kf_cfg_limit <= 0) {
|
||||
kf_cfg_limit = CBM_CONTEXT_KEY_FUNCTIONS_LIMIT;
|
||||
}
|
||||
char *kf_sql = build_key_functions_sql(kf_exclude, NULL, kf_cfg_limit);
|
||||
if (kf_sql) {
|
||||
sqlite3_stmt *kf_stmt = NULL;
|
||||
if (sqlite3_prepare_v2(db, kf_sql, -1, &kf_stmt, NULL) == SQLITE_OK) {
|
||||
@@ -1727,7 +1798,7 @@ static void fill_project_params(const project_expand_t *pe, cbm_search_params_t
|
||||
* On ANY error: returns false, logs actionable warning to stderr,
|
||||
* does NOT crash, does NOT hang, does NOT modify the file.
|
||||
* Opens read-only with busy_timeout to avoid hanging on locked files. */
|
||||
static bool validate_cbm_db(const char *path) {
|
||||
static bool validate_cbm_db_with_timeout(const char *path, int busy_timeout_ms) {
|
||||
if (!path) return false;
|
||||
|
||||
struct stat vst;
|
||||
@@ -1764,7 +1835,7 @@ static bool validate_cbm_db(const char *path) {
|
||||
if (db) sqlite3_close(db);
|
||||
return false;
|
||||
}
|
||||
sqlite3_busy_timeout(db, 1000); /* 1s max — don't hang on locked files */
|
||||
sqlite3_busy_timeout(db, busy_timeout_ms);
|
||||
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
rc = sqlite3_prepare_v2(db,
|
||||
@@ -1808,10 +1879,17 @@ static bool is_project_db_file(const char *name, size_t len) {
|
||||
static void build_project_json_entry(yyjson_mut_doc *doc, yyjson_mut_val *arr, const char *dir_path,
|
||||
const char *name, size_t name_len, int64_t size_bytes) {
|
||||
char project_name[CBM_SZ_1K];
|
||||
snprintf(project_name, sizeof(project_name), "%.*s", (int)(name_len - 3), name);
|
||||
int project_len =
|
||||
snprintf(project_name, sizeof(project_name), "%.*s", (int)(name_len - MCP_DB_EXT), name);
|
||||
if (project_len <= 0 || (size_t)project_len >= sizeof(project_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
char full_path[CBM_SZ_2K];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, name);
|
||||
int full_path_len = snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, name);
|
||||
if (full_path_len <= 0 || (size_t)full_path_len >= sizeof(full_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cbm_store_t *pstore = cbm_store_open_path(full_path);
|
||||
int nodes = 0;
|
||||
@@ -1845,11 +1923,11 @@ static void build_project_json_entry(yyjson_mut_doc *doc, yyjson_mut_val *arr, c
|
||||
/* list_projects: scan cache directory for .db files.
|
||||
* Each project is a single .db file — no central registry needed. */
|
||||
static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) {
|
||||
(void)srv;
|
||||
(void)args;
|
||||
|
||||
char dir_path[CBM_SZ_1K];
|
||||
cache_dir(dir_path, sizeof(dir_path));
|
||||
int validate_busy_timeout_ms = cbm_mcp_db_validate_busy_timeout_ms(srv);
|
||||
|
||||
cbm_dir_t *d = cbm_opendir(dir_path);
|
||||
|
||||
@@ -1864,21 +1942,18 @@ static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) {
|
||||
const char *name = entry->name;
|
||||
size_t len = strlen(name);
|
||||
|
||||
/* Must end with .db and be at least 4 chars (x.db) */
|
||||
if (len < 4 || strcmp(name + len - 3, ".db") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Skip temp/internal files and corrupt project names */
|
||||
if (strncmp(name, "tmp-", 4) == 0 || strncmp(name, "_", 1) == 0 ||
|
||||
strncmp(name, ":memory:", 8) == 0 ||
|
||||
strcmp(name, "..db") == 0 || strcmp(name, ".db") == 0) {
|
||||
if (!is_project_db_file(name, len)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Extract project name = filename without .db suffix */
|
||||
char project_name[1024];
|
||||
snprintf(project_name, sizeof(project_name), "%.*s", (int)(len - 3), name);
|
||||
char project_name[CBM_SZ_1K];
|
||||
int project_len =
|
||||
snprintf(project_name, sizeof(project_name), "%.*s", (int)(len - MCP_DB_EXT),
|
||||
name);
|
||||
if (project_len <= 0 || (size_t)project_len >= sizeof(project_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Skip invalid project names (corrupt entries like ..db) */
|
||||
if (project_name[0] == '\0' || strcmp(project_name, ".") == 0 ||
|
||||
@@ -1887,15 +1962,18 @@ static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) {
|
||||
}
|
||||
|
||||
/* Get file metadata */
|
||||
char full_path[2048];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, name);
|
||||
char full_path[CBM_SZ_2K];
|
||||
int full_path_len = snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, name);
|
||||
if (full_path_len <= 0 || (size_t)full_path_len >= sizeof(full_path)) {
|
||||
continue;
|
||||
}
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Validate db structure before opening — skip corrupt/non-cbm files */
|
||||
if (!validate_cbm_db(full_path)) {
|
||||
if (!validate_cbm_db_with_timeout(full_path, validate_busy_timeout_ms)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2705,7 +2783,7 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
return cbm_mcp_text_result(errbuf, true);
|
||||
}
|
||||
int cfg_search_limit = cbm_config_get_int(srv->config, CBM_CONFIG_SEARCH_LIMIT,
|
||||
CBM_DEFAULT_SEARCH_LIMIT);
|
||||
CBM_MCP_DEFAULT_SEARCH_LIMIT);
|
||||
int limit = cbm_mcp_get_int_arg(args, "limit", cfg_search_limit);
|
||||
/* F4: treat limit<=0 as default */
|
||||
if (limit <= 0) limit = cfg_search_limit;
|
||||
@@ -3199,11 +3277,17 @@ static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
|
||||
/* Delete the .db file + WAL/SHM */
|
||||
char path[CBM_SZ_1K];
|
||||
project_db_path(name, path, sizeof(path));
|
||||
if (!path[0]) {
|
||||
cbm_pipeline_unlock();
|
||||
free(name);
|
||||
return cbm_mcp_text_result("{\"status\":\"delete_failed\",\"error\":\"project path too long\"}",
|
||||
true);
|
||||
}
|
||||
|
||||
char wal[CBM_SZ_1K];
|
||||
char shm[CBM_SZ_1K];
|
||||
snprintf(wal, sizeof(wal), "%s-wal", path);
|
||||
snprintf(shm, sizeof(shm), "%s-shm", path);
|
||||
int wal_len = snprintf(wal, sizeof(wal), "%s-wal", path);
|
||||
int shm_len = snprintf(shm, sizeof(shm), "%s-shm", path);
|
||||
|
||||
bool exists = (access(path, F_OK) == 0);
|
||||
const char *status = "not_found";
|
||||
@@ -3212,8 +3296,12 @@ static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
|
||||
|
||||
if (exists) {
|
||||
int rc = cbm_unlink(path);
|
||||
(void)cbm_unlink(wal);
|
||||
(void)cbm_unlink(shm);
|
||||
if (wal_len > 0 && (size_t)wal_len < sizeof(wal)) {
|
||||
(void)cbm_unlink(wal);
|
||||
}
|
||||
if (shm_len > 0 && (size_t)shm_len < sizeof(shm)) {
|
||||
(void)cbm_unlink(shm);
|
||||
}
|
||||
if (rc == 0) {
|
||||
status = "deleted";
|
||||
} else {
|
||||
@@ -4260,6 +4348,26 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
|
||||
if (sim_thresh > 0.0) {
|
||||
cbm_pipeline_set_similarity_threshold(p, sim_thresh);
|
||||
}
|
||||
double httplink_min =
|
||||
cbm_config_get_double(srv->config, CBM_CONFIG_HTTPLINK_MIN_CONFIDENCE, 0.0);
|
||||
if (httplink_min > 0.0) {
|
||||
cbm_pipeline_set_httplink_min_confidence(p, httplink_min);
|
||||
}
|
||||
double semantic_thresh =
|
||||
cbm_config_get_double(srv->config, CBM_CONFIG_SEMANTIC_THRESHOLD, 0.0);
|
||||
if (semantic_thresh > 0.0) {
|
||||
cbm_pipeline_set_semantic_threshold(p, semantic_thresh);
|
||||
}
|
||||
double gh_min =
|
||||
cbm_config_get_double(srv->config, CBM_CONFIG_GITHISTORY_MIN_COUPLING, 0.0);
|
||||
if (gh_min > 0.0) {
|
||||
cbm_pipeline_set_githistory_min_coupling(p, gh_min);
|
||||
}
|
||||
double lsp_floor =
|
||||
cbm_config_get_double(srv->config, CBM_CONFIG_LSP_CONFIDENCE_FLOOR, 0.0);
|
||||
if (lsp_floor > 0.0) {
|
||||
cbm_pipeline_set_lsp_confidence_floor(p, lsp_floor);
|
||||
}
|
||||
}
|
||||
|
||||
char *project_name = heap_strdup(cbm_pipeline_project_name(p));
|
||||
@@ -4462,6 +4570,12 @@ static yyjson_doc *enrich_node_properties(yyjson_mut_doc *doc, yyjson_mut_val *o
|
||||
if (!k) {
|
||||
continue;
|
||||
}
|
||||
/* Search results flatten node properties into the result object for
|
||||
* token economy, so property keys must not overwrite/collide with
|
||||
* stable result fields such as source:"project" vs source:"infra". */
|
||||
if (yyjson_mut_obj_get(obj, k) != NULL) {
|
||||
continue;
|
||||
}
|
||||
if (yyjson_is_str(val)) {
|
||||
yyjson_mut_obj_add_str(doc, obj, k, yyjson_get_str(val));
|
||||
} else if (yyjson_is_bool(val)) {
|
||||
@@ -5079,29 +5193,32 @@ static int search_result_cmp(const void *a, const void *b) {
|
||||
|
||||
/* Build the grep command string based on scoped vs recursive mode.
|
||||
* case_sensitive=false adds -i for case-insensitive matching (grep default is sensitive). */
|
||||
static void build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bool case_sensitive,
|
||||
static bool build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bool case_sensitive,
|
||||
bool scoped, const char *file_pattern, const char *tmpfile,
|
||||
const char *filelist, const char *root_path) {
|
||||
// NOLINTNEXTLINE(readability-implicit-bool-conversion)
|
||||
const char *flag = use_regex ? "-E" : "-F";
|
||||
const char *ci_flag = case_sensitive ? "" : " -i";
|
||||
int n;
|
||||
if (scoped) {
|
||||
if (file_pattern) {
|
||||
snprintf(cmd, cmd_sz, "xargs grep -n%s %s --include='%s' -f '%s' < '%s' 2>/dev/null",
|
||||
ci_flag, flag, file_pattern, tmpfile, filelist);
|
||||
n = snprintf(cmd, cmd_sz,
|
||||
"xargs grep -n%s %s --include='%s' -f '%s' < '%s' 2>/dev/null",
|
||||
ci_flag, flag, file_pattern, tmpfile, filelist);
|
||||
} else {
|
||||
snprintf(cmd, cmd_sz, "xargs grep -n%s %s -f '%s' < '%s' 2>/dev/null", ci_flag, flag,
|
||||
tmpfile, filelist);
|
||||
n = snprintf(cmd, cmd_sz, "xargs grep -n%s %s -f '%s' < '%s' 2>/dev/null", ci_flag,
|
||||
flag, tmpfile, filelist);
|
||||
}
|
||||
} else {
|
||||
if (file_pattern) {
|
||||
snprintf(cmd, cmd_sz, "grep -rn%s %s --include='%s' -f '%s' '%s' 2>/dev/null",
|
||||
ci_flag, flag, file_pattern, tmpfile, root_path);
|
||||
n = snprintf(cmd, cmd_sz, "grep -rn%s %s --include='%s' -f '%s' '%s' 2>/dev/null",
|
||||
ci_flag, flag, file_pattern, tmpfile, root_path);
|
||||
} else {
|
||||
snprintf(cmd, cmd_sz, "grep -rn%s %s -f '%s' '%s' 2>/dev/null", ci_flag, flag, tmpfile,
|
||||
root_path);
|
||||
n = snprintf(cmd, cmd_sz, "grep -rn%s %s -f '%s' '%s' 2>/dev/null", ci_flag, flag,
|
||||
tmpfile, root_path);
|
||||
}
|
||||
}
|
||||
return n >= 0 && (size_t)n < cmd_sz;
|
||||
}
|
||||
|
||||
/* Build deduplicated file list from search results + raw matches. */
|
||||
@@ -5534,13 +5651,35 @@ static bool validate_search_args(const char *root_path, const char *file_pattern
|
||||
|
||||
/* Write pattern to a temp file for grep -f. Returns true on success. */
|
||||
static bool write_pattern_file(char *tmpfile, int tmpfile_sz, const char *pattern) {
|
||||
snprintf(tmpfile, tmpfile_sz, "%s/cbm_search_%d.pat", cbm_tmpdir(), (int)getpid());
|
||||
FILE *tf = fopen(tmpfile, "w");
|
||||
if (!tmpfile || tmpfile_sz <= 0 || !pattern) {
|
||||
return false;
|
||||
}
|
||||
int n = snprintf(tmpfile, (size_t)tmpfile_sz, "%s/cbm_search_XXXXXX", cbm_tmpdir());
|
||||
if (n < 0 || n >= tmpfile_sz) {
|
||||
return false;
|
||||
}
|
||||
int fd = cbm_mkstemp_s(tmpfile, (size_t)tmpfile_sz);
|
||||
if (fd < 0) {
|
||||
return false;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
FILE *tf = _fdopen(fd, "w");
|
||||
#else
|
||||
FILE *tf = fdopen(fd, "w");
|
||||
#endif
|
||||
if (!tf) {
|
||||
cbm_close_fd(fd);
|
||||
cbm_unlink(tmpfile);
|
||||
return false;
|
||||
}
|
||||
bool ok = fputs(pattern, tf) >= 0 && fputc('\n', tf) != EOF;
|
||||
if (fclose(tf) != 0) {
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) {
|
||||
cbm_unlink(tmpfile);
|
||||
return false;
|
||||
}
|
||||
(void)fprintf(tf, "%s\n", pattern);
|
||||
(void)fclose(tf);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5560,7 +5699,7 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *mode_str = cbm_mcp_get_string_arg(args, "mode");
|
||||
int context_lines = cbm_mcp_get_int_arg(args, "context", 0);
|
||||
int cfg_search_limit_sc = cbm_config_get_int(srv->config, CBM_CONFIG_SEARCH_LIMIT,
|
||||
CBM_DEFAULT_SEARCH_LIMIT);
|
||||
CBM_MCP_DEFAULT_SEARCH_LIMIT);
|
||||
int limit = cbm_mcp_get_int_arg(args, "limit", cfg_search_limit_sc);
|
||||
bool use_regex = cbm_mcp_get_bool_arg(args, "regex");
|
||||
uint64_t search_t0 = cbm_now_ms();
|
||||
@@ -5705,7 +5844,7 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
|
||||
}
|
||||
|
||||
/* ── Phase 1: Grep scan ──────────────────────────────────── */
|
||||
char tmpfile[CBM_SZ_256];
|
||||
char tmpfile[CBM_PATH_MAX];
|
||||
if (!write_pattern_file(tmpfile, sizeof(tmpfile), pattern)) {
|
||||
char errmsg[CBM_SZ_256];
|
||||
snprintf(errmsg, sizeof(errmsg), "search failed: cannot create temp file (%s)",
|
||||
@@ -5732,13 +5871,32 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
|
||||
* miss files written or modified after the last index run (e.g. in tests
|
||||
* and in active development workflows where new files aren't yet indexed).
|
||||
* Vendored/generated code can be excluded via .gitignore or path_filter. */
|
||||
char filelist[256];
|
||||
snprintf(filelist, sizeof(filelist), "%s.files", tmpfile);
|
||||
char filelist[CBM_PATH_MAX];
|
||||
int filelist_len = snprintf(filelist, sizeof(filelist), "%s.files", tmpfile);
|
||||
if (filelist_len < 0 || (size_t)filelist_len >= sizeof(filelist)) {
|
||||
cbm_unlink(tmpfile);
|
||||
free(root_path);
|
||||
free(pattern);
|
||||
free(project);
|
||||
free(file_pattern);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"search failed: temporary file path too long\","
|
||||
"\"hint\":\"Use a shorter TMPDIR/TEMP path or project path.\"}", true);
|
||||
}
|
||||
bool scoped = false;
|
||||
|
||||
char cmd[4096];
|
||||
build_grep_cmd(cmd, sizeof(cmd), use_regex, case_sensitive, scoped, file_pattern, tmpfile,
|
||||
filelist, root_path);
|
||||
if (!build_grep_cmd(cmd, sizeof(cmd), use_regex, case_sensitive, scoped, file_pattern, tmpfile,
|
||||
filelist, root_path)) {
|
||||
cbm_unlink(tmpfile);
|
||||
free(root_path);
|
||||
free(pattern);
|
||||
free(project);
|
||||
free(file_pattern);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"search failed: grep command too long\","
|
||||
"\"hint\":\"Use a shorter project path or file_pattern.\"}", true);
|
||||
}
|
||||
|
||||
FILE *fp = cbm_popen(cmd, "r");
|
||||
if (!fp) {
|
||||
@@ -5936,17 +6094,27 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
|
||||
/* Get changed files via git (-C avoids cd + quoting issues on Windows) */
|
||||
char cmd[CBM_SZ_2K];
|
||||
int cmd_len;
|
||||
#ifdef _WIN32
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"git -C \"%s\" diff --name-only \"%s\"...HEAD 2>NUL & "
|
||||
"git -C \"%s\" diff --name-only 2>NUL",
|
||||
root_path, base_branch, root_path);
|
||||
cmd_len = snprintf(cmd, sizeof(cmd),
|
||||
"git -C \"%s\" diff --name-only \"%s\"...HEAD 2>NUL & "
|
||||
"git -C \"%s\" diff --name-only 2>NUL",
|
||||
root_path, base_branch, root_path);
|
||||
#else
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"{ git -C '%s' diff --name-only '%s'...HEAD 2>/dev/null; "
|
||||
"git -C '%s' diff --name-only 2>/dev/null; } | sort -u",
|
||||
root_path, base_branch, root_path);
|
||||
cmd_len = snprintf(cmd, sizeof(cmd),
|
||||
"{ git -C \"%s\" diff --name-only \"%s\"...HEAD 2>/dev/null; "
|
||||
"git -C \"%s\" diff --name-only 2>/dev/null; } | sort -u",
|
||||
root_path, base_branch, root_path);
|
||||
#endif
|
||||
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) {
|
||||
free(root_path);
|
||||
free(project);
|
||||
free(base_branch);
|
||||
free(scope);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"git diff failed: command too long\","
|
||||
"\"hint\":\"Use shorter project paths or branch names.\"}", true);
|
||||
}
|
||||
|
||||
FILE *fp = cbm_popen(cmd, "r");
|
||||
if (!fp) {
|
||||
@@ -6585,9 +6753,16 @@ static bool db_is_stale(const char *db_path, const char *repo_path, int max_age_
|
||||
}
|
||||
|
||||
/* Check git HEAD commit time vs DB mtime */
|
||||
if (!validate_search_path_arg(repo_path)) return false;
|
||||
char cmd[1024];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"git -C '%s' log -1 --format=%%ct HEAD 2>/dev/null", repo_path);
|
||||
#ifdef _WIN32
|
||||
const char *null_dev = "NUL";
|
||||
#else
|
||||
const char *null_dev = "/dev/null";
|
||||
#endif
|
||||
int cmd_len = snprintf(cmd, sizeof(cmd), "git -C \"%s\" log -1 --format=%%ct HEAD 2>%s",
|
||||
repo_path, null_dev);
|
||||
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) return false;
|
||||
// NOLINTNEXTLINE(bugprone-command-processor,cert-env33-c)
|
||||
FILE *fp = cbm_popen(cmd, "r");
|
||||
if (!fp) return false;
|
||||
@@ -6615,11 +6790,8 @@ static void maybe_auto_index(cbm_mcp_server_t *srv) {
|
||||
/* Check if project already has a populated DB */
|
||||
bool needs_index = true;
|
||||
char db_check[1024] = {0};
|
||||
const char *home = cbm_get_home_dir();
|
||||
if (home) {
|
||||
snprintf(db_check, sizeof(db_check), "%s/.cache/codebase-memory-mcp/%s.db", home,
|
||||
srv->session_project);
|
||||
|
||||
project_db_path(srv->session_project, db_check, sizeof(db_check));
|
||||
if (db_check[0]) {
|
||||
if (db_has_content(db_check)) {
|
||||
/* DB exists and has nodes — check if stale */
|
||||
bool reindex_on_startup = srv->config
|
||||
@@ -6687,26 +6859,47 @@ static void maybe_auto_index(cbm_mcp_server_t *srv) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Quick file count check to avoid OOM on massive repos */
|
||||
if (!cbm_validate_shell_arg(srv->session_root)) {
|
||||
cbm_log_warn("autoindex.skip", "reason", "path contains shell metacharacters");
|
||||
return;
|
||||
}
|
||||
char cmd[CBM_SZ_1K];
|
||||
snprintf(cmd, sizeof(cmd), "git -C '%s' ls-files 2>/dev/null | wc -l", srv->session_root);
|
||||
FILE *fp = cbm_popen(cmd, "r");
|
||||
if (fp) {
|
||||
char line[CBM_SZ_64];
|
||||
if (fgets(line, sizeof(line), fp)) {
|
||||
int count = (int)strtol(line, NULL, CBM_DECIMAL_BASE);
|
||||
/* Quick file count check to avoid OOM on massive repos. A configured limit
|
||||
* of 0 means "no limit", matching the public config registry. */
|
||||
if (file_limit > 0) {
|
||||
if (!validate_search_path_arg(srv->session_root)) {
|
||||
cbm_log_warn("autoindex.skip", "reason", "path contains shell metacharacters");
|
||||
return;
|
||||
}
|
||||
char cmd[CBM_SZ_1K];
|
||||
#ifdef _WIN32
|
||||
const char *null_dev = "NUL";
|
||||
#else
|
||||
const char *null_dev = "/dev/null";
|
||||
#endif
|
||||
int cmd_len = snprintf(cmd, sizeof(cmd), "git -C \"%s\" ls-files 2>%s",
|
||||
srv->session_root, null_dev);
|
||||
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) {
|
||||
cbm_log_warn("autoindex.skip", "reason", "file_count_command_too_long");
|
||||
return;
|
||||
}
|
||||
FILE *fp = cbm_popen(cmd, "r");
|
||||
if (fp) {
|
||||
char *line = NULL;
|
||||
size_t line_cap = 0;
|
||||
int count = 0;
|
||||
while (cbm_getline(&line, &line_cap, fp) > 0) {
|
||||
count++;
|
||||
if (count > file_limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(line);
|
||||
if (count > file_limit) {
|
||||
cbm_log_warn("autoindex.skip", "reason", "too_many_files", "files", line, "limit",
|
||||
char count_buf[CBM_SZ_32];
|
||||
snprintf(count_buf, sizeof(count_buf), "%d", count);
|
||||
cbm_log_warn("autoindex.skip", "reason", "too_many_files", "files", count_buf, "limit",
|
||||
CBM_CONFIG_AUTO_INDEX_LIMIT);
|
||||
cbm_pclose(fp);
|
||||
return;
|
||||
}
|
||||
cbm_pclose(fp);
|
||||
}
|
||||
cbm_pclose(fp);
|
||||
}
|
||||
|
||||
/* Launch auto-index in background */
|
||||
@@ -6722,12 +6915,22 @@ static void maybe_auto_index(cbm_mcp_server_t *srv) {
|
||||
static void *update_check_thread(void *arg) {
|
||||
cbm_mcp_server_t *srv = (cbm_mcp_server_t *)arg;
|
||||
|
||||
/* Use curl with 5s timeout to fetch latest release tag */
|
||||
FILE *fp = cbm_popen("curl -sf --max-time 5 -H 'Accept: application/vnd.github+json' "
|
||||
"'" UPDATE_CHECK_URL "' 2>/dev/null",
|
||||
"r");
|
||||
int timeout_s = cbm_mcp_update_check_timeout_s(srv);
|
||||
if (timeout_s <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char cmd[CBM_SZ_512];
|
||||
int cmd_len = snprintf(cmd, sizeof(cmd),
|
||||
"curl -sf --max-time %d -H 'Accept: application/vnd.github+json' "
|
||||
"'" UPDATE_CHECK_URL "' 2>/dev/null",
|
||||
timeout_s);
|
||||
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FILE *fp = cbm_popen(cmd, "r");
|
||||
if (!fp) {
|
||||
srv->update_checked = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -6746,7 +6949,6 @@ static void *update_check_thread(void *arg) {
|
||||
/* Parse tag_name from JSON response */
|
||||
yyjson_doc *doc = yyjson_read(buf, total, 0);
|
||||
if (!doc) {
|
||||
srv->update_checked = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -6757,17 +6959,18 @@ static void *update_check_thread(void *arg) {
|
||||
if (tag_str) {
|
||||
const char *current = cbm_cli_get_version();
|
||||
if (cbm_compare_versions(tag_str, current) > 0) {
|
||||
cbm_mutex_lock(&srv->update_notice_lock);
|
||||
snprintf(srv->update_notice, sizeof(srv->update_notice),
|
||||
"Update available: %s -> %s -- run: codebase-memory-mcp update | "
|
||||
"Enjoying codebase-memory-mcp? Please leave a star: "
|
||||
"https://github.com/DeusData/codebase-memory-mcp",
|
||||
current, tag_str);
|
||||
cbm_mutex_unlock(&srv->update_notice_lock);
|
||||
cbm_log_info("update.available", "current", current, "latest", tag_str);
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_doc_free(doc);
|
||||
srv->update_checked = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -6783,7 +6986,16 @@ static void start_update_check(cbm_mcp_server_t *srv) {
|
||||
|
||||
/* Prepend update notice to a tool result, then clear it (one-shot). */
|
||||
static char *inject_update_notice(cbm_mcp_server_t *srv, char *result_json) {
|
||||
if (srv->update_notice[0] == '\0') {
|
||||
if (!srv || !result_json) {
|
||||
return result_json;
|
||||
}
|
||||
|
||||
char notice[sizeof(srv->update_notice)];
|
||||
cbm_mutex_lock(&srv->update_notice_lock);
|
||||
snprintf(notice, sizeof(notice), "%s", srv->update_notice);
|
||||
cbm_mutex_unlock(&srv->update_notice_lock);
|
||||
|
||||
if (notice[0] == '\0') {
|
||||
return result_json;
|
||||
}
|
||||
|
||||
@@ -6808,7 +7020,7 @@ static char *inject_update_notice(cbm_mcp_server_t *srv, char *result_json) {
|
||||
/* Prepend a text content item with the update notice */
|
||||
yyjson_mut_val *notice_item = yyjson_mut_obj(mdoc);
|
||||
yyjson_mut_obj_add_str(mdoc, notice_item, "type", "text");
|
||||
yyjson_mut_obj_add_str(mdoc, notice_item, "text", srv->update_notice);
|
||||
yyjson_mut_obj_add_str(mdoc, notice_item, "text", notice);
|
||||
yyjson_mut_arr_prepend(content, notice_item);
|
||||
}
|
||||
|
||||
@@ -6818,7 +7030,9 @@ static char *inject_update_notice(cbm_mcp_server_t *srv, char *result_json) {
|
||||
|
||||
if (new_json) {
|
||||
free(result_json);
|
||||
srv->update_notice[0] = '\0'; /* clear — one-shot */
|
||||
cbm_mutex_lock(&srv->update_notice_lock);
|
||||
srv->update_notice[0] = '\0'; /* clear — one-shot after successful injection */
|
||||
cbm_mutex_unlock(&srv->update_notice_lock);
|
||||
return new_json;
|
||||
}
|
||||
return result_json;
|
||||
@@ -7404,6 +7618,8 @@ static void handle_content_length_frame(cbm_mcp_server_t *srv, FILE *in, FILE *o
|
||||
* Returns: 1 = data ready, 0 = timeout (evicted idle stores), -1 = error/EOF. */
|
||||
static int poll_for_input_unix(cbm_mcp_server_t *srv, int fd, FILE *in) {
|
||||
struct pollfd pfd = {.fd = fd, .events = POLLIN};
|
||||
int idle_timeout_s = cbm_mcp_store_idle_timeout_s(srv);
|
||||
int poll_timeout_ms = idle_timeout_s * MCP_TIMEOUT_MS;
|
||||
int pr = poll(&pfd, SKIP_ONE, 0); /* Phase 1: non-blocking */
|
||||
|
||||
if (pr < 0) {
|
||||
@@ -7417,12 +7633,12 @@ static int poll_for_input_unix(cbm_mcp_server_t *srv, int fd, FILE *in) {
|
||||
int saved_flags = fcntl(fd, F_GETFL);
|
||||
if (saved_flags < 0) {
|
||||
/* fcntl failed — fall through to blocking poll */
|
||||
pr = poll(&pfd, SKIP_ONE, STORE_IDLE_TIMEOUT_S * MCP_TIMEOUT_MS);
|
||||
pr = poll(&pfd, SKIP_ONE, poll_timeout_ms);
|
||||
if (pr < 0) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
if (pr == 0) {
|
||||
cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S);
|
||||
cbm_mcp_server_evict_idle(srv, idle_timeout_s);
|
||||
return 0;
|
||||
}
|
||||
return SKIP_ONE;
|
||||
@@ -7438,12 +7654,12 @@ static int poll_for_input_unix(cbm_mcp_server_t *srv, int fd, FILE *in) {
|
||||
}
|
||||
clearerr(in);
|
||||
/* Phase 3: blocking poll */
|
||||
pr = poll(&pfd, SKIP_ONE, STORE_IDLE_TIMEOUT_S * MCP_TIMEOUT_MS);
|
||||
pr = poll(&pfd, SKIP_ONE, poll_timeout_ms);
|
||||
if (pr < 0) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
if (pr == 0) {
|
||||
cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S);
|
||||
cbm_mcp_server_evict_idle(srv, idle_timeout_s);
|
||||
return 0;
|
||||
}
|
||||
return SKIP_ONE;
|
||||
@@ -7469,7 +7685,7 @@ int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) {
|
||||
* buffered FILE*. When a client sends multiple messages in rapid
|
||||
* succession, the first getline() call may drain ALL kernel data into
|
||||
* libc's internal FILE* buffer. Subsequent poll() calls then see an
|
||||
* empty kernel fd and block for STORE_IDLE_TIMEOUT_S seconds even
|
||||
* empty kernel fd and block for store_idle_timeout_s seconds even
|
||||
* though the next messages are already in the FILE* buffer.
|
||||
*
|
||||
* Fix (Unix): use a three-phase approach —
|
||||
@@ -7484,12 +7700,13 @@ int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) {
|
||||
#ifdef _WIN32
|
||||
/* Windows: WaitForSingleObject on stdin handle */
|
||||
HANDLE hStdin = (HANDLE)_get_osfhandle(fd);
|
||||
DWORD wr = WaitForSingleObject(hStdin, STORE_IDLE_TIMEOUT_S * MCP_TIMEOUT_MS);
|
||||
int idle_timeout_s = cbm_mcp_store_idle_timeout_s(srv);
|
||||
DWORD wr = WaitForSingleObject(hStdin, (DWORD)(idle_timeout_s * MCP_TIMEOUT_MS));
|
||||
if (wr == WAIT_FAILED) {
|
||||
break;
|
||||
}
|
||||
if (wr == WAIT_TIMEOUT) {
|
||||
cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S);
|
||||
cbm_mcp_server_evict_idle(srv, idle_timeout_s);
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
|
||||
+143
-111
@@ -21,6 +21,7 @@ enum {
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/str_util.h"
|
||||
|
||||
#include "zstd_store.h"
|
||||
|
||||
@@ -95,25 +96,6 @@ static int artifact_export_fail(const char *stage, const char *path, const char
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *err;
|
||||
int err_no;
|
||||
} artifact_file_error_t;
|
||||
|
||||
static void file_error_clear(artifact_file_error_t *out) {
|
||||
if (out) {
|
||||
out->err = NULL;
|
||||
out->err_no = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void file_error_set(artifact_file_error_t *out, const char *err, int err_no) {
|
||||
if (out) {
|
||||
out->err = err;
|
||||
out->err_no = err_no;
|
||||
}
|
||||
}
|
||||
|
||||
/* Build path: <repo>/.codebase-memory/<name> into caller-owned buf. */
|
||||
static bool artifact_path(char *buf, size_t bufsz, const char *repo_path, const char *name) {
|
||||
int n = snprintf(buf, bufsz, "%s/%s/%s", repo_path, CBM_ARTIFACT_DIR, name);
|
||||
@@ -148,63 +130,26 @@ static char *read_file_alloc(const char *path, size_t *out_len) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Write buffer to file atomically (write to tmp, rename). Returns 0 on success. */
|
||||
static int write_file_atomic(const char *path, const char *data, size_t len,
|
||||
artifact_file_error_t *out_err) {
|
||||
file_error_clear(out_err);
|
||||
|
||||
char tmp[CBM_SZ_4K];
|
||||
int n = snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
||||
if (n < 0 || (size_t)n >= sizeof(tmp)) {
|
||||
file_error_set(out_err, "path_too_long", 0);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
FILE *fp = fopen(tmp, "wb");
|
||||
if (!fp) {
|
||||
file_error_set(out_err, "open_temp", errno);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
size_t wr = fwrite(data, ART_NUL, len, fp);
|
||||
if (wr != len) {
|
||||
int saved_errno = ferror(fp) ? errno : 0;
|
||||
(void)fclose(fp);
|
||||
cbm_unlink(tmp);
|
||||
file_error_set(out_err, "write_temp", saved_errno);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (fclose(fp) != 0) {
|
||||
int saved_errno = errno;
|
||||
cbm_unlink(tmp);
|
||||
file_error_set(out_err, "close_temp", saved_errno);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* MoveFileEx replace approach suggested by @Ayush7Ranjan in #492. */
|
||||
if (!MoveFileExA(tmp, path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
|
||||
DWORD saved_error = GetLastError();
|
||||
cbm_unlink(tmp);
|
||||
file_error_set(out_err, "rename_temp", (int)saved_error);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
#else
|
||||
if (rename(tmp, path) != 0) {
|
||||
int saved_errno = errno;
|
||||
cbm_unlink(tmp);
|
||||
file_error_set(out_err, "rename_temp", saved_errno);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get current git HEAD hash. buf must be >= CBM_SZ_64. Returns false on error. */
|
||||
static bool git_head_hash(const char *repo_path, char *buf, size_t bufsz) {
|
||||
if (!buf || bufsz == 0 || !cbm_validate_shell_arg(repo_path)) {
|
||||
if (buf && bufsz > 0) {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
char cmd[CBM_SZ_1K];
|
||||
snprintf(cmd, sizeof(cmd), "git -C '%s' rev-parse HEAD 2>/dev/null", repo_path);
|
||||
#ifdef _WIN32
|
||||
const char *null_dev = "NUL";
|
||||
#else
|
||||
const char *null_dev = "/dev/null";
|
||||
#endif
|
||||
int cmd_len = snprintf(cmd, sizeof(cmd), "git -C \"%s\" rev-parse HEAD 2>%s", repo_path,
|
||||
null_dev);
|
||||
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) {
|
||||
buf[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
FILE *fp = cbm_popen(cmd, "r");
|
||||
if (!fp) {
|
||||
buf[0] = '\0';
|
||||
@@ -239,7 +184,9 @@ static void iso_timestamp(char *buf, size_t bufsz) {
|
||||
/* Read schema_version from artifact.json. Returns -1 if missing/invalid. */
|
||||
static int read_metadata_version(const char *repo_path) {
|
||||
char meta_path[CBM_SZ_4K];
|
||||
artifact_path(meta_path, sizeof(meta_path), repo_path, CBM_ARTIFACT_META);
|
||||
if (!artifact_path(meta_path, sizeof(meta_path), repo_path, CBM_ARTIFACT_META)) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
char *json = read_file_alloc(meta_path, &len);
|
||||
@@ -263,7 +210,9 @@ static int read_metadata_version(const char *repo_path) {
|
||||
/* Read original_size from artifact.json. Returns 0 on error. */
|
||||
static size_t read_metadata_original_size(const char *repo_path) {
|
||||
char meta_path[CBM_SZ_4K];
|
||||
artifact_path(meta_path, sizeof(meta_path), repo_path, CBM_ARTIFACT_META);
|
||||
if (!artifact_path(meta_path, sizeof(meta_path), repo_path, CBM_ARTIFACT_META)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
char *json = read_file_alloc(meta_path, &len);
|
||||
@@ -319,11 +268,11 @@ static int write_metadata(const char *repo_path, const char *project_name, int n
|
||||
free(json);
|
||||
return artifact_export_fail("write_metadata", repo_path, "path_too_long", 0);
|
||||
}
|
||||
artifact_file_error_t ioerr;
|
||||
int rc = write_file_atomic(meta_path, json, json_len, &ioerr);
|
||||
cbm_file_error_t ioerr;
|
||||
int rc = cbm_write_file_atomic(meta_path, json, json_len, &ioerr);
|
||||
free(json);
|
||||
if (rc != 0) {
|
||||
return artifact_export_fail("write_metadata", meta_path, ioerr.err, ioerr.err_no);
|
||||
return artifact_export_fail("write_metadata", meta_path, ioerr.stage, ioerr.code);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
@@ -332,7 +281,10 @@ static int write_metadata(const char *repo_path, const char *project_name, int n
|
||||
|
||||
static void ensure_gitattributes(const char *repo_path) {
|
||||
char ga_path[CBM_SZ_4K];
|
||||
artifact_path(ga_path, sizeof(ga_path), repo_path, ".gitattributes");
|
||||
if (!artifact_path(ga_path, sizeof(ga_path), repo_path, ".gitattributes")) {
|
||||
cbm_log_warn("artifact.gitattributes.open path=%s err=%s", repo_path, "path_too_long");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Atomic create-only-if-absent: O_EXCL closes the TOCTOU window
|
||||
* between checking existence and writing. If the file exists, open
|
||||
@@ -356,9 +308,24 @@ static void ensure_gitattributes(const char *repo_path) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Best-effort: configure merge driver */
|
||||
/* Best-effort: configure merge driver. The popen fallback uses a shell, so
|
||||
* reuse the repository-wide argument validator before quoting repo_path. */
|
||||
if (!cbm_validate_shell_arg(repo_path)) {
|
||||
cbm_log_warn("artifact.merge_driver.skip", "reason", "unsafe_repo_path");
|
||||
return;
|
||||
}
|
||||
char cmd[CBM_SZ_1K];
|
||||
snprintf(cmd, sizeof(cmd), "git -C '%s' config merge.ours.driver true 2>/dev/null", repo_path);
|
||||
#ifdef _WIN32
|
||||
const char *null_dev = "NUL";
|
||||
#else
|
||||
const char *null_dev = "/dev/null";
|
||||
#endif
|
||||
int cmd_len = snprintf(cmd, sizeof(cmd), "git -C \"%s\" config merge.ours.driver true 2>%s",
|
||||
repo_path, null_dev);
|
||||
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) {
|
||||
cbm_log_warn("artifact.merge_driver.skip", "reason", "command_too_long");
|
||||
return;
|
||||
}
|
||||
FILE *p = cbm_popen(cmd, "r");
|
||||
if (p) {
|
||||
(void)cbm_pclose(p);
|
||||
@@ -384,7 +351,19 @@ static const char *DROP_INDEXES_SQL = "DROP INDEX IF EXISTS idx_nodes_label;"
|
||||
* VACUUM INTO → drop indexes → VACUUM. Returns malloc'd buffer or NULL. */
|
||||
static char *prepare_stripped_db(const char *db_path, size_t *out_size) {
|
||||
char tmp_path[CBM_SZ_4K];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s/cbm_artifact_tmp.db", cbm_tmpdir());
|
||||
int tmp_len = snprintf(tmp_path, sizeof(tmp_path), "%s/cbm_artifact_tmp_XXXXXX", cbm_tmpdir());
|
||||
if (tmp_len < 0 || (size_t)tmp_len >= sizeof(tmp_path)) {
|
||||
artifact_export_fail("prepare_temp_db", cbm_tmpdir(), "path_too_long", 0);
|
||||
return NULL;
|
||||
}
|
||||
int tmp_fd = cbm_mkstemp_s(tmp_path, sizeof(tmp_path));
|
||||
if (tmp_fd < 0) {
|
||||
artifact_export_fail("prepare_temp_db", tmp_path, "create_temp", errno);
|
||||
return NULL;
|
||||
}
|
||||
cbm_close_fd(tmp_fd);
|
||||
/* VACUUM INTO requires the destination file not to exist. The unique name
|
||||
* still prevents cross-process collision after this unlink. */
|
||||
cbm_unlink(tmp_path);
|
||||
|
||||
/* VACUUM INTO: clean compacted copy. Use raw sqlite3 to bypass store authorizer
|
||||
@@ -397,10 +376,15 @@ static char *prepare_stripped_db(const char *db_path, size_t *out_size) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char vacuum_sql[CBM_SZ_4K];
|
||||
snprintf(vacuum_sql, sizeof(vacuum_sql), "VACUUM INTO '%s';", tmp_path);
|
||||
char *vacuum_sql = sqlite3_mprintf("VACUUM INTO %Q;", tmp_path);
|
||||
if (!vacuum_sql) {
|
||||
artifact_export_fail("vacuum_into", tmp_path, "alloc_sql", 0);
|
||||
sqlite3_close(raw_db);
|
||||
return NULL;
|
||||
}
|
||||
char *errmsg = NULL;
|
||||
int vrc = sqlite3_exec(raw_db, vacuum_sql, NULL, NULL, &errmsg);
|
||||
sqlite3_free(vacuum_sql);
|
||||
sqlite3_close(raw_db);
|
||||
|
||||
if (vrc != SQLITE_OK) {
|
||||
@@ -427,10 +411,14 @@ static char *prepare_stripped_db(const char *db_path, size_t *out_size) {
|
||||
/* Clean up WAL/SHM from temp */
|
||||
char wal[CBM_SZ_4K];
|
||||
char shm[CBM_SZ_4K];
|
||||
snprintf(wal, sizeof(wal), "%s-wal", tmp_path);
|
||||
snprintf(shm, sizeof(shm), "%s-shm", tmp_path);
|
||||
cbm_unlink(wal);
|
||||
cbm_unlink(shm);
|
||||
int wal_len = snprintf(wal, sizeof(wal), "%s-wal", tmp_path);
|
||||
int shm_len = snprintf(shm, sizeof(shm), "%s-shm", tmp_path);
|
||||
if (wal_len >= 0 && (size_t)wal_len < sizeof(wal)) {
|
||||
cbm_unlink(wal);
|
||||
}
|
||||
if (shm_len >= 0 && (size_t)shm_len < sizeof(shm)) {
|
||||
cbm_unlink(shm);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -500,12 +488,12 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char *
|
||||
free(compressed);
|
||||
return artifact_export_fail("write_artifact", repo_path, "path_too_long", 0);
|
||||
}
|
||||
artifact_file_error_t ioerr;
|
||||
int wrc = write_file_atomic(zst_path, compressed, (size_t)clen, &ioerr);
|
||||
cbm_file_error_t ioerr;
|
||||
int wrc = cbm_write_file_atomic(zst_path, compressed, (size_t)clen, &ioerr);
|
||||
free(compressed);
|
||||
|
||||
if (wrc != 0) {
|
||||
return artifact_export_fail("write_artifact", zst_path, ioerr.err, ioerr.err_no);
|
||||
return artifact_export_fail("write_artifact", zst_path, ioerr.stage, ioerr.code);
|
||||
}
|
||||
|
||||
/* Get node/edge counts for metadata */
|
||||
@@ -561,7 +549,10 @@ int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
|
||||
|
||||
/* Read compressed artifact */
|
||||
char zst_path[CBM_SZ_4K];
|
||||
artifact_path(zst_path, sizeof(zst_path), repo_path, CBM_ARTIFACT_FILENAME);
|
||||
if (!artifact_path(zst_path, sizeof(zst_path), repo_path, CBM_ARTIFACT_FILENAME)) {
|
||||
cbm_log_error("artifact.import", "err", "artifact_path_too_long");
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
size_t clen = 0;
|
||||
char *compressed = read_file_alloc(zst_path, &clen);
|
||||
@@ -586,31 +577,49 @@ int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Write to temp file, then rename for atomicity */
|
||||
/* Write to a unique temp file, then rename for atomicity */
|
||||
char tmp_path[CBM_SZ_4K];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s.import_tmp", cache_db_path);
|
||||
int tmp_len = snprintf(tmp_path, sizeof(tmp_path), "%s.import.XXXXXX", cache_db_path);
|
||||
if (tmp_len < 0 || (size_t)tmp_len >= sizeof(tmp_path)) {
|
||||
free(decompressed);
|
||||
cbm_log_error("artifact.import", "err", "cache_path_too_long");
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
int tmp_fd = cbm_mkstemp_s(tmp_path, sizeof(tmp_path));
|
||||
if (tmp_fd < 0) {
|
||||
free(decompressed);
|
||||
cbm_log_error("artifact.import", "err", "create_temp_db", "path", tmp_path);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
cbm_close_fd(tmp_fd);
|
||||
|
||||
/* Ensure cache directory exists */
|
||||
char cache_dir[CBM_SZ_1K];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s", cache_db_path);
|
||||
int dir_len = snprintf(cache_dir, sizeof(cache_dir), "%s", cache_db_path);
|
||||
if (dir_len < 0 || (size_t)dir_len >= sizeof(cache_dir)) {
|
||||
free(decompressed);
|
||||
cbm_log_error("artifact.import", "err", "cache_dir_path_too_long");
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
char *last_slash = strrchr(cache_dir, '/');
|
||||
if (last_slash) {
|
||||
*last_slash = '\0';
|
||||
cbm_mkdir_p(cache_dir, ART_DIR_PERMS);
|
||||
}
|
||||
|
||||
artifact_file_error_t ioerr;
|
||||
int wrc = write_file_atomic(tmp_path, decompressed, (size_t)dlen, &ioerr);
|
||||
cbm_file_error_t ioerr;
|
||||
int wrc = cbm_write_file_atomic(tmp_path, decompressed, (size_t)dlen, &ioerr);
|
||||
free(decompressed);
|
||||
|
||||
if (wrc != 0) {
|
||||
if (ioerr.err_no != 0) {
|
||||
cbm_log_error("artifact.import", "err", "write_temp_db", "detail", ioerr.err, "errno",
|
||||
itoa_buf(ioerr.err_no), "path", tmp_path);
|
||||
if (ioerr.code != 0) {
|
||||
cbm_log_error("artifact.import", "err", "write_temp_db", "detail", ioerr.stage,
|
||||
"errno", itoa_buf(ioerr.code), "path", tmp_path);
|
||||
} else {
|
||||
cbm_log_error("artifact.import", "err", "write_temp_db", "detail", ioerr.err, "path",
|
||||
tmp_path);
|
||||
cbm_log_error("artifact.import", "err", "write_temp_db", "detail", ioerr.stage,
|
||||
"path", tmp_path);
|
||||
}
|
||||
cbm_unlink(tmp_path);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
@@ -632,9 +641,24 @@ int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
|
||||
|
||||
cbm_store_close(store);
|
||||
|
||||
/* Atomic rename to final path */
|
||||
if (rename(tmp_path, cache_db_path) != 0) {
|
||||
cbm_log_error("artifact.import", "err", "rename_to_cache");
|
||||
/* A stale WAL/SHM can describe the previous cache DB. Remove sidecars only
|
||||
* after the imported DB has passed integrity and immediately before publish. */
|
||||
char final_wal[CBM_SZ_4K];
|
||||
char final_shm[CBM_SZ_4K];
|
||||
int final_wal_len = snprintf(final_wal, sizeof(final_wal), "%s-wal", cache_db_path);
|
||||
int final_shm_len = snprintf(final_shm, sizeof(final_shm), "%s-shm", cache_db_path);
|
||||
if (final_wal_len >= 0 && (size_t)final_wal_len < sizeof(final_wal)) {
|
||||
cbm_unlink(final_wal);
|
||||
}
|
||||
if (final_shm_len >= 0 && (size_t)final_shm_len < sizeof(final_shm)) {
|
||||
cbm_unlink(final_shm);
|
||||
}
|
||||
|
||||
/* Preserve the Windows persistence fix from #400/#486: replacing an
|
||||
* existing cache DB must use MoveFileEx(REPLACE_EXISTING) via compat_fs. */
|
||||
int replace_error = 0;
|
||||
if (cbm_replace_file_ex(tmp_path, cache_db_path, &replace_error) != 0) {
|
||||
cbm_log_error("artifact.import", "err", "replace_cache", "errno", itoa_buf(replace_error));
|
||||
cbm_unlink(tmp_path);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
@@ -642,10 +666,14 @@ int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
|
||||
/* Clean up any stale WAL/SHM from the temp open */
|
||||
char wal[CBM_SZ_4K];
|
||||
char shm[CBM_SZ_4K];
|
||||
snprintf(wal, sizeof(wal), "%s-wal", tmp_path);
|
||||
snprintf(shm, sizeof(shm), "%s-shm", tmp_path);
|
||||
cbm_unlink(wal);
|
||||
cbm_unlink(shm);
|
||||
int wal_len = snprintf(wal, sizeof(wal), "%s-wal", tmp_path);
|
||||
int shm_len = snprintf(shm, sizeof(shm), "%s-shm", tmp_path);
|
||||
if (wal_len >= 0 && (size_t)wal_len < sizeof(wal)) {
|
||||
cbm_unlink(wal);
|
||||
}
|
||||
if (shm_len >= 0 && (size_t)shm_len < sizeof(shm)) {
|
||||
cbm_unlink(shm);
|
||||
}
|
||||
|
||||
cbm_log_info("artifact.import", "db", cache_db_path, "size_mb",
|
||||
itoa_buf((int)((size_t)dlen / ART_BYTES_PER_MB)));
|
||||
@@ -661,7 +689,9 @@ bool cbm_artifact_exists(const char *repo_path) {
|
||||
}
|
||||
|
||||
char zst_path[CBM_SZ_4K];
|
||||
artifact_path(zst_path, sizeof(zst_path), repo_path, CBM_ARTIFACT_FILENAME);
|
||||
if (!artifact_path(zst_path, sizeof(zst_path), repo_path, CBM_ARTIFACT_FILENAME)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(zst_path, &st) != 0 || st.st_size == 0) {
|
||||
@@ -681,7 +711,9 @@ char *cbm_artifact_commit(const char *repo_path) {
|
||||
}
|
||||
|
||||
char meta_path[CBM_SZ_4K];
|
||||
artifact_path(meta_path, sizeof(meta_path), repo_path, CBM_ARTIFACT_META);
|
||||
if (!artifact_path(meta_path, sizeof(meta_path), repo_path, CBM_ARTIFACT_META)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
char *json = read_file_alloc(meta_path, &len);
|
||||
|
||||
@@ -45,21 +45,23 @@
|
||||
* the textual callee_name as the last dot-separated segment. The
|
||||
* pointer returned aliases into `arr` and stays valid as long as the
|
||||
* underlying CBMFileResult is alive. */
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
|
||||
const CBMResolvedCallArray *arr, const CBMCall *call) {
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution_with_floor(
|
||||
const CBMResolvedCallArray *arr, const CBMCall *call, double confidence_floor) {
|
||||
if (!arr || arr->count == 0 || !call) {
|
||||
return NULL;
|
||||
}
|
||||
if (!call->enclosing_func_qn || !call->callee_name) {
|
||||
return NULL;
|
||||
}
|
||||
double floor =
|
||||
confidence_floor > 0.0 ? confidence_floor : (double)CBM_LSP_CONFIDENCE_FLOOR;
|
||||
const CBMResolvedCall *best = NULL;
|
||||
for (int i = 0; i < arr->count; i++) {
|
||||
const CBMResolvedCall *rc = &arr->items[i];
|
||||
if (!rc->caller_qn || !rc->callee_qn) {
|
||||
continue;
|
||||
}
|
||||
if (rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
if ((double)rc->confidence < floor) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(rc->caller_qn, call->enclosing_func_qn) != 0) {
|
||||
@@ -77,6 +79,11 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
|
||||
return best;
|
||||
}
|
||||
|
||||
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
|
||||
const CBMResolvedCallArray *arr, const CBMCall *call) {
|
||||
return cbm_pipeline_find_lsp_resolution_with_floor(arr, call, 0.0);
|
||||
}
|
||||
|
||||
/* Resolve an LSP-emitted callee_qn to a graph-buffer node.
|
||||
*
|
||||
* Per-file LSPs (notably py_lsp) sometimes emit `callee_qn` as the raw
|
||||
|
||||
@@ -186,6 +186,13 @@ static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca
|
||||
const cbm_gbuf_node_t *source_node, const char *module_qn,
|
||||
const char **imp_keys, const char **imp_vals, int imp_count) {
|
||||
const char *method = cbm_service_pattern_route_method(call->callee_name);
|
||||
/* Reject CLI slash-command args (e.g. "/ar:allow") that start with '/' and
|
||||
* so pass the caller's first_string_arg[0]=='/' check, but aren't valid
|
||||
* route paths. Same gate as pass_route_nodes.c — keeps command-syntax
|
||||
* strings from becoming spurious Route nodes. */
|
||||
if (!cbm_service_pattern_is_http_route_literal(call->first_string_arg, call->callee_name)) {
|
||||
return;
|
||||
}
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method ? method : "ANY",
|
||||
@@ -270,6 +277,16 @@ static void emit_http_async_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
|
||||
(url_or_topic[0] == '/' || strstr(url_or_topic, "://") != NULL));
|
||||
bool is_topic = (url_or_topic && url_or_topic[0] != '\0' && svc == CBM_SVC_ASYNC &&
|
||||
strlen(url_or_topic) > PAIR_LEN);
|
||||
/* An HTTP call whose URL isn't a valid route path (e.g. CLI "/ar:ok" from a
|
||||
* .get(...) accessor that the service-pattern matcher misclassified as
|
||||
* HTTP) must not become a spurious Route node. Drop the URL so we fall
|
||||
* through to the plain CALLS edge — the call relationship is preserved
|
||||
* without fabricating a route. (Async topics aren't route paths, so only
|
||||
* gate HTTP.) */
|
||||
if (svc == CBM_SVC_HTTP && is_url &&
|
||||
!cbm_service_pattern_is_http_route_literal(url_or_topic, call->callee_name)) {
|
||||
is_url = false;
|
||||
}
|
||||
if (!is_url && !is_topic) {
|
||||
char esc_callee[CBM_SZ_256];
|
||||
cbm_json_escape(esc_callee, sizeof(esc_callee), call->callee_name);
|
||||
@@ -366,7 +383,8 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
}
|
||||
|
||||
/* LSP-resolved calls take precedence over registry-textual matching. */
|
||||
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution(lsp_calls, call);
|
||||
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution_with_floor(
|
||||
lsp_calls, call, ctx->lsp_confidence_floor);
|
||||
if (lsp) {
|
||||
const cbm_gbuf_node_t *target_node =
|
||||
cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name, lsp->callee_qn);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "foundation/platform.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "service_patterns.h"
|
||||
|
||||
#include <sqlite3/sqlite3.h>
|
||||
#include <stdint.h>
|
||||
@@ -283,6 +284,9 @@ static int match_http_routes(cbm_store_t *src_store, const char *src_project,
|
||||
if (!url_path[0]) {
|
||||
continue;
|
||||
}
|
||||
if (!cbm_service_pattern_is_http_route_literal(url_path, NULL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Build the expected Route QN in the target project (param-canonicalized
|
||||
* so client url_path matches the server handler regardless of framework
|
||||
|
||||
@@ -315,6 +315,7 @@ typedef struct {
|
||||
cbm_change_coupling_t *out;
|
||||
int out_count;
|
||||
int max_out;
|
||||
double min_coupling_score;
|
||||
} collect_coupling_ctx_t;
|
||||
|
||||
static void collect_coupling_cb(const char *pair_key, void *val, void *ud) {
|
||||
@@ -353,7 +354,9 @@ static void collect_coupling_cb(const char *pair_key, void *val, void *ud) {
|
||||
}
|
||||
|
||||
double score = (double)co_count / (double)min_total;
|
||||
if (score < MIN_COUPLING_SCORE) {
|
||||
double min_score =
|
||||
cctx->min_coupling_score > 0.0 ? cctx->min_coupling_score : MIN_COUPLING_SCORE;
|
||||
if (score < min_score) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -366,8 +369,10 @@ static void collect_coupling_cb(const char *pair_key, void *val, void *ud) {
|
||||
cc->last_co_change = ts ? *ts : 0;
|
||||
}
|
||||
|
||||
int cbm_compute_change_coupling(const cbm_commit_files_t *commits, int commit_count,
|
||||
cbm_change_coupling_t *out, int max_out) {
|
||||
int cbm_compute_change_coupling_with_threshold(const cbm_commit_files_t *commits,
|
||||
int commit_count,
|
||||
cbm_change_coupling_t *out, int max_out,
|
||||
double min_coupling_score) {
|
||||
CBMHashTable *file_counts = cbm_ht_create(CBM_SZ_1K);
|
||||
CBMHashTable *pair_counts = cbm_ht_create(CBM_SZ_2K);
|
||||
/* Parallel table mapping pair_key → max commit timestamp seen for that
|
||||
@@ -438,6 +443,7 @@ int cbm_compute_change_coupling(const cbm_commit_files_t *commits, int commit_co
|
||||
.out = out,
|
||||
.out_count = 0,
|
||||
.max_out = max_out,
|
||||
.min_coupling_score = min_coupling_score,
|
||||
};
|
||||
cbm_ht_foreach(pair_counts, collect_coupling_cb, &cctx);
|
||||
|
||||
@@ -451,6 +457,11 @@ int cbm_compute_change_coupling(const cbm_commit_files_t *commits, int commit_co
|
||||
return cctx.out_count;
|
||||
}
|
||||
|
||||
int cbm_compute_change_coupling(const cbm_commit_files_t *commits, int commit_count,
|
||||
cbm_change_coupling_t *out, int max_out) {
|
||||
return cbm_compute_change_coupling_with_threshold(commits, commit_count, out, max_out, 0.0);
|
||||
}
|
||||
|
||||
/* ── Split pass: compute (I/O-bound) + apply (gbuf writes) ───────── */
|
||||
|
||||
/* Pre-computed coupling result buffer for fused post-pass parallelism. */
|
||||
@@ -459,7 +470,9 @@ int cbm_compute_change_coupling(const cbm_commit_files_t *commits, int commit_co
|
||||
|
||||
/* Compute change couplings without touching the graph buffer.
|
||||
* Can run on a separate thread while other passes use the gbuf. */
|
||||
int cbm_pipeline_githistory_compute(const char *repo_path, cbm_githistory_result_t *result) {
|
||||
int cbm_pipeline_githistory_compute_with_threshold(const char *repo_path,
|
||||
cbm_githistory_result_t *result,
|
||||
double min_coupling_score) {
|
||||
result->couplings = NULL;
|
||||
result->count = 0;
|
||||
result->commit_count = 0;
|
||||
@@ -492,7 +505,8 @@ int cbm_pipeline_githistory_compute(const char *repo_path, cbm_githistory_result
|
||||
}
|
||||
|
||||
cbm_change_coupling_t *couplings = malloc(MAX_COUPLINGS * sizeof(cbm_change_coupling_t));
|
||||
int coupling_count = cbm_compute_change_coupling(cf, commit_count, couplings, MAX_COUPLINGS);
|
||||
int coupling_count = cbm_compute_change_coupling_with_threshold(
|
||||
cf, commit_count, couplings, MAX_COUPLINGS, min_coupling_score);
|
||||
|
||||
/* Per-file temporal aggregation: change_count + last_modified.
|
||||
* Single hash-table pass over the same commit set used for coupling so
|
||||
@@ -543,6 +557,10 @@ int cbm_pipeline_githistory_compute(const char *repo_path, cbm_githistory_result
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cbm_pipeline_githistory_compute(const char *repo_path, cbm_githistory_result_t *result) {
|
||||
return cbm_pipeline_githistory_compute_with_threshold(repo_path, result, 0.0);
|
||||
}
|
||||
|
||||
/* Apply pre-computed couplings to the graph buffer (must be on main thread). */
|
||||
int cbm_pipeline_githistory_apply(cbm_pipeline_ctx_t *ctx, const cbm_githistory_result_t *result) {
|
||||
int edge_count = 0;
|
||||
@@ -609,7 +627,8 @@ int cbm_pipeline_pass_githistory(cbm_pipeline_ctx_t *ctx) {
|
||||
cbm_log_info("pass.start", "pass", "githistory");
|
||||
|
||||
cbm_githistory_result_t result = {0};
|
||||
cbm_pipeline_githistory_compute(ctx->repo_path, &result);
|
||||
cbm_pipeline_githistory_compute_with_threshold(ctx->repo_path, &result,
|
||||
ctx->githistory_min_coupling);
|
||||
|
||||
int edge_count = 0;
|
||||
if (result.count > 0 || result.file_temporal_count > 0) {
|
||||
|
||||
@@ -1030,8 +1030,10 @@ static int match_and_link(cbm_pipeline_ctx_t *ctx, cbm_route_handler_t *routes,
|
||||
}
|
||||
|
||||
/* Score path match */
|
||||
double min_conf = ctx->httplink_min_confidence > 0.0 ? ctx->httplink_min_confidence
|
||||
: MIN_PATH_CONFIDENCE;
|
||||
double score = cbm_path_match_score(cs->path, rh->path);
|
||||
if (score < MIN_PATH_CONFIDENCE) {
|
||||
if (score < min_conf) {
|
||||
continue; /* minimum confidence threshold */
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 };
|
||||
#include "foundation/compat_regex.h"
|
||||
#include "cbm.h"
|
||||
#include "simhash/minhash.h"
|
||||
|
||||
#include "semantic/ast_profile.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
@@ -499,7 +500,8 @@ static void insert_def_into_gbuf(extract_worker_state_t *ws, const cbm_file_info
|
||||
def->qualified_name, def->file_path ? def->file_path : fi->rel_path,
|
||||
(int)def->start_line, (int)def->end_line, props);
|
||||
ws->nodes_created++;
|
||||
if (def->route_path && def->route_path[0] != '\0') {
|
||||
if (def->route_path && def->route_path[0] != '\0' &&
|
||||
cbm_service_pattern_is_http_route_literal(def->route_path, NULL)) {
|
||||
const char *rm = def->route_method ? def->route_method : "ANY";
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
char cpath[CBM_SZ_256];
|
||||
@@ -1003,6 +1005,7 @@ typedef struct {
|
||||
CBMFileResult **result_cache;
|
||||
const cbm_gbuf_t *main_gbuf; /* READ-ONLY during Phase 4 */
|
||||
const cbm_registry_t *registry; /* READ-ONLY during Phase 4 */
|
||||
double lsp_confidence_floor;
|
||||
_Atomic int64_t *shared_ids;
|
||||
_Atomic int *cancelled;
|
||||
_Atomic int next_file_idx;
|
||||
@@ -1245,6 +1248,11 @@ static void emit_http_async_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t
|
||||
(svc == CBM_SVC_HTTP) ? cbm_service_pattern_http_method(call->callee_name) : NULL;
|
||||
const char *broker =
|
||||
(svc == CBM_SVC_ASYNC) ? cbm_service_pattern_broker(res->qualified_name) : NULL;
|
||||
/* An HTTP call whose URL isn't a valid route path (CLI slash-command syntax
|
||||
* or a filesystem path) must not become a spurious Route node. */
|
||||
if (svc == CBM_SVC_HTTP && !cbm_service_pattern_is_http_route_literal(arg, call->callee_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t route_id = build_service_route(gbuf, arg, method, broker, svc);
|
||||
|
||||
@@ -1299,6 +1307,10 @@ static void emit_route_registration(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *sou
|
||||
const cbm_registry_t *registry, const cbm_gbuf_t *main_gbuf,
|
||||
const char **ik, const char **iv, int ic) {
|
||||
const char *method = cbm_service_pattern_route_method(call->callee_name);
|
||||
/* Reject CLI slash-command / filesystem-path args masquerading as routes. */
|
||||
if (!cbm_service_pattern_is_http_route_literal(route_path, call->callee_name)) {
|
||||
return;
|
||||
}
|
||||
char rqn[CBM_ROUTE_QN_SIZE];
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(rqn, sizeof(rqn), "__route__%s__%s", method ? method : "ANY",
|
||||
@@ -1382,8 +1394,12 @@ static bool normalize_url_arg(const char *url, char *norm, int norm_sz) {
|
||||
}
|
||||
|
||||
/* Detect API paths in call arguments and create HTTP_CALLS edges. */
|
||||
static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
|
||||
const CBMCall *call) {
|
||||
static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
|
||||
const char *rel, CBMLanguage lang) {
|
||||
const char *source_path = source && source->file_path && source->file_path[0] ? source->file_path : rel;
|
||||
if (cbm_is_test_file(source_path, lang)) {
|
||||
return;
|
||||
}
|
||||
for (int ai = 0; ai < call->arg_count; ai++) {
|
||||
const CBMCallArg *ca = &call->args[ai];
|
||||
const char *url = ca->value ? ca->value : ca->expr;
|
||||
@@ -1394,11 +1410,15 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
|
||||
if (!normalize_url_arg(url, norm, (int)sizeof(norm))) {
|
||||
continue;
|
||||
}
|
||||
if (!cbm_service_pattern_is_http_route_literal(norm, call->callee_name)) {
|
||||
continue;
|
||||
}
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s",
|
||||
cbm_route_canon_path(norm, cpath, sizeof(cpath)));
|
||||
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn, "", 0, 0,
|
||||
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn,
|
||||
source_path ? source_path : "", 0, 0,
|
||||
"{\"source\":\"arg_url\"}");
|
||||
char esc_c[CBM_SZ_256];
|
||||
char esc_n[CBM_SZ_256];
|
||||
@@ -1590,7 +1610,8 @@ static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
|
||||
const cbm_gbuf_node_t *target, const CBMCall *call,
|
||||
const cbm_resolution_t *res, const char *module_qn,
|
||||
const cbm_registry_t *registry, const cbm_gbuf_t *main_gbuf,
|
||||
const char **imp_keys, const char **imp_vals, int imp_count) {
|
||||
const char **imp_keys, const char **imp_vals, int imp_count,
|
||||
const char *rel, CBMLanguage lang) {
|
||||
cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name);
|
||||
const char *arg = call->first_string_arg;
|
||||
|
||||
@@ -1640,7 +1661,7 @@ static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
|
||||
emit_normal_calls_edge(gbuf, source, target, call, res);
|
||||
}
|
||||
|
||||
detect_url_in_args(gbuf, source, call);
|
||||
detect_url_in_args(gbuf, source, call, rel, lang);
|
||||
}
|
||||
|
||||
/* Find the source node for an edge: enclosing function or file node. */
|
||||
@@ -1738,8 +1759,11 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
if (lsp_idx) {
|
||||
for (int i = 0; i < result->resolved_calls.count; i++) {
|
||||
CBMResolvedCall *rc_e = &result->resolved_calls.items[i];
|
||||
double lsp_floor = rc->lsp_confidence_floor > 0.0
|
||||
? rc->lsp_confidence_floor
|
||||
: (double)CBM_LSP_CONFIDENCE_FLOOR;
|
||||
if (!rc_e->caller_qn || !rc_e->callee_qn ||
|
||||
rc_e->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
|
||||
(double)rc_e->confidence < lsp_floor) {
|
||||
continue;
|
||||
}
|
||||
const char *short_name = strrchr(rc_e->callee_qn, '.');
|
||||
@@ -1798,7 +1822,8 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
/* Fallback to the linear scan for edge cases the index may
|
||||
* miss (e.g. callee_name that wasn't the registered short
|
||||
* name). Keeps semantics identical. */
|
||||
lsp = cbm_pipeline_find_lsp_resolution(&result->resolved_calls, call);
|
||||
lsp = cbm_pipeline_find_lsp_resolution_with_floor(
|
||||
&result->resolved_calls, call, rc->lsp_confidence_floor);
|
||||
}
|
||||
atomic_fetch_add_explicit(&rc->time_ns_rc_lsp_lookup, extract_now_ns() - _rc_t0,
|
||||
memory_order_relaxed);
|
||||
@@ -1852,7 +1877,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
.strategy = "callee_suffix"};
|
||||
emit_service_edge(ws->local_edge_buf, source_node, source_node, call, &fake_res,
|
||||
module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals,
|
||||
imp_count);
|
||||
imp_count, rel, lang);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1874,7 +1899,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
}
|
||||
_rc_t0 = extract_now_ns();
|
||||
emit_service_edge(ws->local_edge_buf, source_node, target_node, call, &res, module_qn,
|
||||
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count);
|
||||
rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count, rel, lang);
|
||||
atomic_fetch_add_explicit(&rc->time_ns_rc_emit, extract_now_ns() - _rc_t0,
|
||||
memory_order_relaxed);
|
||||
ws->calls_resolved++;
|
||||
@@ -2442,6 +2467,7 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
.result_cache = result_cache,
|
||||
.main_gbuf = ctx->gbuf,
|
||||
.registry = ctx->registry,
|
||||
.lsp_confidence_floor = ctx->lsp_confidence_floor,
|
||||
.shared_ids = shared_ids,
|
||||
.cancelled = ctx->cancelled,
|
||||
.all_defs = all_defs,
|
||||
|
||||
@@ -33,12 +33,11 @@ enum {
|
||||
#include <stdint.h>
|
||||
#include "graph_buffer/graph_buffer.h"
|
||||
#include "foundation/log.h"
|
||||
#include "service_patterns.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name);
|
||||
|
||||
/* True for characters that may appear in a ":name" route parameter. */
|
||||
static inline bool is_route_ident_char(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
|
||||
@@ -446,6 +445,12 @@ static int ensure_one_decorator_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *fun
|
||||
if (path[0] != '/') {
|
||||
return 0;
|
||||
}
|
||||
/* Reject CLI slash-command paths (e.g. "/ar:ok") extracted from
|
||||
* command-registry/decorator-like call sites. Same gate as the
|
||||
* HTTP_CALLS path above — a real route's ':' is at segment start. */
|
||||
if (!cbm_service_pattern_is_http_route_literal(path, NULL)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char method[CBM_SZ_16] = "ANY";
|
||||
extract_json_prop(func->properties_json, "route_method", method, sizeof(method));
|
||||
|
||||
@@ -1199,6 +1199,9 @@ int cbm_pipeline_pass_semantic_edges(cbm_pipeline_ctx_t *ctx) {
|
||||
|
||||
cbm_gbuf_t *gbuf = ctx->gbuf;
|
||||
cbm_sem_config_t cfg = cbm_sem_get_config();
|
||||
if (ctx->semantic_threshold > 0.0) {
|
||||
cfg.threshold = (float)ctx->semantic_threshold;
|
||||
}
|
||||
|
||||
CBM_PROF_START(t_phase1a);
|
||||
cbm_sem_func_t *funcs = NULL;
|
||||
|
||||
+82
-20
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
#include "foundation/constants.h"
|
||||
|
||||
enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6, PL_WAL_BUF = 1040 };
|
||||
enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6 };
|
||||
#define PL_NSEC_PER_SEC 1000000000LL
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "pipeline/artifact.h"
|
||||
@@ -47,6 +47,10 @@ static inline void *intptr_to_ptr(intptr_t v) {
|
||||
return p;
|
||||
}
|
||||
|
||||
static double pipeline_unit_threshold(double threshold) {
|
||||
return (threshold > 0.0 && threshold <= 1.0) ? threshold : 0.0;
|
||||
}
|
||||
|
||||
/* ── Global index lock ─────────────────────────────────────────── */
|
||||
/* Prevents concurrent pipeline runs on the same DB file.
|
||||
* Atomic spinlock: 0 = free, 1 = locked. */
|
||||
@@ -79,6 +83,10 @@ struct cbm_pipeline {
|
||||
char *branch_qn;
|
||||
cbm_index_mode_t mode;
|
||||
double similarity_threshold; /* Jaccard threshold for SIMILAR edges; <=0 = default (#41) */
|
||||
double httplink_min_confidence;
|
||||
double semantic_threshold;
|
||||
double githistory_min_coupling;
|
||||
double lsp_confidence_floor;
|
||||
atomic_int cancelled;
|
||||
cbm_store_t *flush_store; /* when set, use flush_to_store instead of dump_to_sqlite */
|
||||
bool persistence; /* write .codebase-memory/graph.db.zst after indexing */
|
||||
@@ -160,6 +168,10 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path,
|
||||
p->branch_qn = cbm_git_context_branch_qn(p->project_name, &p->git_ctx);
|
||||
p->mode = mode;
|
||||
p->similarity_threshold = 0.0; /* 0 = use CBM_MINHASH_JACCARD_THRESHOLD default */
|
||||
p->httplink_min_confidence = 0.0;
|
||||
p->semantic_threshold = 0.0;
|
||||
p->githistory_min_coupling = 0.0;
|
||||
p->lsp_confidence_floor = 0.0;
|
||||
p->persistence = false;
|
||||
p->committed_nodes = -1;
|
||||
p->committed_edges = -1;
|
||||
@@ -184,10 +196,54 @@ void cbm_pipeline_set_flush_store(cbm_pipeline_t *p, cbm_store_t *store) {
|
||||
* Must be called before cbm_pipeline_run(). */
|
||||
void cbm_pipeline_set_similarity_threshold(cbm_pipeline_t *p, double threshold) {
|
||||
if (p) {
|
||||
p->similarity_threshold = threshold;
|
||||
p->similarity_threshold = pipeline_unit_threshold(threshold);
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_pipeline_set_httplink_min_confidence(cbm_pipeline_t *p, double threshold) {
|
||||
if (p) {
|
||||
p->httplink_min_confidence = pipeline_unit_threshold(threshold);
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_pipeline_set_semantic_threshold(cbm_pipeline_t *p, double threshold) {
|
||||
if (p) {
|
||||
p->semantic_threshold = pipeline_unit_threshold(threshold);
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_pipeline_set_githistory_min_coupling(cbm_pipeline_t *p, double threshold) {
|
||||
if (p) {
|
||||
p->githistory_min_coupling = pipeline_unit_threshold(threshold);
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_pipeline_set_lsp_confidence_floor(cbm_pipeline_t *p, double threshold) {
|
||||
if (p) {
|
||||
p->lsp_confidence_floor = pipeline_unit_threshold(threshold);
|
||||
}
|
||||
}
|
||||
|
||||
double cbm_pipeline_httplink_min_confidence(const cbm_pipeline_t *p) {
|
||||
return p ? p->httplink_min_confidence : 0.0;
|
||||
}
|
||||
|
||||
double cbm_pipeline_similarity_threshold(const cbm_pipeline_t *p) {
|
||||
return p ? p->similarity_threshold : 0.0;
|
||||
}
|
||||
|
||||
double cbm_pipeline_semantic_threshold(const cbm_pipeline_t *p) {
|
||||
return p ? p->semantic_threshold : 0.0;
|
||||
}
|
||||
|
||||
double cbm_pipeline_githistory_min_coupling(const cbm_pipeline_t *p) {
|
||||
return p ? p->githistory_min_coupling : 0.0;
|
||||
}
|
||||
|
||||
double cbm_pipeline_lsp_confidence_floor(const cbm_pipeline_t *p) {
|
||||
return p ? p->lsp_confidence_floor : 0.0;
|
||||
}
|
||||
|
||||
void cbm_pipeline_set_persistence(cbm_pipeline_t *p, bool enabled) {
|
||||
if (p) {
|
||||
p->persistence = enabled;
|
||||
@@ -443,11 +499,13 @@ static int pass_structure(cbm_pipeline_t *p, const cbm_file_info_t *files, int f
|
||||
typedef struct {
|
||||
const char *repo_path;
|
||||
cbm_githistory_result_t *result;
|
||||
double min_coupling_score;
|
||||
} gh_compute_arg_t;
|
||||
|
||||
static void *gh_compute_thread_fn(void *arg) {
|
||||
gh_compute_arg_t *a = arg;
|
||||
cbm_pipeline_githistory_compute(a->repo_path, a->result);
|
||||
cbm_pipeline_githistory_compute_with_threshold(a->repo_path, a->result,
|
||||
a->min_coupling_score);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -809,7 +867,7 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
|
||||
/* Try incremental pipeline or delete old DB for reindex.
|
||||
* Returns >= 0 if incremental was used (the return code), or -1 to proceed with full. */
|
||||
static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *files, int file_count) {
|
||||
static int try_incremental_or_reindex(cbm_pipeline_t *p, cbm_file_info_t *files, int file_count) {
|
||||
char *db_path = resolve_db_path(p);
|
||||
if (!db_path) {
|
||||
return CBM_NOT_FOUND;
|
||||
@@ -840,14 +898,7 @@ static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *file
|
||||
} else if (check_store) {
|
||||
cbm_store_close(check_store);
|
||||
}
|
||||
cbm_log_info("pipeline.route", "path", "reindex", "action", "deleting old db");
|
||||
cbm_unlink(db_path);
|
||||
char wal[PL_WAL_BUF];
|
||||
char shm[PL_WAL_BUF];
|
||||
snprintf(wal, sizeof(wal), "%s-wal", db_path);
|
||||
snprintf(shm, sizeof(shm), "%s-shm", db_path);
|
||||
cbm_unlink(wal);
|
||||
cbm_unlink(shm);
|
||||
cbm_log_info("pipeline.route", "path", "reindex", "action", "atomic_rewrite");
|
||||
free(db_path);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
@@ -865,7 +916,11 @@ static int run_githistory(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx) {
|
||||
cbm_githistory_result_t gh_result = {0};
|
||||
cbm_thread_t gh_thread;
|
||||
bool gh_threaded = false;
|
||||
gh_compute_arg_t gh_arg = {.repo_path = ctx->repo_path, .result = &gh_result};
|
||||
gh_compute_arg_t gh_arg = {
|
||||
.repo_path = ctx->repo_path,
|
||||
.result = &gh_result,
|
||||
.min_coupling_score = ctx->githistory_min_coupling,
|
||||
};
|
||||
|
||||
if (p->mode != CBM_MODE_FAST) {
|
||||
if (cbm_default_worker_count(true) > SKIP_ONE) {
|
||||
@@ -874,7 +929,8 @@ static int run_githistory(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx) {
|
||||
}
|
||||
}
|
||||
if (!gh_threaded) {
|
||||
cbm_pipeline_githistory_compute(ctx->repo_path, &gh_result);
|
||||
cbm_pipeline_githistory_compute_with_threshold(ctx->repo_path, &gh_result,
|
||||
ctx->githistory_min_coupling);
|
||||
cbm_log_info("pass.timing", "pass", "githistory_compute", "elapsed_ms",
|
||||
itoa_buf((int)elapsed_ms(t_gh)));
|
||||
}
|
||||
@@ -1002,7 +1058,7 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
|
||||
* path uses an in-memory store and never writes a DB file, so there is no
|
||||
* old DB to consult or delete. */
|
||||
if (!p->flush_store) {
|
||||
rc = try_incremental_or_delete_db(p, files, file_count);
|
||||
rc = try_incremental_or_reindex(p, files, file_count);
|
||||
if (rc >= 0) {
|
||||
cbm_discover_free(files, file_count);
|
||||
return rc;
|
||||
@@ -1027,6 +1083,10 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
|
||||
.cancelled = &p->cancelled,
|
||||
.mode = (int)p->mode,
|
||||
.similarity_threshold = p->similarity_threshold,
|
||||
.httplink_min_confidence = p->httplink_min_confidence,
|
||||
.semantic_threshold = p->semantic_threshold,
|
||||
.githistory_min_coupling = p->githistory_min_coupling,
|
||||
.lsp_confidence_floor = p->lsp_confidence_floor,
|
||||
.path_aliases = path_aliases,
|
||||
};
|
||||
|
||||
@@ -1095,16 +1155,18 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
|
||||
if (!check_cancel(p)) {
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
|
||||
const char *home = cbm_get_home_dir();
|
||||
char db_path[1024];
|
||||
if (p->db_path) {
|
||||
snprintf(db_path, sizeof(db_path), "%s", p->db_path);
|
||||
} else {
|
||||
if (!home) {
|
||||
home = cbm_tmpdir();
|
||||
/* Honor CBM_CACHE_DIR (via cbm_resolve_cache_dir) so tests and
|
||||
* isolated runs don't write into the user's real store. Falls back
|
||||
* to the system tmp dir only if no cache dir can be resolved. */
|
||||
const char *cdir = cbm_resolve_cache_dir();
|
||||
if (!cdir) {
|
||||
cdir = cbm_tmpdir();
|
||||
}
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/%s.db", home,
|
||||
p->project_name);
|
||||
snprintf(db_path, sizeof(db_path), "%s/%s.db", cdir, p->project_name);
|
||||
}
|
||||
|
||||
/* Ensure parent directory exists (e.g. ~/.cache/codebase-memory-mcp/) */
|
||||
|
||||
@@ -79,6 +79,15 @@ void cbm_pipeline_set_flush_store(cbm_pipeline_t *p, cbm_store_t *store);
|
||||
/* Set the Jaccard similarity threshold for SIMILAR-edge creation (pass_similarity).
|
||||
* <=0 (or unset) uses the CBM_MINHASH_JACCARD_THRESHOLD default. Before run(). */
|
||||
void cbm_pipeline_set_similarity_threshold(cbm_pipeline_t *p, double threshold);
|
||||
void cbm_pipeline_set_httplink_min_confidence(cbm_pipeline_t *p, double threshold);
|
||||
void cbm_pipeline_set_semantic_threshold(cbm_pipeline_t *p, double threshold);
|
||||
void cbm_pipeline_set_githistory_min_coupling(cbm_pipeline_t *p, double threshold);
|
||||
void cbm_pipeline_set_lsp_confidence_floor(cbm_pipeline_t *p, double threshold);
|
||||
double cbm_pipeline_similarity_threshold(const cbm_pipeline_t *p);
|
||||
double cbm_pipeline_httplink_min_confidence(const cbm_pipeline_t *p);
|
||||
double cbm_pipeline_semantic_threshold(const cbm_pipeline_t *p);
|
||||
double cbm_pipeline_githistory_min_coupling(const cbm_pipeline_t *p);
|
||||
double cbm_pipeline_lsp_confidence_floor(const cbm_pipeline_t *p);
|
||||
|
||||
/* Get the project name derived from repo_path. Returned string is
|
||||
* owned by the pipeline. Valid until cbm_pipeline_free(). */
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
#include "foundation/constants.h"
|
||||
|
||||
enum { INCR_RING_BUF = 4, INCR_RING_MASK = 3, INCR_TS_BUF = 24, INCR_WAL_BUF = 1040 };
|
||||
enum { INCR_RING_BUF = 4, INCR_RING_MASK = 3, INCR_TS_BUF = 24 };
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "pipeline/artifact.h"
|
||||
#include <stdio.h>
|
||||
@@ -634,7 +634,7 @@ static void run_postpasses(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed_fil
|
||||
itoa_buf_incr((int)elapsed_ms_incr(t)));
|
||||
}
|
||||
}
|
||||
/* Delete old DB and dump merged graph + hashes to disk.
|
||||
/* Atomically dump merged graph + hashes to disk.
|
||||
* Mode-skipped hash rows are preserved across the rebuild so subsequent
|
||||
* reindexes can correctly distinguish "never indexed" from "indexed but
|
||||
* not visited this pass". */
|
||||
@@ -645,14 +645,6 @@ static void dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *
|
||||
struct timespec t;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
|
||||
cbm_unlink(db_path);
|
||||
char wal[INCR_WAL_BUF];
|
||||
char shm[INCR_WAL_BUF];
|
||||
snprintf(wal, sizeof(wal), "%s-wal", db_path);
|
||||
snprintf(shm, sizeof(shm), "%s-shm", db_path);
|
||||
cbm_unlink(wal);
|
||||
cbm_unlink(shm);
|
||||
|
||||
int dump_rc = cbm_gbuf_dump_to_sqlite(gbuf, db_path);
|
||||
cbm_log_info("incremental.dump", "rc", itoa_buf_incr(dump_rc), "elapsed_ms",
|
||||
itoa_buf_incr((int)elapsed_ms_incr(t)));
|
||||
@@ -842,6 +834,11 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
|
||||
.registry = registry,
|
||||
.cancelled = cbm_pipeline_cancelled_ptr(p),
|
||||
.mode = cbm_pipeline_get_mode(p),
|
||||
.similarity_threshold = cbm_pipeline_similarity_threshold(p),
|
||||
.httplink_min_confidence = cbm_pipeline_httplink_min_confidence(p),
|
||||
.semantic_threshold = cbm_pipeline_semantic_threshold(p),
|
||||
.githistory_min_coupling = cbm_pipeline_githistory_min_coupling(p),
|
||||
.lsp_confidence_floor = cbm_pipeline_lsp_confidence_floor(p),
|
||||
.path_aliases = path_aliases,
|
||||
};
|
||||
|
||||
|
||||
@@ -68,6 +68,10 @@ typedef struct {
|
||||
int mode; /* cbm_index_mode_t (0=full, 1=moderate, 2=fast, 3=advanced) */
|
||||
double similarity_threshold; /* Jaccard threshold for SIMILAR edges; <=0 means
|
||||
* use the CBM_MINHASH_JACCARD_THRESHOLD default (#41). */
|
||||
double httplink_min_confidence; /* <=0 uses httplink pass default 0.25 */
|
||||
double semantic_threshold; /* <=0 uses semantic default 0.75 */
|
||||
double githistory_min_coupling; /* <=0 uses git-history default 0.3 */
|
||||
double lsp_confidence_floor; /* <=0 uses LSP default 0.6 */
|
||||
|
||||
/* Extraction result cache (sequential pipeline optimization).
|
||||
* When non-NULL, pass_definitions stores results here instead of freeing,
|
||||
@@ -189,6 +193,10 @@ typedef struct {
|
||||
* Caller owns out[]. */
|
||||
int cbm_compute_change_coupling(const cbm_commit_files_t *commits, int commit_count,
|
||||
cbm_change_coupling_t *out, int max_out);
|
||||
int cbm_compute_change_coupling_with_threshold(const cbm_commit_files_t *commits,
|
||||
int commit_count,
|
||||
cbm_change_coupling_t *out, int max_out,
|
||||
double min_coupling_score);
|
||||
|
||||
/* Go-style implicit interface satisfaction on graph buffer.
|
||||
* Finds Interface nodes, matches method sets against Class nodes,
|
||||
@@ -494,6 +502,9 @@ typedef struct {
|
||||
/* Compute change couplings without touching the graph buffer.
|
||||
* Can run on a separate thread while other passes use the gbuf. */
|
||||
int cbm_pipeline_githistory_compute(const char *repo_path, cbm_githistory_result_t *result);
|
||||
int cbm_pipeline_githistory_compute_with_threshold(const char *repo_path,
|
||||
cbm_githistory_result_t *result,
|
||||
double min_coupling_score);
|
||||
|
||||
/* Apply pre-computed couplings to the graph buffer (main thread only). */
|
||||
int cbm_pipeline_githistory_apply(cbm_pipeline_ctx_t *ctx, const cbm_githistory_result_t *result);
|
||||
|
||||
+133
-32
@@ -32,8 +32,12 @@ enum {
|
||||
ST_RETRY_WAIT_US = 1000,
|
||||
ST_INIT_CAP_8 = 8,
|
||||
ST_INIT_CAP_16 = 16,
|
||||
ST_SQLITE_BUSY_TIMEOUT_MS = 10000,
|
||||
ST_SQL_BUF = 8192,
|
||||
ST_MAX_ROW_CHECK = 5,
|
||||
ST_ARCH_ROUTE_RESULT_LIMIT = 20,
|
||||
ST_ARCH_ROUTE_SCAN_LIMIT = ST_ARCH_ROUTE_RESULT_LIMIT * 10,
|
||||
ST_ARCH_ENTRY_POINT_LIMIT = 20,
|
||||
ST_ARCH_DOC_LIMIT = 20,
|
||||
ST_QN_MAX_DOTS = 5,
|
||||
ST_QN_MIN_DOTS = 3,
|
||||
ST_IN_CLAUSE_MARGIN = 4,
|
||||
@@ -57,8 +61,10 @@ enum {
|
||||
|
||||
#define SLEN(s) (sizeof(s) - 1)
|
||||
#include "store/store.h"
|
||||
#include "service_patterns.h"
|
||||
#include "foundation/platform.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/compat_regex.h"
|
||||
#include "foundation/str_util.h"
|
||||
@@ -376,7 +382,10 @@ static int configure_pragmas(cbm_store_t *s, bool in_memory) {
|
||||
/* busy_timeout must be set BEFORE journal_mode=WAL so that lock
|
||||
* contention during WAL mode activation is handled with a timeout
|
||||
* rather than an immediate SQLITE_BUSY error. */
|
||||
rc = exec_sql(s, "PRAGMA busy_timeout = 10000;");
|
||||
char busy_sql[ST_BUF_64];
|
||||
snprintf(busy_sql, sizeof(busy_sql), "PRAGMA busy_timeout = %d;",
|
||||
ST_SQLITE_BUSY_TIMEOUT_MS);
|
||||
rc = exec_sql(s, busy_sql);
|
||||
if (rc != CBM_STORE_OK) {
|
||||
return rc;
|
||||
}
|
||||
@@ -711,9 +720,10 @@ bool cbm_store_check_integrity_full(cbm_store_t *s, bool *path_only_failure) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Each project gets its own .db file, so the projects table should have
|
||||
* exactly 1 row. More than 5 rows is definitely corrupt (allows some slack
|
||||
* for edge cases). Also check that root_path looks like a real path. */
|
||||
/* The writer creates one projects row initially, then dependency indexing
|
||||
* may add rows for projects stored in the parent DB. Treat the table as
|
||||
* sane if it is readable and non-empty; row count alone is not a corruption
|
||||
* signal. Real B1 corruption is caught below by malformed root_path values. */
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int rc =
|
||||
sqlite3_prepare_v2(s->db, "SELECT count(*) FROM projects;", CBM_NOT_FOUND, &stmt, NULL);
|
||||
@@ -725,12 +735,14 @@ bool cbm_store_check_integrity_full(cbm_store_t *s, bool *path_only_failure) {
|
||||
bool rows_ok = true;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int row_count = sqlite3_column_int(stmt, 0);
|
||||
if (row_count > ST_MAX_ROW_CHECK) {
|
||||
(void)fprintf(stderr, "ERROR store.corrupt table=projects rows=%d (expected 1)\n",
|
||||
row_count);
|
||||
if (row_count < 0) {
|
||||
(void)fprintf(stderr, "ERROR store.corrupt table=projects rows=%d\n", row_count);
|
||||
ok = false;
|
||||
rows_ok = false;
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
rows_ok = false;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
@@ -786,7 +798,10 @@ cbm_store_t *cbm_store_open(const char *project) {
|
||||
cdir = cbm_tmpdir();
|
||||
}
|
||||
char path[CBM_SZ_1K];
|
||||
snprintf(path, sizeof(path), "%s/%s.db", cdir, project);
|
||||
int path_len = snprintf(path, sizeof(path), "%s/%s.db", cdir, project);
|
||||
if (path_len <= 0 || (size_t)path_len >= sizeof(path)) {
|
||||
return NULL;
|
||||
}
|
||||
return store_open_internal(path, false);
|
||||
}
|
||||
|
||||
@@ -945,22 +960,37 @@ int cbm_store_dump_to_file(cbm_store_t *s, const char *dest_path) {
|
||||
|
||||
/* Ensure parent directory exists */
|
||||
char dir[CBM_SZ_1K];
|
||||
snprintf(dir, sizeof(dir), "%s", dest_path);
|
||||
int dir_len = snprintf(dir, sizeof(dir), "%s", dest_path);
|
||||
if (dir_len <= 0 || (size_t)dir_len >= sizeof(dir)) {
|
||||
store_set_error(s, "dump: destination path too long");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
char *sl = strrchr(dir, '/');
|
||||
if (sl) {
|
||||
*sl = '\0';
|
||||
(void)cbm_mkdir(dir);
|
||||
}
|
||||
|
||||
/* Write to temp file for atomic swap */
|
||||
/* Write to a unique temp file for atomic swap. This avoids deleting or
|
||||
* colliding with a sibling writer's predictable "<dest>.tmp" file. */
|
||||
char tmp_path[CBM_SZ_1K];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", dest_path);
|
||||
(void)unlink(tmp_path);
|
||||
int tmp_len = snprintf(tmp_path, sizeof(tmp_path), "%s.tmp.XXXXXX", dest_path);
|
||||
if (tmp_len <= 0 || (size_t)tmp_len >= sizeof(tmp_path)) {
|
||||
store_set_error(s, "dump: temp path too long");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
int tmp_fd = cbm_mkstemp_s(tmp_path, sizeof(tmp_path));
|
||||
if (tmp_fd < 0) {
|
||||
store_set_error(s, "dump: cannot create temp file");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
cbm_close_fd(tmp_fd);
|
||||
|
||||
sqlite3 *dest_db = NULL;
|
||||
int rc = sqlite3_open(tmp_path, &dest_db);
|
||||
if (rc != SQLITE_OK) {
|
||||
store_set_error(s, "dump: cannot open temp file");
|
||||
(void)cbm_unlink(tmp_path);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
@@ -968,7 +998,7 @@ int cbm_store_dump_to_file(cbm_store_t *s, const char *dest_path) {
|
||||
if (!bk) {
|
||||
store_set_error(s, "dump: backup init failed");
|
||||
sqlite3_close(dest_db);
|
||||
(void)unlink(tmp_path);
|
||||
(void)cbm_unlink(tmp_path);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
@@ -978,7 +1008,7 @@ int cbm_store_dump_to_file(cbm_store_t *s, const char *dest_path) {
|
||||
if (rc != SQLITE_DONE) {
|
||||
store_set_error(s, "dump: backup step failed");
|
||||
sqlite3_close(dest_db);
|
||||
(void)unlink(tmp_path);
|
||||
(void)cbm_unlink(tmp_path);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
@@ -988,9 +1018,9 @@ int cbm_store_dump_to_file(cbm_store_t *s, const char *dest_path) {
|
||||
|
||||
/* Atomic rename: old WAL/SHM become stale and get recreated by
|
||||
* the next reader's configure_pragmas call. */
|
||||
if (rename(tmp_path, dest_path) != 0) {
|
||||
if (cbm_replace_file(tmp_path, dest_path) != 0) {
|
||||
store_set_error(s, "dump: rename failed");
|
||||
(void)unlink(tmp_path);
|
||||
(void)cbm_unlink(tmp_path);
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
@@ -3573,13 +3603,14 @@ static int arch_entry_points(cbm_store_t *s, const char *project, cbm_architectu
|
||||
"WHERE project=?1 AND json_extract(properties, '$.is_entry_point') = 1 "
|
||||
"AND (json_extract(properties, '$.is_test') IS NULL OR "
|
||||
"json_extract(properties, '$.is_test') != 1) "
|
||||
"AND file_path NOT LIKE '%test%' LIMIT 20";
|
||||
"AND file_path NOT LIKE '%test%' LIMIT ?2";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "arch_entry_points");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
bind_text(stmt, SKIP_ONE, project);
|
||||
sqlite3_bind_int(stmt, ST_COL_2, ST_ARCH_ENTRY_POINT_LIMIT);
|
||||
|
||||
int cap = ST_INIT_CAP_8;
|
||||
int n = 0;
|
||||
@@ -3624,12 +3655,38 @@ static char *extract_json_string_prop(const char *json, const char *key, int key
|
||||
return heap_strdup(vbuf);
|
||||
}
|
||||
|
||||
static bool arch_route_should_include(const char *name, const char *qn) {
|
||||
if (!name || !name[0]) {
|
||||
return false;
|
||||
}
|
||||
bool http_like_name = name[0] == '/' || strstr(name, "://") != NULL;
|
||||
if (!http_like_name) {
|
||||
return true;
|
||||
}
|
||||
/* Absolute URL infra nodes support cross-service matching in the graph, but
|
||||
* __route__infra__ entries are endpoints from config/package metadata rather
|
||||
* than application routes. Keep code-created absolute URL routes visible:
|
||||
* many languages emit HTTP client routes as full URLs. */
|
||||
if (qn && strncmp(qn, "__route__infra__", SLEN("__route__infra__")) == 0) {
|
||||
return false;
|
||||
}
|
||||
if (qn && (strncmp(qn, "__grpc__", SLEN("__grpc__")) == 0 ||
|
||||
strncmp(qn, "__graphql__", SLEN("__graphql__")) == 0 ||
|
||||
strncmp(qn, "__trpc__", SLEN("__trpc__")) == 0)) {
|
||||
return true;
|
||||
}
|
||||
return cbm_service_pattern_is_http_route_literal(name, NULL);
|
||||
}
|
||||
|
||||
static int arch_routes(cbm_store_t *s, const char *project, cbm_architecture_info_t *out) {
|
||||
const char *sql = "SELECT name, properties, COALESCE(file_path, '') FROM nodes "
|
||||
"WHERE project=?1 AND label='Route' "
|
||||
"AND (json_extract(properties, '$.is_test') IS NULL OR "
|
||||
"json_extract(properties, '$.is_test') != 1) "
|
||||
"LIMIT 20";
|
||||
char sql[ST_SQL_BUF];
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT name, properties, COALESCE(file_path, ''), qualified_name FROM nodes "
|
||||
"WHERE project=?1 AND label='Route' "
|
||||
"AND (json_extract(properties, '$.is_test') IS NULL OR "
|
||||
"json_extract(properties, '$.is_test') != 1) "
|
||||
"LIMIT %d",
|
||||
ST_ARCH_ROUTE_SCAN_LIMIT);
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "arch_routes");
|
||||
@@ -3644,9 +3701,19 @@ static int arch_routes(cbm_store_t *s, const char *project, cbm_architecture_inf
|
||||
const char *name = (const char *)sqlite3_column_text(stmt, 0);
|
||||
const char *props = (const char *)sqlite3_column_text(stmt, SKIP_ONE);
|
||||
const char *fp = (const char *)sqlite3_column_text(stmt, CBM_SZ_2);
|
||||
const char *qn = (const char *)sqlite3_column_text(stmt, CBM_SZ_3);
|
||||
if (cbm_is_test_file_path(fp)) {
|
||||
continue;
|
||||
}
|
||||
/* Output-level safety net for HTTP-like Route names only. Non-HTTP
|
||||
* Route classes (gRPC, GraphQL, tRPC, async topics) are legitimate
|
||||
* architecture signals even when their names are not URL paths. */
|
||||
if (!arch_route_should_include(name, qn)) {
|
||||
continue;
|
||||
}
|
||||
if (n >= ST_ARCH_ROUTE_RESULT_LIMIT) {
|
||||
break;
|
||||
}
|
||||
if (n >= cap) {
|
||||
cap *= ST_GROWTH;
|
||||
arr = safe_realloc(arr, cap * sizeof(cbm_route_info_t));
|
||||
@@ -5010,6 +5077,41 @@ static void cluster_add_pkg(const char **pkgs, int *counts, int *count, int cap,
|
||||
}
|
||||
}
|
||||
|
||||
static bool cluster_label_is_generic(const char *label) {
|
||||
static const char *const generic[] = {"get", "set", "run", "main", "init",
|
||||
"new", "open", "close", "read", "write",
|
||||
"start", "stop", "test", NULL};
|
||||
if (!label) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; generic[i]; i++) {
|
||||
if (strcmp(label, generic[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static char *cluster_make_label(const cbm_cluster_info_t *ci) {
|
||||
if (ci->top_node_count > 0) {
|
||||
const char *primary = ci->top_nodes[0];
|
||||
if (cluster_label_is_generic(primary) && ci->top_node_count > 1) {
|
||||
const char *secondary = ci->top_nodes[1];
|
||||
size_t len = strlen(primary) + strlen(secondary) + 4;
|
||||
char *label = malloc(len);
|
||||
if (label) {
|
||||
snprintf(label, len, "%s/%s", primary, secondary);
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return heap_strdup(primary);
|
||||
}
|
||||
if (ci->package_count > 0) {
|
||||
return heap_strdup(ci->packages[0]);
|
||||
}
|
||||
return heap_strdup("cluster");
|
||||
}
|
||||
|
||||
/* Build the cluster_info for one community c into *ci. */
|
||||
static void cluster_build_one(cbm_cluster_info_t *ci, int c, int n, const int *comm,
|
||||
const int *degree, const char **names, const char **qns, int members,
|
||||
@@ -5065,22 +5167,20 @@ static void cluster_build_one(cbm_cluster_info_t *ci, int c, int n, const int *c
|
||||
}
|
||||
if (pc > 0) {
|
||||
ci->packages = malloc((size_t)pc * sizeof(char *));
|
||||
int best = 0;
|
||||
for (int i = 0; i < pc; i++) {
|
||||
ci->packages[i] = heap_strdup(pkgs[i]);
|
||||
if (pkg_counts[i] > pkg_counts[best]) {
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
ci->package_count = pc;
|
||||
ci->label = heap_strdup(pkgs[best]);
|
||||
for (int i = 0; i < pc; i++) {
|
||||
safe_str_free(&pkgs[i]);
|
||||
}
|
||||
}
|
||||
if (!ci->label) {
|
||||
ci->label = heap_strdup(ci->top_node_count > 0 ? ci->top_nodes[0] : "cluster");
|
||||
}
|
||||
/* Label: the top hub node is the most informative AND discriminable name
|
||||
* for the community (e.g. "create_task", "install_plugins",
|
||||
* "execute_tmux_command"). Labeling by the dominant package made every
|
||||
* cluster in a single-package repo share one identical, uninformative
|
||||
* label. The package list is preserved separately in `packages`. */
|
||||
ci->label = cluster_make_label(ci);
|
||||
|
||||
ci->edge_types = malloc(sizeof(char *));
|
||||
ci->edge_types[0] = heap_strdup("CALLS");
|
||||
@@ -5807,13 +5907,14 @@ int cbm_store_find_architecture_docs(cbm_store_t *s, const char *project, char *
|
||||
"AND (file_path LIKE '%ARCHITECTURE.md' OR file_path LIKE '%ADR.md' "
|
||||
"OR file_path LIKE '%DECISIONS.md' OR file_path LIKE 'docs/adr/%' "
|
||||
"OR file_path LIKE 'doc/adr/%' OR file_path LIKE 'adr/%') "
|
||||
"ORDER BY file_path LIMIT 20";
|
||||
"ORDER BY file_path LIMIT ?2";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "find_arch_docs");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
bind_text(stmt, SKIP_ONE, project);
|
||||
sqlite3_bind_int(stmt, ST_COL_2, ST_ARCH_DOC_LIMIT);
|
||||
|
||||
int cap = ST_INIT_CAP_8;
|
||||
int n = 0;
|
||||
|
||||
@@ -62,10 +62,6 @@ struct cbm_watcher {
|
||||
|
||||
/* ── Constants ─────────────────────────────────────────────────── */
|
||||
|
||||
/* Time unit conversions */
|
||||
#define NS_PER_SEC 1000000000LL
|
||||
#define US_PER_MS 1000000LL
|
||||
|
||||
/* Adaptive poll interval parameters (ms) */
|
||||
#define POLL_BASE_MS 5000
|
||||
#define POLL_FILE_STEP 500 /* add 1s per this many files */
|
||||
@@ -79,7 +75,7 @@ struct cbm_watcher {
|
||||
static int64_t now_ns(void) {
|
||||
struct timespec ts;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ((int64_t)ts.tv_sec * NS_PER_SEC) + ts.tv_nsec;
|
||||
return ((int64_t)ts.tv_sec * (int64_t)CBM_NSEC_PER_SEC) + ts.tv_nsec;
|
||||
}
|
||||
|
||||
/* ── Adaptive interval ──────────────────────────────────────────── */
|
||||
@@ -399,7 +395,7 @@ static void init_baseline(project_state_t *s, const cbm_watcher_t *w) {
|
||||
cbm_log_info("watcher.baseline", "project", s->project_name, "strategy", "none");
|
||||
}
|
||||
|
||||
s->next_poll_ns = now_ns() + ((int64_t)s->interval_ms * US_PER_MS);
|
||||
s->next_poll_ns = now_ns() + ((int64_t)s->interval_ms * (int64_t)CBM_NSEC_PER_MSEC);
|
||||
}
|
||||
|
||||
/* Check if a project has changes. Returns true if reindex needed. */
|
||||
@@ -469,7 +465,7 @@ static void poll_project(const char *key, void *val, void *ud) {
|
||||
/* Check for changes */
|
||||
bool changed = check_changes(s);
|
||||
if (!changed) {
|
||||
s->next_poll_ns = ctx->now + ((int64_t)s->interval_ms * US_PER_MS);
|
||||
s->next_poll_ns = ctx->now + ((int64_t)s->interval_ms * (int64_t)CBM_NSEC_PER_MSEC);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -491,7 +487,7 @@ static void poll_project(const char *key, void *val, void *ud) {
|
||||
}
|
||||
}
|
||||
|
||||
s->next_poll_ns = ctx->now + ((int64_t)s->interval_ms * US_PER_MS);
|
||||
s->next_poll_ns = ctx->now + ((int64_t)s->interval_ms * (int64_t)CBM_NSEC_PER_MSEC);
|
||||
}
|
||||
|
||||
/* Callback to snapshot project state pointers into an array. */
|
||||
|
||||
@@ -203,7 +203,9 @@ static cbm_store_t *cp_open_indexed(CP_Proj *lp) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -107,7 +107,9 @@ static cbm_store_t *ei_index_files(EILangProj *lp, const EILangFile *files, int
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -174,7 +174,9 @@ static cbm_store_t *es_lang_open_indexed(ES_LangProj *lp) {
|
||||
home = "/tmp";
|
||||
}
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -102,7 +102,9 @@ static cbm_store_t *et_index_files(EtProj *lp, const EtFile *files, int nfiles)
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -32,6 +32,14 @@
|
||||
#include <math.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Resolve the on-disk cache dir — honors the CBM_CACHE_DIR env var (used by the
|
||||
* test runner to isolate each run into a per-run temp dir) and otherwise falls
|
||||
* back to ~/.cache/codebase-memory-mcp. Defined in foundation/platform.c.
|
||||
* Forward-declared here so every test file builds the SAME db path the pipeline
|
||||
* writes (the pipeline honors CBM_CACHE_DIR); hardcoding ~/.cache mismatched the
|
||||
* write path and yielded empty-store failures under isolation. */
|
||||
const char *cbm_resolve_cache_dir(void);
|
||||
|
||||
/* ── Global counters (defined in test_main.c) ──────────────────── */
|
||||
|
||||
extern int tf_pass_count;
|
||||
|
||||
@@ -76,7 +76,9 @@ static cbm_store_t *gpa_open_indexed(GpaProj *lp) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -72,7 +72,9 @@ static cbm_store_t *pb_open_indexed(ProbeLangProj *lp) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -60,7 +60,9 @@ static cbm_store_t *gp_open_indexed(GP_Proj *lp) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -74,7 +74,9 @@ static cbm_store_t *gpd_open_indexed(GpdProj *lp) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -81,7 +81,9 @@ static cbm_store_t *gpe_open_indexed(GpeProj *lp) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -70,7 +70,9 @@ static cbm_store_t *gpf_open_indexed(GpfProj *lp) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -83,7 +83,9 @@ static cbm_store_t *gpg_open_indexed(GpgProj *lp) {
|
||||
if (!home)
|
||||
home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
#include <pipeline/pipeline.h>
|
||||
#include <foundation/log.h>
|
||||
#include <foundation/mem.h>
|
||||
/* Forward decl (foundation/platform.c): honors CBM_CACHE_DIR so the test reads
|
||||
* the index from the same dir the pipeline writes it (needed for isolation). */
|
||||
const char *cbm_resolve_cache_dir(void);
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
@@ -227,13 +230,18 @@ static int incremental_setup(void) {
|
||||
if (!g_project)
|
||||
return -1;
|
||||
|
||||
const char *home = getenv("HOME");
|
||||
if (!home)
|
||||
home = "/tmp";
|
||||
snprintf(g_dbpath, sizeof(g_dbpath), "%s/.cache/codebase-memory-mcp/%s.db", home, g_project);
|
||||
/* Resolve the cache dir via cbm_resolve_cache_dir() so it honors CBM_CACHE_DIR
|
||||
* and matches the index WRITE path (pipeline.c). Hardcoding ~/.cache here
|
||||
* made get_node_count read from a different dir than the index wrote under
|
||||
* CBM_TEST_ISOLATE, yielding 0-node indexes (and a div-by-zero). */
|
||||
const char *cdir = cbm_resolve_cache_dir();
|
||||
if (!cdir) {
|
||||
cdir = "/tmp";
|
||||
}
|
||||
snprintf(g_dbpath, sizeof(g_dbpath), "%s/%s.db", cdir, g_project);
|
||||
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s", cdir);
|
||||
cbm_mkdir(cache_dir);
|
||||
|
||||
unlink(g_dbpath);
|
||||
|
||||
+21
-2
@@ -1,12 +1,11 @@
|
||||
#include "test_framework.h"
|
||||
#include "graph_buffer/graph_buffer.h"
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
#include "service_patterns.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name);
|
||||
|
||||
static int has_data_flow(cbm_gbuf_t *gb, int64_t source_id, int64_t target_id) {
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int count = 0;
|
||||
@@ -28,7 +27,27 @@ TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths) {
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/api", "os.path.join"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal(NULL, "requests.get"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("", "requests.get"));
|
||||
/* CLI slash-command syntax: ':' mid-segment is not a route param
|
||||
* (autorun's "/ar:allow", "/ar:a" etc. — not HTTP routes). */
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/ar:allow", "app.command"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/ar:a", "app.command"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/gh:pr", "app.command"));
|
||||
/* Filesystem paths with document/source extensions are never routes. */
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/new/file.txt", "open"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/fake/path.pdf", "open"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/Users/test/plans/foo.md", "open"));
|
||||
/* Filesystem roots. */
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/usr/bin/uv", "subprocess"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/home/user/.claude/plans/bar.md", "open"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/tmp/alpha", "requests.get"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/Users/dev/project", "requests.get"));
|
||||
/* Whitespace: command/description strings are not routes. */
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/autorun test task description", "run"));
|
||||
/* Positive controls: real routes must still pass. */
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/api/orders", "requests.get"));
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/users/:id", "app.route"));
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/teams/:team/users/:id", "app.route"));
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/items/{id}", "router.get"));
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("https://orders.example/api/orders",
|
||||
"requests.get"));
|
||||
PASS();
|
||||
|
||||
@@ -105,15 +105,16 @@ static int integration_setup(void) {
|
||||
if (!g_project)
|
||||
return -1;
|
||||
|
||||
/* Build db path for direct store queries (pipeline writes here) */
|
||||
const char *home = getenv("HOME");
|
||||
if (!home)
|
||||
home = "/tmp";
|
||||
snprintf(g_dbpath, sizeof(g_dbpath), "%s/.cache/codebase-memory-mcp/%s.db", home, g_project);
|
||||
/* Build db path for direct store queries (pipeline writes here). Honors
|
||||
* CBM_CACHE_DIR so it matches the pipeline write path (test isolation). */
|
||||
snprintf(g_dbpath, sizeof(g_dbpath), "%s/%s.db",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp", g_project);
|
||||
|
||||
/* Ensure cache dir exists */
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
|
||||
/* Remove stale db from previous test runs */
|
||||
|
||||
@@ -77,7 +77,9 @@ static cbm_store_t *lang_open_indexed(LangProj *lp) {
|
||||
home = "/tmp";
|
||||
}
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -127,7 +127,9 @@ static cbm_store_t *lrp_open_indexed(LRP_Proj *lp) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -112,20 +112,56 @@ extern void suite_dump_verify_io(void);
|
||||
* caches at thread teardown (pass_parallel.c). */
|
||||
extern void cbm_kind_in_set_free_cache(void);
|
||||
|
||||
/* Capacity for the per-run isolated cache dir path. comfortably fits
|
||||
* "/tmp/cbm-test-cache-" + the 6 mkdtemp placeholder chars + headroom. */
|
||||
#define TEST_CACHE_DIR_CAP 512
|
||||
/* setenv() overwrite flag: nonzero = replace an existing value. */
|
||||
#define ENV_OVERWRITE 1
|
||||
|
||||
int main(void) {
|
||||
printf("\n codebase-memory-mcp C test suite\n");
|
||||
|
||||
/* DEFAULT-ON store isolation: redirect every test index into a per-run
|
||||
* temp dir so the suite never pollutes the user's real
|
||||
* ~/.cache/codebase-memory-mcp. Opt out with CBM_TEST_NO_ISOLATE=1 (e.g.
|
||||
* to debug against the real store).
|
||||
*
|
||||
* Works because every test helper now builds its db path via
|
||||
* cbm_resolve_cache_dir() (honors CBM_CACHE_DIR), matching the pipeline
|
||||
* write path. Earlier these helpers hardcoded ~/.cache and mismatched the
|
||||
* CBM_CACHE_DIR-honoring write → 815 empty-store failures. The production
|
||||
* path (pipeline.c + mcp.c) honors CBM_CACHE_DIR regardless. */
|
||||
const char *no_iso = getenv("CBM_TEST_NO_ISOLATE");
|
||||
if (!no_iso || no_iso[0] == '\0') {
|
||||
static char test_cache_dir[TEST_CACHE_DIR_CAP];
|
||||
snprintf(test_cache_dir, sizeof(test_cache_dir), "/tmp/cbm-test-cache-XXXXXX");
|
||||
if (mkdtemp(test_cache_dir)) {
|
||||
setenv("CBM_CACHE_DIR", test_cache_dir, ENV_OVERWRITE);
|
||||
}
|
||||
}
|
||||
|
||||
const char *only_suite = getenv("CBM_ONLY_SUITE");
|
||||
if (only_suite && only_suite[0]) {
|
||||
if (strstr("incremental", only_suite)) RUN_SUITE(incremental);
|
||||
if (strstr("mcp", only_suite)) RUN_SUITE(mcp);
|
||||
if (strstr("tool_consolidation", only_suite)) RUN_SUITE(tool_consolidation);
|
||||
if (strstr("cli", only_suite)) RUN_SUITE(cli);
|
||||
if (strstr("pipeline", only_suite)) RUN_SUITE(pipeline);
|
||||
if (strstr("parallel", only_suite)) RUN_SUITE(parallel);
|
||||
if (strstr("store_nodes", only_suite)) RUN_SUITE(store_nodes);
|
||||
if (strstr("store_search", only_suite)) RUN_SUITE(store_search);
|
||||
if (strstr("store_bulk", only_suite)) RUN_SUITE(store_bulk);
|
||||
if (strstr("store_pragmas", only_suite)) RUN_SUITE(store_pragmas);
|
||||
if (strstr("store_checkpoint", only_suite)) RUN_SUITE(store_checkpoint);
|
||||
if (strstr("sqlite_writer", only_suite)) RUN_SUITE(sqlite_writer);
|
||||
if (strstr("graph_buffer", only_suite)) RUN_SUITE(graph_buffer);
|
||||
if (strstr("pagerank", only_suite)) RUN_SUITE(pagerank);
|
||||
if (strstr("depindex", only_suite)) RUN_SUITE(depindex);
|
||||
if (strstr("store_arch", only_suite)) RUN_SUITE(store_arch);
|
||||
if (strstr("infrascan", only_suite)) RUN_SUITE(infrascan);
|
||||
if (strstr("watcher", only_suite)) RUN_SUITE(watcher);
|
||||
if (strstr("security", only_suite)) RUN_SUITE(security);
|
||||
if (strstr("artifact", only_suite)) RUN_SUITE(artifact);
|
||||
TEST_SUMMARY();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@ static cbm_store_t *mkc_open_indexed(MKC_Proj *lp) {
|
||||
if (!home)
|
||||
home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -68,7 +68,9 @@ static cbm_store_t *mn_open_indexed(MN_LangProj *lp) {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
+60
-1
@@ -412,6 +412,56 @@ TEST(tool_list_projects_empty) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_list_projects_includes_tmp_prefixed_project) {
|
||||
char cache[256];
|
||||
snprintf(cache, sizeof(cache), "/tmp/cbm-list-tmp-XXXXXX");
|
||||
if (!cbm_mkdtemp(cache)) {
|
||||
PASS(); /* skip if mkdtemp fails */
|
||||
}
|
||||
|
||||
const char *saved = getenv("CBM_CACHE_DIR");
|
||||
char *saved_copy = saved ? strdup(saved) : NULL;
|
||||
cbm_setenv("CBM_CACHE_DIR", cache, 1);
|
||||
|
||||
char db_path[512];
|
||||
int db_len = snprintf(db_path, sizeof(db_path), "%s/tmp-valid-project.db", cache);
|
||||
ASSERT_TRUE(db_len > 0 && (size_t)db_len < sizeof(db_path));
|
||||
cbm_store_t *store = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(store);
|
||||
ASSERT_EQ(cbm_store_upsert_project(store, "tmp-valid-project", "/tmp/valid-project"),
|
||||
CBM_STORE_OK);
|
||||
cbm_store_close(store);
|
||||
|
||||
cbm_mcp_server_t *srv = setup_mcp_with_data();
|
||||
char *resp =
|
||||
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":10,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"list_projects\",\"arguments\":{}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "tmp-valid-project"));
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
|
||||
if (saved_copy) {
|
||||
cbm_setenv("CBM_CACHE_DIR", saved_copy, 1);
|
||||
free(saved_copy);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_CACHE_DIR");
|
||||
}
|
||||
cbm_unlink(db_path);
|
||||
char wal[512];
|
||||
char shm[512];
|
||||
int wal_len = snprintf(wal, sizeof(wal), "%s-wal", db_path);
|
||||
int shm_len = snprintf(shm, sizeof(shm), "%s-shm", db_path);
|
||||
if (wal_len > 0 && (size_t)wal_len < sizeof(wal)) {
|
||||
cbm_unlink(wal);
|
||||
}
|
||||
if (shm_len > 0 && (size_t)shm_len < sizeof(shm)) {
|
||||
cbm_unlink(shm);
|
||||
}
|
||||
cbm_rmdir(cache);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_get_graph_schema_empty) {
|
||||
cbm_mcp_server_t *srv = setup_mcp_with_data();
|
||||
|
||||
@@ -484,6 +534,13 @@ TEST(tool_search_graph_includes_node_properties) {
|
||||
ASSERT_NOT_NULL(strstr(inner, "signature"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "func HandleRequest"));
|
||||
ASSERT_NOT_NULL(strstr(inner, "is_exported"));
|
||||
const char *scan = inner;
|
||||
int source_keys = 0;
|
||||
while ((scan = strstr(scan, "\"source\"")) != NULL) {
|
||||
source_keys++;
|
||||
scan += strlen("\"source\"");
|
||||
}
|
||||
ASSERT_EQ(source_keys, 1);
|
||||
free(inner);
|
||||
free(resp);
|
||||
|
||||
@@ -1371,7 +1428,8 @@ static cbm_mcp_server_t *setup_snippet_server(char *tmp_dir, size_t tmp_sz) {
|
||||
n_hr.end_line = 5;
|
||||
n_hr.properties_json = "{\"signature\":\"func HandleRequest() error\","
|
||||
"\"return_type\":\"error\","
|
||||
"\"is_exported\":true}";
|
||||
"\"is_exported\":true,"
|
||||
"\"source\":\"infra\"}";
|
||||
int64_t id_hr = cbm_store_upsert_node(st, &n_hr);
|
||||
|
||||
cbm_node_t n_po = {0};
|
||||
@@ -2332,6 +2390,7 @@ SUITE(mcp) {
|
||||
|
||||
/* Tool handlers */
|
||||
RUN_TEST(tool_list_projects_empty);
|
||||
RUN_TEST(tool_list_projects_includes_tmp_prefixed_project);
|
||||
RUN_TEST(tool_get_graph_schema_empty);
|
||||
RUN_TEST(tool_unknown_tool);
|
||||
RUN_TEST(tool_search_graph_basic);
|
||||
|
||||
@@ -76,7 +76,9 @@ static cbm_store_t *ncp_open_indexed(NcpLangProj *lp) {
|
||||
if (!home)
|
||||
home = "/tmp";
|
||||
char cache_dir[512];
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||||
/* Honor CBM_CACHE_DIR so this matches the pipeline write path (test isolation). */
|
||||
snprintf(cache_dir, sizeof(cache_dir), "%s",
|
||||
cbm_resolve_cache_dir() ? cbm_resolve_cache_dir() : "/tmp");
|
||||
cbm_mkdir(cache_dir);
|
||||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||||
unlink(lp->dbpath);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
#include "store/store.h"
|
||||
#include "cli/cli.h"
|
||||
#include "git/git_context.h"
|
||||
#include "foundation/dump_verify.h"
|
||||
|
||||
@@ -5571,6 +5572,63 @@ TEST(pipeline_double_free_prevention) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_unit_threshold_setters_clamp_invalid_values) {
|
||||
cbm_pipeline_t *p = cbm_pipeline_new("/tmp/nonexistent", NULL, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
|
||||
cbm_pipeline_set_similarity_threshold(p, 0.7);
|
||||
cbm_pipeline_set_httplink_min_confidence(p, 0.25);
|
||||
cbm_pipeline_set_semantic_threshold(p, 0.75);
|
||||
cbm_pipeline_set_githistory_min_coupling(p, 0.3);
|
||||
cbm_pipeline_set_lsp_confidence_floor(p, 0.6);
|
||||
ASSERT_TRUE(cbm_pipeline_similarity_threshold(p) == 0.7);
|
||||
ASSERT_TRUE(cbm_pipeline_httplink_min_confidence(p) == 0.25);
|
||||
ASSERT_TRUE(cbm_pipeline_semantic_threshold(p) == 0.75);
|
||||
ASSERT_TRUE(cbm_pipeline_githistory_min_coupling(p) == 0.3);
|
||||
ASSERT_TRUE(cbm_pipeline_lsp_confidence_floor(p) == 0.6);
|
||||
|
||||
cbm_pipeline_set_similarity_threshold(p, -1.0);
|
||||
cbm_pipeline_set_httplink_min_confidence(p, 0.0);
|
||||
cbm_pipeline_set_semantic_threshold(p, 1.5);
|
||||
cbm_pipeline_set_githistory_min_coupling(p, 2.0);
|
||||
cbm_pipeline_set_lsp_confidence_floor(p, -0.1);
|
||||
ASSERT_TRUE(cbm_pipeline_similarity_threshold(p) == 0.0);
|
||||
ASSERT_TRUE(cbm_pipeline_httplink_min_confidence(p) == 0.0);
|
||||
ASSERT_TRUE(cbm_pipeline_semantic_threshold(p) == 0.0);
|
||||
ASSERT_TRUE(cbm_pipeline_githistory_min_coupling(p) == 0.0);
|
||||
ASSERT_TRUE(cbm_pipeline_lsp_confidence_floor(p) == 0.0);
|
||||
|
||||
cbm_pipeline_free(p);
|
||||
PASS();
|
||||
}
|
||||
|
||||
static const cbm_config_entry_t *find_config_entry(const char *key) {
|
||||
for (int i = 0; CBM_CONFIG_REGISTRY[i].key; i++) {
|
||||
if (strcmp(CBM_CONFIG_REGISTRY[i].key, key) == 0) {
|
||||
return &CBM_CONFIG_REGISTRY[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TEST(config_registry_includes_mcp_timeout_knobs) {
|
||||
const cbm_config_entry_t *idle = find_config_entry("store_idle_timeout_s");
|
||||
ASSERT_NOT_NULL(idle);
|
||||
ASSERT_STR_EQ(idle->default_val, "60");
|
||||
ASSERT_STR_EQ(idle->category, "MCP");
|
||||
|
||||
const cbm_config_entry_t *validate = find_config_entry("db_validate_busy_timeout_ms");
|
||||
ASSERT_NOT_NULL(validate);
|
||||
ASSERT_STR_EQ(validate->default_val, "1000");
|
||||
ASSERT_STR_EQ(validate->category, "MCP");
|
||||
|
||||
const cbm_config_entry_t *update = find_config_entry("update_check_timeout_s");
|
||||
ASSERT_NOT_NULL(update);
|
||||
ASSERT_STR_EQ(update->default_val, "5");
|
||||
ASSERT_STR_EQ(update->category, "MCP");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(trackable_source_files) {
|
||||
/* Common source extensions are trackable */
|
||||
ASSERT_TRUE(cbm_is_trackable_file("main.go"));
|
||||
@@ -6090,6 +6148,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_cancel);
|
||||
RUN_TEST(pipeline_cancel_null);
|
||||
RUN_TEST(pipeline_run_null);
|
||||
RUN_TEST(pipeline_unit_threshold_setters_clamp_invalid_values);
|
||||
RUN_TEST(config_registry_includes_mcp_timeout_knobs);
|
||||
/* File persistence */
|
||||
RUN_TEST(store_file_persistence);
|
||||
RUN_TEST(store_bulk_persistence);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cypher/cypher.h>
|
||||
#include "../src/foundation/str_util.h"
|
||||
#include "../src/foundation/compat_fs.h"
|
||||
#include "../src/foundation/compat_thread.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -366,6 +367,136 @@ TEST(exec_no_shell_captures_exit_code) {
|
||||
|
||||
#endif /* _WIN32 */
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* PORTABLE FILE REPLACEMENT
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(compat_replace_file_replaces_destination) {
|
||||
char *dir = th_mktempdir("cbm_replace_file");
|
||||
ASSERT_NOT_NULL(dir);
|
||||
char root[256];
|
||||
snprintf(root, sizeof(root), "%s", dir);
|
||||
|
||||
const char *dest = TH_PATH(root, "target.txt");
|
||||
const char *tmp = TH_PATH(root, "target.txt.tmp");
|
||||
ASSERT_EQ(th_write_file(dest, "old"), 0);
|
||||
ASSERT_EQ(th_write_file(tmp, "new"), 0);
|
||||
|
||||
ASSERT_EQ(cbm_replace_file(tmp, dest), 0);
|
||||
|
||||
FILE *fp = fopen(dest, "rb");
|
||||
ASSERT_NOT_NULL(fp);
|
||||
char buf[8] = {0};
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
fclose(fp);
|
||||
ASSERT_EQ((int)n, 3);
|
||||
ASSERT_STR_EQ(buf, "new");
|
||||
|
||||
struct stat st;
|
||||
ASSERT_NEQ(stat(tmp, &st), 0);
|
||||
th_cleanup(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(compat_write_file_atomic_replaces_destination) {
|
||||
char *dir = th_mktempdir("cbm_write_file_atomic");
|
||||
ASSERT_NOT_NULL(dir);
|
||||
char root[256];
|
||||
snprintf(root, sizeof(root), "%s", dir);
|
||||
|
||||
const char *dest = TH_PATH(root, "payload.bin");
|
||||
ASSERT_EQ(th_write_file(dest, "old"), 0);
|
||||
|
||||
cbm_file_error_t err = {0};
|
||||
ASSERT_EQ(cbm_write_file_atomic(dest, "new", 3, &err), 0);
|
||||
ASSERT_NULL(err.stage);
|
||||
ASSERT_EQ(err.code, 0);
|
||||
|
||||
FILE *fp = fopen(dest, "rb");
|
||||
ASSERT_NOT_NULL(fp);
|
||||
char buf[8] = {0};
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
fclose(fp);
|
||||
ASSERT_EQ((int)n, 3);
|
||||
ASSERT_STR_EQ(buf, "new");
|
||||
|
||||
struct stat st;
|
||||
ASSERT_NEQ(stat(TH_PATH(root, "payload.bin.tmp"), &st), 0);
|
||||
th_cleanup(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(compat_write_file_atomic_reports_replace_failure) {
|
||||
char *dir = th_mktempdir("cbm_write_file_atomic_fail");
|
||||
ASSERT_NOT_NULL(dir);
|
||||
char root[256];
|
||||
snprintf(root, sizeof(root), "%s", dir);
|
||||
|
||||
const char *dest = TH_PATH(root, "target.txt");
|
||||
ASSERT_TRUE(cbm_mkdir_p(dest, 0755));
|
||||
|
||||
cbm_file_error_t err = {0};
|
||||
ASSERT_NEQ(cbm_write_file_atomic(dest, "new", 3, &err), 0);
|
||||
ASSERT_NOT_NULL(err.stage);
|
||||
ASSERT_STR_EQ(err.stage, "rename_temp");
|
||||
ASSERT_NEQ(err.code, 0);
|
||||
|
||||
th_cleanup(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
enum { ATOMIC_CONCURRENT_WRITES = 64 };
|
||||
|
||||
typedef struct {
|
||||
const char *dest;
|
||||
const char *payload;
|
||||
int failures;
|
||||
} atomic_writer_arg_t;
|
||||
|
||||
static void *atomic_writer_thread(void *arg) {
|
||||
atomic_writer_arg_t *wa = (atomic_writer_arg_t *)arg;
|
||||
size_t len = strlen(wa->payload);
|
||||
for (int i = 0; i < ATOMIC_CONCURRENT_WRITES; i++) {
|
||||
cbm_file_error_t err = {0};
|
||||
if (cbm_write_file_atomic(wa->dest, wa->payload, len, &err) != 0) {
|
||||
wa->failures++;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TEST(compat_write_file_atomic_concurrent_same_destination) {
|
||||
char *dir = th_mktempdir("cbm_write_file_atomic_concurrent");
|
||||
ASSERT_NOT_NULL(dir);
|
||||
char root[256];
|
||||
snprintf(root, sizeof(root), "%s", dir);
|
||||
|
||||
char dest[512];
|
||||
snprintf(dest, sizeof(dest), "%s", TH_PATH(root, "payload.bin"));
|
||||
ASSERT_EQ(th_write_file(dest, "initial"), 0);
|
||||
|
||||
atomic_writer_arg_t a = {.dest = dest, .payload = "alpha", .failures = 0};
|
||||
atomic_writer_arg_t b = {.dest = dest, .payload = "bravo", .failures = 0};
|
||||
cbm_thread_t ta, tb;
|
||||
ASSERT_EQ(cbm_thread_create(&ta, 0, atomic_writer_thread, &a), 0);
|
||||
ASSERT_EQ(cbm_thread_create(&tb, 0, atomic_writer_thread, &b), 0);
|
||||
ASSERT_EQ(cbm_thread_join(&ta), 0);
|
||||
ASSERT_EQ(cbm_thread_join(&tb), 0);
|
||||
ASSERT_EQ(a.failures, 0);
|
||||
ASSERT_EQ(b.failures, 0);
|
||||
|
||||
FILE *fp = fopen(dest, "rb");
|
||||
ASSERT_NOT_NULL(fp);
|
||||
char buf[16] = {0};
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
fclose(fp);
|
||||
ASSERT_TRUE((n == strlen(a.payload) && strcmp(buf, a.payload) == 0) ||
|
||||
(n == strlen(b.payload) && strcmp(buf, b.payload) == 0));
|
||||
|
||||
th_cleanup(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* SUITE
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
@@ -418,4 +549,9 @@ SUITE(security) {
|
||||
RUN_TEST(exec_no_shell_null_argv_returns_error);
|
||||
RUN_TEST(exec_no_shell_captures_exit_code);
|
||||
#endif
|
||||
|
||||
RUN_TEST(compat_replace_file_replaces_destination);
|
||||
RUN_TEST(compat_write_file_atomic_replaces_destination);
|
||||
RUN_TEST(compat_write_file_atomic_reports_replace_failure);
|
||||
RUN_TEST(compat_write_file_atomic_concurrent_same_destination);
|
||||
}
|
||||
|
||||
+33
-4
@@ -252,15 +252,44 @@ TEST(arch_languages) {
|
||||
|
||||
TEST(arch_routes) {
|
||||
cbm_store_t *s = setup_arch_test_store();
|
||||
cbm_node_t infra_url = {
|
||||
.project = "test",
|
||||
.label = "Route",
|
||||
.name = "https://example.com/api/orders",
|
||||
.qualified_name = "__route__infra__https://example.com/api/orders",
|
||||
.properties_json = "{\"source\":\"infra\",\"key_path\":\"ServiceUrl\"}"};
|
||||
cbm_store_upsert_node(s, &infra_url);
|
||||
cbm_node_t code_url = {
|
||||
.project = "test",
|
||||
.label = "Route",
|
||||
.name = "https://api.example.com/v1/orders",
|
||||
.qualified_name = "__route__GET__https://api.example.com/v1/orders",
|
||||
.properties_json =
|
||||
"{\"method\":\"GET\",\"path\":\"https://api.example.com/v1/orders\"}"};
|
||||
cbm_store_upsert_node(s, &code_url);
|
||||
|
||||
cbm_architecture_info_t info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
const char *aspects[] = {"routes"};
|
||||
ASSERT_EQ(cbm_store_get_architecture(s, "test", aspects, 1, &info, 0, 1.0), CBM_STORE_OK);
|
||||
|
||||
ASSERT_EQ(info.route_count, 1);
|
||||
ASSERT_STR_EQ(info.routes[0].method, "POST");
|
||||
ASSERT_STR_EQ(info.routes[0].path, "/api/orders");
|
||||
ASSERT_STR_EQ(info.routes[0].handler, "HandleRequest");
|
||||
ASSERT_EQ(info.route_count, 2);
|
||||
bool saw_app_route = false;
|
||||
bool saw_code_url = false;
|
||||
for (int i = 0; i < info.route_count; i++) {
|
||||
if (strcmp(info.routes[i].path, "/api/orders") == 0) {
|
||||
saw_app_route = true;
|
||||
ASSERT_STR_EQ(info.routes[i].method, "POST");
|
||||
ASSERT_STR_EQ(info.routes[i].handler, "HandleRequest");
|
||||
}
|
||||
if (strcmp(info.routes[i].path, "https://api.example.com/v1/orders") == 0) {
|
||||
saw_code_url = true;
|
||||
ASSERT_STR_EQ(info.routes[i].method, "GET");
|
||||
}
|
||||
ASSERT_STR_NEQ(info.routes[i].path, "https://example.com/api/orders");
|
||||
}
|
||||
ASSERT_TRUE(saw_app_route);
|
||||
ASSERT_TRUE(saw_code_url);
|
||||
|
||||
cbm_store_architecture_free(&info);
|
||||
cbm_store_close(s);
|
||||
|
||||
@@ -988,8 +988,9 @@ TEST(store_integrity_windows_lowercase_drive_issue367) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_integrity_corrupt_too_many_rows) {
|
||||
/* Simulate corruption: >5 rows in projects table */
|
||||
TEST(store_integrity_multiple_project_rows_allowed) {
|
||||
/* Dependency projects are stored in the parent DB, so a valid store may
|
||||
* contain more than one projects row. Row count alone is not corruption. */
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
sqlite3 *db = cbm_store_get_db(s);
|
||||
@@ -1001,7 +1002,7 @@ TEST(store_integrity_corrupt_too_many_rows) {
|
||||
i, i);
|
||||
sqlite3_exec(db, sql, NULL, NULL, NULL);
|
||||
}
|
||||
ASSERT_FALSE(cbm_store_check_integrity(s));
|
||||
ASSERT_TRUE(cbm_store_check_integrity(s));
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
@@ -1037,7 +1038,7 @@ TEST(store_integrity_full_path_only_classification) {
|
||||
ASSERT_FALSE(cbm_store_check_integrity_full(s, &path_only));
|
||||
ASSERT_TRUE(path_only);
|
||||
|
||||
/* Too many rows: fails, path_only == false (genuine corruption). */
|
||||
/* Many valid project rows are allowed and are not a path-only failure. */
|
||||
sqlite3_exec(db, "DELETE FROM projects;", NULL, NULL, NULL);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
char sql[256];
|
||||
@@ -1048,7 +1049,7 @@ TEST(store_integrity_full_path_only_classification) {
|
||||
sqlite3_exec(db, sql, NULL, NULL, NULL);
|
||||
}
|
||||
path_only = true;
|
||||
ASSERT_FALSE(cbm_store_check_integrity_full(s, &path_only));
|
||||
ASSERT_TRUE(cbm_store_check_integrity_full(s, &path_only));
|
||||
ASSERT_FALSE(path_only);
|
||||
|
||||
cbm_store_close(s);
|
||||
@@ -1590,7 +1591,7 @@ SUITE(store_nodes) {
|
||||
RUN_TEST(store_integrity_empty);
|
||||
RUN_TEST(store_integrity_corrupt_bad_path);
|
||||
RUN_TEST(store_integrity_windows_lowercase_drive_issue367);
|
||||
RUN_TEST(store_integrity_corrupt_too_many_rows);
|
||||
RUN_TEST(store_integrity_multiple_project_rows_allowed);
|
||||
RUN_TEST(store_integrity_null_check);
|
||||
RUN_TEST(store_integrity_full_path_only_classification);
|
||||
RUN_TEST(store_project_crud);
|
||||
|
||||
@@ -1014,8 +1014,8 @@ TEST(dep_search_explicit_dep_project_name) {
|
||||
free(r);
|
||||
/* Clean up any DB file that resolve_store may have created */
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/.cache/codebase-memory-mcp/_tc_deptest_proj_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(path, sizeof(path), "%s/_tc_deptest_proj_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
(void)unlink(path);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
@@ -1328,8 +1328,8 @@ TEST(cross_project_search_not_confused_by_prefix) {
|
||||
|
||||
/* Clean up any spurious DB file created by resolve_store */
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/.cache/codebase-memory-mcp/myapp-other-project.db",
|
||||
getenv("HOME"));
|
||||
snprintf(path, sizeof(path), "%s/myapp-other-project.db",
|
||||
cbm_resolve_cache_dir());
|
||||
(void)unlink(path);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
@@ -1384,7 +1384,7 @@ TEST(prefix_collision_dash_after_session_name) {
|
||||
ASSERT_NOT_NULL(r);
|
||||
free(r);
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/.cache/codebase-memory-mcp/myapp-v2.db", getenv("HOME"));
|
||||
snprintf(path, sizeof(path), "%s/myapp-v2.db", cbm_resolve_cache_dir());
|
||||
(void)unlink(path);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
@@ -1400,7 +1400,7 @@ TEST(prefix_collision_underscore_after_session_name) {
|
||||
ASSERT_NOT_NULL(r);
|
||||
free(r);
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/.cache/codebase-memory-mcp/myapp_test.db", getenv("HOME"));
|
||||
snprintf(path, sizeof(path), "%s/myapp_test.db", cbm_resolve_cache_dir());
|
||||
(void)unlink(path);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
@@ -1434,7 +1434,7 @@ TEST(prefix_collision_completely_different_project) {
|
||||
ASSERT_NOT_NULL(r);
|
||||
free(r);
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/.cache/codebase-memory-mcp/other-project.db", getenv("HOME"));
|
||||
snprintf(path, sizeof(path), "%s/other-project.db", cbm_resolve_cache_dir());
|
||||
(void)unlink(path);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
@@ -1451,7 +1451,7 @@ TEST(prefix_collision_session_is_substring_of_project) {
|
||||
ASSERT_NOT_NULL(r);
|
||||
free(r);
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/.cache/codebase-memory-mcp/abc.db", getenv("HOME"));
|
||||
snprintf(path, sizeof(path), "%s/abc.db", cbm_resolve_cache_dir());
|
||||
(void)unlink(path);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
@@ -1468,8 +1468,8 @@ TEST(prefix_collision_session_is_substring_of_project) {
|
||||
TEST(get_code_no_project_uses_open_store_tier1) {
|
||||
/* Create a file DB with one node */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_gc_proj_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_gc_proj_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_gc_proj_", "/tmp");
|
||||
@@ -1509,8 +1509,8 @@ TEST(get_code_no_project_uses_open_store_tier1) {
|
||||
TEST(get_code_single_fuzzy_result_resolves_not_ambiguous) {
|
||||
/* Create a file DB with one node */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_gc_fuzzy_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_gc_fuzzy_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_gc_fuzzy_", "/tmp");
|
||||
@@ -1549,8 +1549,8 @@ TEST(get_code_single_fuzzy_result_resolves_not_ambiguous) {
|
||||
* QN, so get_code works even when srv->current_project is unset. */
|
||||
TEST(get_code_cold_start_parses_project_from_qn) {
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_gc_cold_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_gc_cold_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_gc_cold_", "/tmp");
|
||||
@@ -1615,8 +1615,8 @@ TEST(watcher_registered_after_index_repository) {
|
||||
TEST(watcher_registered_on_resolve_store) {
|
||||
/* Pre-populate a DB with a project that has a known root_path */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_watcher_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_watcher_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_watcher_", "/tmp/cbm_watcher_root");
|
||||
@@ -1648,8 +1648,8 @@ TEST(watcher_registered_on_resolve_store) {
|
||||
TEST(watcher_not_registered_for_unknown_path) {
|
||||
/* Project entry exists but root_path is empty — watcher must NOT be registered */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_watcher_nopath_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_watcher_nopath_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_watcher_nopath_", "");
|
||||
@@ -1699,8 +1699,8 @@ TEST(compact_defaults_to_true) {
|
||||
/* When compact is not provided, name field should be omitted if it's
|
||||
* the last segment of qualified_name */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_compact_default_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_compact_default_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_compact_default_", "/tmp/compact_test");
|
||||
@@ -1740,8 +1740,8 @@ TEST(pagerank_output_has_limited_precision) {
|
||||
/* Pagerank values should be serialized with limited precision (~4 sig figs),
|
||||
* not full 17-digit double precision */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_pr_precision_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_pr_precision_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_pr_precision_", "/tmp/pr_test");
|
||||
@@ -1775,8 +1775,8 @@ TEST(empty_db_not_treated_as_indexed) {
|
||||
/* A DB file with schema but 0 nodes should NOT prevent re-indexing.
|
||||
* Regression test: previously stat(db_path)==0 was enough to skip. */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_empty_db_test_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_empty_db_test_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
/* Create DB with schema but no data */
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
@@ -1813,8 +1813,8 @@ TEST(empty_db_not_treated_as_indexed) {
|
||||
TEST(search_exclude_filters_file_paths) {
|
||||
/* exclude param should remove matching results */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_exclude_test_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_exclude_test_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_exclude_test_", "/tmp/exclude_test");
|
||||
@@ -1862,8 +1862,8 @@ TEST(search_exclude_filters_file_paths) {
|
||||
TEST(search_exclude_empty_array_no_effect) {
|
||||
/* Empty exclude array should not filter anything */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_excl_empty_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_excl_empty_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_excl_empty_", "/tmp/excl_empty");
|
||||
@@ -1889,8 +1889,8 @@ TEST(search_exclude_empty_array_no_effect) {
|
||||
TEST(search_exclude_all_returns_empty) {
|
||||
/* Excluding everything should return 0 results, not error */
|
||||
char db_path[1024];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_excl_all_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_excl_all_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "_tc_excl_all_", "/tmp/excl_all");
|
||||
@@ -1979,8 +1979,8 @@ TEST(project_missing_returns_structured_error) {
|
||||
TEST(source_grep_case_insensitive_by_default) {
|
||||
/* Register a project in the store so get_project_root can resolve it */
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_ci_test_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_ci_test_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
char proj_dir[256];
|
||||
snprintf(proj_dir, sizeof(proj_dir), "%s/cbm_ci_test_%d", cbm_tmpdir(), (int)getpid());
|
||||
cbm_mkdir_p(proj_dir, 0755);
|
||||
@@ -2022,8 +2022,8 @@ TEST(source_grep_case_insensitive_by_default) {
|
||||
* Pattern "HELLO_WORLD" vs file containing "hello_world" only → 0 matches. */
|
||||
TEST(source_grep_case_sensitive_flag_works) {
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_cs_test_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_cs_test_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
char proj_dir[256];
|
||||
snprintf(proj_dir, sizeof(proj_dir), "%s/cbm_cs_test_%d", cbm_tmpdir(), (int)getpid());
|
||||
cbm_mkdir_p(proj_dir, 0755);
|
||||
@@ -2087,8 +2087,8 @@ TEST(graph_mode_compact_error_is_descriptive) {
|
||||
* After fix: response contains "mode_warning" field → PASS. */
|
||||
TEST(source_grep_mode_summary_warns) {
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_sm_test_.db",
|
||||
getenv("HOME"));
|
||||
snprintf(db_path, sizeof(db_path), "%s/_tc_sm_test_.db",
|
||||
cbm_resolve_cache_dir());
|
||||
char proj_dir[256];
|
||||
snprintf(proj_dir, sizeof(proj_dir), "%s/cbm_sm_test_%d", cbm_tmpdir(), (int)getpid());
|
||||
cbm_mkdir_p(proj_dir, 0755);
|
||||
|
||||
Reference in New Issue
Block a user