Skip to content

Commit afebf00

Browse files
authored
Merge pull request #1326 from Enferlain/fix/1287-persisted-coverage-summary
fix(index): preserve persisted coverage summaries
2 parents 39bd040 + fe4396f commit afebf00

2 files changed

Lines changed: 80 additions & 2 deletions

File tree

src/mcp/mcp.c

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7347,6 +7347,54 @@ static void add_parse_partial_summary(yyjson_mut_doc *doc, yyjson_mut_val *root,
73477347
yyjson_mut_obj_add_val(doc, root, "parse_partial", pp);
73487348
}
73497349

7350+
/* The pipeline persists the complete current coverage set before this
7351+
* response is built. Prefer that set over the per-run errors so incremental
7352+
* runs that do not revisit a flagged file, and artifact bootstraps, do not
7353+
* make existing gaps appear to have vanished. By-design exclusions have
7354+
* their own response surface and are not failures. */
7355+
static bool add_persisted_failure_summaries(yyjson_mut_doc *doc, yyjson_mut_val *root,
7356+
cbm_store_t *store, const char *project,
7357+
const char *logfile) {
7358+
cbm_coverage_row_t *rows = NULL;
7359+
int row_count = 0;
7360+
if (cbm_store_coverage_get(store, project, &rows, &row_count) != CBM_STORE_OK) {
7361+
return false;
7362+
}
7363+
7364+
int failure_count = 0;
7365+
for (int i = 0; i < row_count; i++) {
7366+
const char *kind = rows[i].kind ? rows[i].kind : "";
7367+
if (strcmp(kind, "not_indexed_dir") != 0 && strcmp(kind, "not_indexed_file") != 0) {
7368+
failure_count++;
7369+
}
7370+
}
7371+
7372+
cbm_file_error_t *failures =
7373+
failure_count > 0 ? calloc((size_t)failure_count, sizeof(*failures)) : NULL;
7374+
if (failure_count > 0 && !failures) {
7375+
cbm_store_free_coverage(rows, row_count);
7376+
return false;
7377+
}
7378+
7379+
int n = 0;
7380+
for (int i = 0; i < row_count; i++) {
7381+
const char *kind = rows[i].kind ? rows[i].kind : "";
7382+
if (strcmp(kind, "not_indexed_dir") == 0 || strcmp(kind, "not_indexed_file") == 0) {
7383+
continue;
7384+
}
7385+
failures[n].path = (char *)rows[i].rel_path;
7386+
failures[n].reason = (char *)rows[i].detail;
7387+
failures[n].phase = (char *)rows[i].kind;
7388+
n++;
7389+
}
7390+
7391+
add_skipped_summary(doc, root, failures, failure_count, logfile);
7392+
add_parse_partial_summary(doc, root, failures, failure_count);
7393+
free(failures);
7394+
cbm_store_free_coverage(rows, row_count);
7395+
return true;
7396+
}
7397+
73507398
/* Write the FULL (uncapped) skip list to a per-run logfile — ONLY when >=1 file
73517399
* was skipped (no logfile on a clean run). Location:
73527400
* $CBM_INDEX_LOG (override) else <cache_dir>/logs/<project>-<epoch>.log
@@ -7406,8 +7454,6 @@ static bool build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *
74067454
const cbm_file_error_t *file_errors, int file_error_count,
74077455
const char *logfile) {
74087456
add_excluded_summary(doc, root, excluded_dirs, excluded_count);
7409-
add_skipped_summary(doc, root, file_errors, file_error_count, logfile);
7410-
add_parse_partial_summary(doc, root, file_errors, file_error_count);
74117457
add_not_indexed_files_summary(doc, root, p);
74127458

74137459
int exp_nodes = -1;
@@ -7418,6 +7464,10 @@ static bool build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *
74187464
const int min_floor = CBM_DUMP_VERIFY_MIN_FLOOR;
74197465

74207466
cbm_store_t *store = resolve_store(srv, project_name);
7467+
if (!store || !add_persisted_failure_summaries(doc, root, store, project_name, logfile)) {
7468+
add_skipped_summary(doc, root, file_errors, file_error_count, logfile);
7469+
add_parse_partial_summary(doc, root, file_errors, file_error_count);
7470+
}
74217471
int nodes = 0;
74227472
int edges = 0;
74237473
bool degraded = false;

tests/test_index_resilience.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,34 @@ TEST(index_parse_partial_reported) {
439439
/* Clean neighbors still extract. */
440440
int funcs = rh_count_label(store, lp.project, "Function");
441441
ASSERT_GTE(funcs, 1);
442+
cbm_store_close(store);
443+
store = NULL;
444+
445+
/* An incremental run that changes only a clean neighbor has no new parser
446+
* errors, but its response must still describe the persisted current
447+
* coverage state for the untouched partial file. */
448+
struct timespec delay = {.tv_sec = 0, .tv_nsec = INCR_FIX_SLEEP_NS};
449+
nanosleep(&delay, NULL);
450+
ri_write_text(lp.tmpdir, "good.py", "def alpha():\n return 2\n");
451+
char iargs[700];
452+
snprintf(iargs, sizeof(iargs), "{\"repo_path\":\"%s\"}", lp.tmpdir);
453+
char *iresp = cbm_mcp_handle_tool(lp.srv, "index_repository", iargs);
454+
ASSERT_NOT_NULL(iresp);
455+
yyjson_doc *idoc = yyjson_read(iresp, strlen(iresp), 0);
456+
ASSERT_NOT_NULL(idoc);
457+
yyjson_val *isc = yyjson_obj_get(yyjson_doc_get_root(idoc), "structuredContent");
458+
ASSERT_NOT_NULL(isc);
459+
ASSERT_GTE(yyjson_get_int(yyjson_obj_get(isc, "parse_partial_count")), 1);
460+
yyjson_val *ipp = yyjson_obj_get(isc, "parse_partial");
461+
ASSERT_NOT_NULL(ipp);
462+
char *ipp_json = yyjson_val_write(ipp, 0, NULL);
463+
ASSERT_NOT_NULL(ipp_json);
464+
ASSERT_NOT_NULL(strstr(ipp_json, "split.c"));
465+
free(ipp_json);
466+
/* The coverage is persisted state, but the logfile remains per-run. */
467+
ASSERT_NULL(yyjson_obj_get(isc, "logfile"));
468+
yyjson_doc_free(idoc);
469+
free(iresp);
442470

443471
yyjson_doc_free(d);
444472
free(resp);

0 commit comments

Comments
 (0)