mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-25 06:33:13 +08:00
Stabilize several graph-producing paths and add stronger regression coverage before the incremental redesign work continues. Key changes: - resolve call import maps from resolved graph-buffer IMPORTS edges so re-exported targets are reused consistently - make semantic, similarity, complexity, route/section upserts, and resolver scoring more deterministic across full/incremental and worker-order differences - propagate incremental phase/persist failures instead of continuing toward publication after failed phases - add canonical graph diff helpers and stricter incremental artifacts for full-vs-incremental comparison - add source-safety lint coverage for MCP stdout, unsafe string APIs, and new raw env/fs API usage - replace touched unsafe string copies with bounded or project-wrapper-based forms Validation: - make -f Makefile.cbm build/c/test-runner - CBM_ONLY_SUITE=pipeline ./build/c/test-runner: 217 passed - CBM_ONLY_SUITE=graph_buffer ./build/c/test-runner: 55 passed - CBM_ONLY_SUITE=mcp ./build/c/test-runner: 117 passed - CBM_ONLY_SUITE=simhash ./build/c/test-runner: 24 passed - CBM_ONLY_SUITE=parallel ./build/c/test-runner: 21 passed - CBM_ONLY_SUITE=watcher ./build/c/test-runner: 57 passed - bash scripts/check-source-safety.sh: OK Known remaining: - CBM_ONLY_SUITE=incremental still fails incr_accuracy_vs_full: 159 passed, 1 failed. Current artifacts show stale IMPORTS/CALLS/USAGE targets in unchanged importers and downstream SEMANTICALLY_RELATED drift; this is tracked in the forensic notes as a fallback-first incremental redesign task. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
255 lines
9.7 KiB
C
255 lines
9.7 KiB
C
/*
|
|
* test_graph_diff.h - Canonical graph comparison helpers for tests.
|
|
*
|
|
* These helpers compare graph facts by stable keys instead of transient row IDs.
|
|
* They are intentionally test-only so production store APIs stay unchanged.
|
|
*/
|
|
#ifndef TEST_GRAPH_DIFF_H
|
|
#define TEST_GRAPH_DIFF_H
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include "../src/foundation/compat.h"
|
|
#include "../src/foundation/constants.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
enum { TG_ROW_SET_INIT_CAP = CBM_SZ_128 };
|
|
|
|
typedef struct {
|
|
char **items;
|
|
int count;
|
|
int cap;
|
|
} tg_row_set_t;
|
|
|
|
static inline void tg_row_set_free(tg_row_set_t *rows) {
|
|
if (!rows) {
|
|
return;
|
|
}
|
|
for (int i = 0; i < rows->count; i++) {
|
|
free(rows->items[i]);
|
|
}
|
|
free(rows->items);
|
|
rows->items = NULL;
|
|
rows->count = 0;
|
|
rows->cap = 0;
|
|
}
|
|
|
|
static inline int tg_set_error(char *err, size_t err_sz, const char *msg) {
|
|
if (err && err_sz > 0) {
|
|
int n = snprintf(err, err_sz, "%s", msg ? msg : "graph diff failed");
|
|
if (n < 0 || (size_t)n >= err_sz) {
|
|
err[err_sz - 1] = '\0';
|
|
}
|
|
}
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
|
|
static inline int tg_row_set_push(tg_row_set_t *rows, const char *row, char *err, size_t err_sz) {
|
|
if (rows->count == rows->cap) {
|
|
int next_cap = rows->cap ? rows->cap * PAIR_LEN : TG_ROW_SET_INIT_CAP;
|
|
char **next = (char **)realloc(rows->items, (size_t)next_cap * sizeof(*next));
|
|
if (!next) {
|
|
return tg_set_error(err, err_sz, "graph diff: out of memory growing row set");
|
|
}
|
|
rows->items = next;
|
|
rows->cap = next_cap;
|
|
}
|
|
rows->items[rows->count] = cbm_strdup(row ? row : "");
|
|
if (!rows->items[rows->count]) {
|
|
return tg_set_error(err, err_sz, "graph diff: out of memory copying row");
|
|
}
|
|
rows->count++;
|
|
return 0;
|
|
}
|
|
|
|
static inline int tg_collect_query(sqlite3 *db, const char *project, const char *sql,
|
|
tg_row_set_t *rows, char *err, size_t err_sz) {
|
|
sqlite3_stmt *stmt = NULL;
|
|
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
return tg_set_error(err, err_sz, sqlite3_errmsg(db));
|
|
}
|
|
rc = sqlite3_bind_text(stmt, 1, project, -1, SQLITE_TRANSIENT);
|
|
if (rc != SQLITE_OK) {
|
|
sqlite3_finalize(stmt);
|
|
return tg_set_error(err, err_sz, sqlite3_errmsg(db));
|
|
}
|
|
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
|
const unsigned char *txt = sqlite3_column_text(stmt, 0);
|
|
if (tg_row_set_push(rows, (const char *)txt, err, err_sz) != 0) {
|
|
sqlite3_finalize(stmt);
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
if (rc != SQLITE_DONE) {
|
|
return tg_set_error(err, err_sz, sqlite3_errmsg(db));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static inline int tg_compare_rows(const char *kind, const tg_row_set_t *left,
|
|
const tg_row_set_t *right, char *err, size_t err_sz) {
|
|
if (left->count != right->count) {
|
|
if (err && err_sz > 0) {
|
|
int n = snprintf(err, err_sz, "%s count differs: left=%d right=%d", kind,
|
|
left->count, right->count);
|
|
if (n < 0 || (size_t)n >= err_sz) {
|
|
err[err_sz - 1] = '\0';
|
|
}
|
|
}
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
int left_idx = 0;
|
|
int right_idx = 0;
|
|
while (left_idx < left->count && right_idx < right->count) {
|
|
int cmp = strcmp(left->items[left_idx], right->items[right_idx]);
|
|
if (cmp == 0) {
|
|
left_idx++;
|
|
right_idx++;
|
|
continue;
|
|
}
|
|
if (cmp < 0) {
|
|
if (err && err_sz > 0) {
|
|
int n = snprintf(err, err_sz,
|
|
"%s left-only row %d:\n left: %s\n right row %d: %s", kind,
|
|
left_idx, left->items[left_idx], right_idx,
|
|
right->items[right_idx]);
|
|
if (n < 0 || (size_t)n >= err_sz) {
|
|
err[err_sz - 1] = '\0';
|
|
}
|
|
}
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
if (err && err_sz > 0) {
|
|
int n = snprintf(err, err_sz,
|
|
"%s right-only row %d:\n right: %s\n left row %d: %s", kind,
|
|
right_idx, right->items[right_idx], left_idx, left->items[left_idx]);
|
|
if (n < 0 || (size_t)n >= err_sz) {
|
|
err[err_sz - 1] = '\0';
|
|
}
|
|
}
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
if (left_idx < left->count || right_idx < right->count) {
|
|
if (err && err_sz > 0) {
|
|
int n = snprintf(err, err_sz, "%s exhausted unevenly: left_row=%d right_row=%d", kind,
|
|
left_idx, right_idx);
|
|
if (n < 0 || (size_t)n >= err_sz) {
|
|
err[err_sz - 1] = '\0';
|
|
}
|
|
}
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static inline int tg_compare_query(sqlite3 *left_db, sqlite3 *right_db, const char *project,
|
|
const char *kind, const char *sql, char *err,
|
|
size_t err_sz) {
|
|
tg_row_set_t left = {0};
|
|
tg_row_set_t right = {0};
|
|
int rc = tg_collect_query(left_db, project, sql, &left, err, err_sz);
|
|
if (rc == 0) {
|
|
rc = tg_collect_query(right_db, project, sql, &right, err, err_sz);
|
|
}
|
|
if (rc == 0) {
|
|
rc = tg_compare_rows(kind, &left, &right, err, err_sz);
|
|
}
|
|
tg_row_set_free(&left);
|
|
tg_row_set_free(&right);
|
|
return rc;
|
|
}
|
|
|
|
static inline int cbm_test_compare_canonical_graphs(const char *left_db_path,
|
|
const char *right_db_path,
|
|
const char *project, char *err,
|
|
size_t err_sz) {
|
|
static const char *nodes_sql =
|
|
"SELECT quote(label) || char(9) || quote(name) || char(9) || "
|
|
"quote(qualified_name) || char(9) || quote(coalesce(file_path,'')) || char(9) || "
|
|
"start_line || char(9) || end_line || char(9) || " /* properties below */
|
|
"COALESCE((SELECT group_concat(item, char(30)) FROM ("
|
|
"SELECT quote(je.key) || '=' || je.type || '=' || "
|
|
"COALESCE(quote(CAST(je.value AS TEXT)), 'NULL') AS item "
|
|
"FROM json_each(n.properties) AS je "
|
|
"ORDER BY je.key, je.type, CAST(je.value AS TEXT)"
|
|
")), '') "
|
|
"FROM nodes n WHERE project = ?1 "
|
|
"ORDER BY label, name, qualified_name, coalesce(file_path,''), start_line, end_line, "
|
|
"COALESCE((SELECT group_concat(item, char(30)) FROM ("
|
|
"SELECT quote(je.key) || '=' || je.type || '=' || "
|
|
"COALESCE(quote(CAST(je.value AS TEXT)), 'NULL') AS item "
|
|
"FROM json_each(n.properties) AS je "
|
|
"ORDER BY je.key, je.type, CAST(je.value AS TEXT)"
|
|
")), '');";
|
|
static const char *edges_sql =
|
|
"SELECT quote(s.label) || char(9) || quote(s.qualified_name) || char(9) || "
|
|
"quote(coalesce(s.file_path,'')) || char(9) || s.start_line || char(9) || "
|
|
"s.end_line || char(9) || quote(t.label) || char(9) || quote(t.qualified_name) || "
|
|
"char(9) || quote(coalesce(t.file_path,'')) || char(9) || t.start_line || char(9) || "
|
|
"t.end_line || char(9) || quote(e.type) || char(9) || "
|
|
"COALESCE((SELECT group_concat(item, char(30)) FROM ("
|
|
"SELECT quote(je.key) || '=' || je.type || '=' || "
|
|
"COALESCE(quote(CAST(je.value AS TEXT)), 'NULL') AS item "
|
|
"FROM json_each(e.properties) AS je "
|
|
"ORDER BY je.key, je.type, CAST(je.value AS TEXT)"
|
|
")), '') "
|
|
"FROM edges e "
|
|
"JOIN nodes s ON s.id = e.source_id "
|
|
"JOIN nodes t ON t.id = e.target_id "
|
|
"WHERE e.project = ?1 "
|
|
"ORDER BY s.label, s.qualified_name, coalesce(s.file_path,''), s.start_line, s.end_line, "
|
|
"t.label, t.qualified_name, coalesce(t.file_path,''), t.start_line, t.end_line, "
|
|
"e.type, COALESCE((SELECT group_concat(item, char(30)) FROM ("
|
|
"SELECT quote(je.key) || '=' || je.type || '=' || "
|
|
"COALESCE(quote(CAST(je.value AS TEXT)), 'NULL') AS item "
|
|
"FROM json_each(e.properties) AS je "
|
|
"ORDER BY je.key, je.type, CAST(je.value AS TEXT)"
|
|
")), '');";
|
|
static const char *hashes_sql =
|
|
"SELECT quote(rel_path) || char(9) || quote(sha256) || char(9) || mtime_ns || char(9) || "
|
|
"size FROM file_hashes WHERE project = ?1 ORDER BY rel_path;";
|
|
|
|
sqlite3 *left_db = NULL;
|
|
sqlite3 *right_db = NULL;
|
|
int rc = sqlite3_open_v2(left_db_path, &left_db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
if (left_db) {
|
|
tg_set_error(err, err_sz, sqlite3_errmsg(left_db));
|
|
sqlite3_close(left_db);
|
|
} else {
|
|
tg_set_error(err, err_sz, "graph diff: cannot open left DB");
|
|
}
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
rc = sqlite3_open_v2(right_db_path, &right_db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
if (right_db) {
|
|
tg_set_error(err, err_sz, sqlite3_errmsg(right_db));
|
|
sqlite3_close(right_db);
|
|
} else {
|
|
tg_set_error(err, err_sz, "graph diff: cannot open right DB");
|
|
}
|
|
sqlite3_close(left_db);
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
|
|
rc = tg_compare_query(left_db, right_db, project, "canonical nodes", nodes_sql, err, err_sz);
|
|
if (rc == 0) {
|
|
rc = tg_compare_query(left_db, right_db, project, "canonical edges", edges_sql, err, err_sz);
|
|
}
|
|
if (rc == 0) {
|
|
rc = tg_compare_query(left_db, right_db, project, "file hashes", hashes_sql, err, err_sz);
|
|
}
|
|
|
|
sqlite3_close(right_db);
|
|
sqlite3_close(left_db);
|
|
return rc;
|
|
}
|
|
|
|
#endif /* TEST_GRAPH_DIFF_H */
|