Skip to content

Commit d8874fa

Browse files
author
Tommy Corbett
committed
fix(incremental): name File nodes by basename, matching the full pipeline
The incremental path passed changed_files[i].rel_path as the File node NAME, while the full pipeline names File nodes by basename (pipeline.c "Create File node"). Node upserts match by qualified name, so an incremental re-index of a touched file RENAMED its File node in place ("applications.py" -> "fastapi/applications.py"). File nodes are USAGE-edge sources, so: - the same tree yields different graphs depending on whether it was indexed fresh or incrementally, and the divergence grows as files are edited (add AND modify both rename); - name-based traversal dead-ends at any touched file's node (e.g. walking inbound USAGE transitively misses everything that reaches a target through a recently added/modified class). Mirror the full pipeline's node shape at the incremental call site: basename as the name, and the same {"extension": ...} props (previously "{}"). Adds incr_file_node_name_stays_basename to the incremental suite: modify a file, incremental re-index, then assert no File node name contains '/'. Red without the fix, green with it; full incremental suite 162/162. Signed-off-by: Tommy Corbett <spde89@gmail.com>
1 parent 6b57db2 commit d8874fa

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

src/pipeline/pipeline_incremental.c

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -848,8 +848,19 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
848848
for (int i = 0; i < ci; i++) {
849849
char *file_qn = cbm_pipeline_fqn_compute(project, changed_files[i].rel_path, "__file__");
850850
if (file_qn) {
851-
cbm_gbuf_upsert_node(existing, "File", changed_files[i].rel_path, file_qn,
852-
changed_files[i].rel_path, 0, 0, "{}");
851+
/* Mirror the full pipeline's File-node shape (pipeline.c "Create
852+
* File node"): name is the BASENAME, not the repo-relative path.
853+
* The upsert matches the existing node by QN, so passing rel_path
854+
* here RENAMED the node in place — an incrementally re-indexed
855+
* file's File node (a USAGE-edge source) diverged from what a full
856+
* build produces, and the graph drifted as files were touched. */
857+
const char *rel = changed_files[i].rel_path;
858+
const char *slash = strrchr(rel, '/');
859+
const char *basename = slash ? slash + SKIP_ONE : rel;
860+
char props[CBM_SZ_256];
861+
const char *ext = strrchr(basename, '.');
862+
snprintf(props, sizeof(props), "{\"extension\":\"%s\"}", ext ? ext : "");
863+
cbm_gbuf_upsert_node(existing, "File", basename, file_qn, rel, 0, 0, props);
853864
free(file_qn);
854865
}
855866
}

tests/test_incremental.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,38 @@ static int resp_lacks_key(const char *resp, const char *key) {
950950

951951
/* ── list_projects ─────────────────────────────────────────────── */
952952

953+
TEST(incr_file_node_name_stays_basename) {
954+
/* Regression: the incremental path passed rel_path as the File node NAME
955+
* (full build uses the basename). The upsert matches by QN, so a touched
956+
* file's File node was RENAMED in place ("applications.py" ->
957+
* "fastapi/applications.py") and the graph diverged from a full build as
958+
* files were edited. Assert no File node name contains a path separator
959+
* after an incremental re-index of a modified file. */
960+
char path[512];
961+
snprintf(path, sizeof(path), "%s/fastapi/applications.py", g_repodir);
962+
FILE *f = fopen(path, "a");
963+
ASSERT(f != NULL);
964+
fprintf(f, "\n# incr_file_node_name_stays_basename touch\n");
965+
fclose(f);
966+
967+
char *resp = index_repo();
968+
ASSERT(resp != NULL);
969+
free(resp);
970+
971+
double ms;
972+
char *r = call_tool_timed("query_graph", &ms,
973+
"{\"project\":\"%s\","
974+
"\"query\":\"MATCH (n:File) WHERE n.name CONTAINS '/'"
975+
" RETURN n.name LIMIT 5\"}",
976+
g_project);
977+
TOOL_OK(r, ms);
978+
/* No File node may carry a path separator in its name; the query engine
979+
* returns an empty rows array when nothing matches. */
980+
ASSERT(strstr(r, "\"rows\":[]") != NULL || strstr(r, "\\\"rows\\\":[]") != NULL);
981+
free(r);
982+
PASS();
983+
}
984+
953985
TEST(tool_list_projects_basic) {
954986
double ms;
955987
char *r = call_tool_timed("list_projects", &ms, "{}");
@@ -2956,6 +2988,7 @@ SUITE(incremental) {
29562988

29572989
/* Phase 3: Incremental deltas */
29582990
RUN_TEST(incr_modify_file);
2991+
RUN_TEST(incr_file_node_name_stays_basename);
29592992
RUN_TEST(incr_formatter_run);
29602993
RUN_TEST(incr_add_file);
29612994
RUN_TEST(incr_delete_file);

0 commit comments

Comments
 (0)