mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(incremental): re-resolve documents whose member rows name a changed file (#2587)
An unresolved `path::member` row (missing or ambiguous) leads to its file by no edge, so an incremental run never re-resolved the document when that file changed. Two edits then left a row where a full index has an edge: a body edit that writes a missing member's name (no definition added), and a same-named pair of methods shrinking to one (the name stays, so no removed name reaches the rows). The closure now also takes the files whose missing or ambiguous rows with "::" name a changed or deleted file, matched by base name since a reference may be written relative to the document or to the root. When memory runs out, every such row's file is taken. Test: doc_links_md_member_missing_incremental requires incremental == full on the closure-repair route for both edits and for dropping the name again. Without the new closure rule the "member written" step diverges; with it limited to missing rows the "group shrinks" step does. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
1a7239f52b
commit
69155bcf6c
2 files changed
+125
-2
No files matched your search
@@ -1496,7 +1496,12 @@ static int surface_added_names(const char *stored_json, const char *fresh_json,
|
||||
* change to it is GLOBAL (it sets the global usings of a whole project);
|
||||
* - a REMOVED name can make another file's unresolved reference resolve (an
|
||||
* overload group shrinks) or change its reason: the files whose rows
|
||||
* mention it re-resolve with the closure. */
|
||||
* mention it re-resolve with the closure;
|
||||
* - an unresolved `path::member` row (missing, ambiguous) depends on the
|
||||
* member's file as a whole, its text included (a member the file writes
|
||||
* without a definition is no missing member, doc_links_md.c) and the size
|
||||
* of a same-named group: the files whose such rows name a changed or
|
||||
* deleted file, by its base name, re-resolve with the closure. */
|
||||
|
||||
/* Add s[0..n) to a heap-keyed name set. false when it cannot be recorded (too
|
||||
* long for a key, or out of memory): callers fail closed. */
|
||||
@@ -1644,6 +1649,58 @@ static void doc_row_name_dependents(const cbm_doc_link_row_t *rows, int n,
|
||||
}
|
||||
}
|
||||
|
||||
/* Is an unresolved row's reason one a member lookup gives (doc_links_md.c)? */
|
||||
static bool doc_row_member_reason(const char *reason) {
|
||||
return reason && (strcmp(reason, "missing") == 0 || strcmp(reason, "ambiguous") == 0);
|
||||
}
|
||||
|
||||
/* Files whose unresolved `path::member` rows name a member of one of `paths`
|
||||
* (the changed and deleted files), and no edge leads from such a row to the
|
||||
* file: a body edit can write or drop a missing member's name without
|
||||
* changing a definition, and a same-named group can shrink to one without
|
||||
* removing its name. Matched by the base name before "::", since a
|
||||
* reference may be written relative to the document or to the root. When
|
||||
* memory runs out every such row's file is added: re-resolving a file that did
|
||||
* not need it costs time, keeping a stale row costs correctness. Keys are
|
||||
* borrowed from `rows`. */
|
||||
static void doc_row_member_dependents(const cbm_doc_link_row_t *rows, int n, char *const *paths,
|
||||
int npaths, CBMHashTable *out_paths) {
|
||||
if (npaths <= 0) {
|
||||
return;
|
||||
}
|
||||
CBMHashTable *bases = cbm_ht_create(CBM_SZ_64); /* NULL: fail closed, every member row */
|
||||
for (int i = 0; bases && i < npaths; i++) {
|
||||
const char *slash = strrchr(paths[i], '/');
|
||||
const char *base = slash ? slash + SKIP_ONE : paths[i];
|
||||
cbm_ht_set(bases, base, (void *)base); /* borrowed from `paths` */
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char *raw = rows[i].raw;
|
||||
const char *rel = rows[i].rel_path;
|
||||
const char *sep = raw ? strstr(raw, "::") : NULL;
|
||||
if (!sep || !rel || !rel[0] || !doc_row_member_reason(rows[i].reason) ||
|
||||
cbm_ht_get(out_paths, rel)) {
|
||||
continue;
|
||||
}
|
||||
const char *base = sep;
|
||||
while (base > raw && base[-1] != '/') {
|
||||
base--;
|
||||
}
|
||||
char key[CBM_SZ_512];
|
||||
size_t bl = (size_t)(sep - base);
|
||||
bool hit = !bases || bl >= sizeof(key);
|
||||
if (!hit && bl > 0) {
|
||||
memcpy(key, base, bl);
|
||||
key[bl] = '\0';
|
||||
hit = cbm_ht_get(bases, key) != NULL;
|
||||
}
|
||||
if (hit) {
|
||||
cbm_ht_set(out_paths, rel, (void *)rel);
|
||||
}
|
||||
}
|
||||
cbm_ht_free(bases);
|
||||
}
|
||||
|
||||
/* Doc-link carry-forward state of the legacy partial route: the previous
|
||||
* rows, the stored scopes of the files it does not re-extract, and the set
|
||||
* of re-extracted or deleted paths whose old rows it replaces. */
|
||||
@@ -2652,8 +2709,11 @@ static int closure_try_plan(cbm_pipeline_t *p, cbm_store_t *store, const char *p
|
||||
}
|
||||
}
|
||||
/* Files whose unresolved doc-link references name something a changed or
|
||||
* deleted file no longer declares re-resolve too. */
|
||||
* deleted file no longer declares re-resolve too, and so do the files
|
||||
* whose unresolved `path::member` rows name a changed or deleted file. */
|
||||
doc_row_name_dependents(plan->doc_rows, plan->doc_row_count, removed_names, row_deps);
|
||||
doc_row_member_dependents(plan->doc_rows, plan->doc_row_count, changed_paths,
|
||||
n_changed + n_deleted, row_deps);
|
||||
{
|
||||
row_dep_walk_t walk = {.closure = closure_set, .files = files_by_path, .added = 0};
|
||||
cbm_ht_foreach(row_deps, row_dep_visitor, &walk);
|
||||
|
||||
@@ -604,6 +604,68 @@ TEST(doc_links_md_incremental) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2587 incremental == full: whether a `path::member` row is missing depends
|
||||
* on the member file's text, and the row leads to that file by no edge. A
|
||||
* body edit that writes the name (no definition added) turns the row into a
|
||||
* file edge, one that drops it turns the edge back into the row; a same-named
|
||||
* group that shrinks to one turns an ambiguous row into an edge. */
|
||||
TEST(doc_links_md_member_missing_incremental) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlmd_inc_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
|
||||
char repo[512];
|
||||
snprintf(repo, sizeof(repo), "%s/repo", tmp);
|
||||
th_write_file(TH_PATH(repo, "docs/helpers.md"), "# Helpers\n\nSee `src/tools.py::helper2`.\n");
|
||||
th_write_file(TH_PATH(repo, "docs/areas.md"), "# Areas\n\nSee `src/shapes.py::area`.\n");
|
||||
th_write_file(TH_PATH(repo, "src/tools.py"), "def helper():\n"
|
||||
" return 1\n");
|
||||
th_write_file(TH_PATH(repo, "src/shapes.py"), "class Circle:\n"
|
||||
" def area(self):\n"
|
||||
" return 3\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"class Square:\n"
|
||||
" def area(self):\n"
|
||||
" return 4\n");
|
||||
char db[512];
|
||||
char full_db[512];
|
||||
snprintf(db, sizeof(db), "%s/inc.db", tmp);
|
||||
snprintf(full_db, sizeof(full_db), "%s/full.db", tmp);
|
||||
ASSERT_EQ(dm_index(repo, db, NULL), 0);
|
||||
char reason[64];
|
||||
dm_row(db, "docs/helpers.md", "src/tools.py::helper2", reason, sizeof(reason), NULL, 0);
|
||||
ASSERT_STR_EQ(reason, "missing");
|
||||
dm_row(db, "docs/areas.md", "src/shapes.py::area", reason, sizeof(reason), NULL, 0);
|
||||
ASSERT_STR_EQ(reason, "ambiguous");
|
||||
|
||||
/* the body writes the name, no definition added: the row becomes an edge */
|
||||
th_write_file(TH_PATH(repo, "src/tools.py"), "def helper():\n"
|
||||
" return 1 # helper2 follows\n");
|
||||
ASSERT_EQ(dm_step(repo, db, full_db, "member written", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR),
|
||||
0);
|
||||
ASSERT_EQ(md_file_edges(db, "Helpers", "src/tools.py"), 1);
|
||||
/* and drops it again: missing again */
|
||||
th_write_file(TH_PATH(repo, "src/tools.py"), "def helper():\n"
|
||||
" return 1\n");
|
||||
ASSERT_EQ(dm_step(repo, db, full_db, "member dropped", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR),
|
||||
0);
|
||||
dm_row(db, "docs/helpers.md", "src/tools.py::helper2", reason, sizeof(reason), NULL, 0);
|
||||
ASSERT_STR_EQ(reason, "missing");
|
||||
/* one of two `area` methods goes, the name stays: the one left binds */
|
||||
th_write_file(TH_PATH(repo, "src/shapes.py"), "class Circle:\n"
|
||||
" pass\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"class Square:\n"
|
||||
" def area(self):\n"
|
||||
" return 4\n");
|
||||
ASSERT_EQ(dm_step(repo, db, full_db, "group shrinks", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
|
||||
char props[512];
|
||||
ASSERT_EQ(md_edge(db, "Areas", "Square.area", props, sizeof(props)), 1);
|
||||
th_cleanup(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── ADRs ────────────────────────────────────────────────────────── */
|
||||
|
||||
static void md_write_adrs(const char *tmp, const char *first_status) {
|
||||
@@ -1028,6 +1090,7 @@ SUITE(doc_links_md) {
|
||||
RUN_TEST(doc_links_md_member_not_defined_issue2587);
|
||||
RUN_TEST(doc_links_md_member_written_issue2587);
|
||||
RUN_TEST(doc_links_md_incremental);
|
||||
RUN_TEST(doc_links_md_member_missing_incremental);
|
||||
RUN_TEST(doc_links_md_adr);
|
||||
RUN_TEST(doc_links_md_adr_label_choice);
|
||||
RUN_TEST(doc_links_md_adr_reported);
|
||||
|
||||
Reference in new issue
Block a user