@@ -6594,6 +6594,43 @@ static void coverage_add_row_json(yyjson_mut_doc *doc, yyjson_mut_val *array,
65946594 yyjson_mut_arr_add_val(array, item);
65956595}
65966596
6597+ enum { COVERAGE_EXCLUDED_PREFIX_LENGTH = 11 };
6598+
6599+ enum {
6600+ COVERAGE_STATUS_PARSE,
6601+ COVERAGE_STATUS_EXCLUDED,
6602+ COVERAGE_STATUS_SKIPPED,
6603+ COVERAGE_STATUS_UNRESOLVED,
6604+ COVERAGE_STATUS_PASSES
6605+ };
6606+
6607+ static const char *coverage_row_status(const char *kind, int pass) {
6608+ kind = kind ? kind : "";
6609+ if (pass == COVERAGE_STATUS_PARSE) {
6610+ if (strcmp(kind, "parse_unusable") == 0) {
6611+ return "unusable";
6612+ }
6613+ if (strcmp(kind, "parse_partial") == 0) {
6614+ return "partial";
6615+ }
6616+ }
6617+ if (pass == COVERAGE_STATUS_EXCLUDED &&
6618+ strncmp(kind, "not_indexed", COVERAGE_EXCLUDED_PREFIX_LENGTH) == 0) {
6619+ return "excluded";
6620+ }
6621+ if (pass == COVERAGE_STATUS_SKIPPED && kind[0] && strcmp(kind, "unresolved_calls") != 0) {
6622+ return "skipped";
6623+ }
6624+ if (pass == COVERAGE_STATUS_UNRESOLVED && strcmp(kind, "unresolved_calls") == 0) {
6625+ return "unresolved_calls";
6626+ }
6627+ return NULL;
6628+ }
6629+
6630+ static bool coverage_unresolved_complete(bool have_meta, const cbm_coverage_meta_t *meta) {
6631+ return have_meta && meta->unresolved_calls_complete;
6632+ }
6633+
65976634static const char *coverage_status(const cbm_coverage_row_t *rows, int count,
65986635 const char *requested_path, const char *recording_status,
65996636 bool generation_matches, bool lookup_ok,
@@ -6608,29 +6645,14 @@ static const char *coverage_status(const cbm_coverage_row_t *rows, int count,
66086645 break;
66096646 }
66106647 }
6611- for (int pass = 0; pass < 3 ; pass++) {
6648+ for (int pass = 0; pass < COVERAGE_STATUS_PASSES ; pass++) {
66126649 for (int i = 0; i < count; i++) {
66136650 if (exact && (!rows[i].rel_path || strcmp(rows[i].rel_path, requested_path) != 0)) {
66146651 continue;
66156652 }
6616- const char *kind = rows[i].kind ? rows[i].kind : "";
6617- /* "parse_unusable" must be named here. Without its own case it
6618- * falls through to the catch-all below and reports "skipped",
6619- * which is wrong in the way that matters: the file WAS indexed. */
6620- if (pass == 0 && strcmp(kind, "parse_unusable") == 0) {
6621- return "unusable";
6622- }
6623- if (pass == 0 && strcmp(kind, "parse_partial") == 0) {
6624- return "partial";
6625- }
6626- if (pass == 0 && strcmp(kind, "unresolved_calls") == 0) {
6627- return "unresolved_calls";
6628- }
6629- if (pass == 1 && strncmp(kind, "not_indexed", 11) == 0) {
6630- return "excluded";
6631- }
6632- if (pass == 2 && kind[0]) {
6633- return "skipped";
6653+ const char *status = coverage_row_status(rows[i].kind, pass);
6654+ if (status) {
6655+ return status;
66346656 }
66356657 }
66366658 }
@@ -6736,6 +6758,8 @@ static char *handle_check_index_coverage(cbm_mcp_server_t *srv, const char *args
67366758 have_meta ? meta.ignored_files_total : 0);
67376759 yyjson_mut_obj_add_bool(doc, meta_obj, "hash_records_complete",
67386760 have_meta && meta.hash_records_complete);
6761+ yyjson_mut_obj_add_bool(doc, meta_obj, "unresolved_calls_complete",
6762+ coverage_unresolved_complete(have_meta, &meta));
67396763 yyjson_mut_obj_add_int(doc, meta_obj, "coverage_version",
67406764 have_meta ? meta.coverage_version : 0);
67416765 yyjson_mut_obj_add_bool(doc, meta_obj, "generation_matches", generation_matches);
@@ -9265,7 +9289,6 @@ static void trace_coverage_match_site(trace_coverage_match_t *match, const char
92659289 yyjson_val *site) {
92669290 const char *caller = yyjson_get_str(yyjson_obj_get(site, "caller"));
92679291 const char *leaf = yyjson_get_str(yyjson_obj_get(site, "leaf"));
9268- const char *candidate = yyjson_get_str(yyjson_obj_get(site, "candidate"));
92699292 const cbm_node_t *caller_node =
92709293 match->callers && caller ? cbm_ht_get(match->callers, caller) : NULL;
92719294 int line = yyjson_get_int(yyjson_obj_get(site, "line"));
@@ -9277,25 +9300,22 @@ static void trace_coverage_match_site(trace_coverage_match_t *match, const char
92779300 trace_coverage_may_target_project(match, leaf)) {
92789301 match->unresolved_out = true;
92799302 }
9280- /* A short-name collision is not evidence that the resolver considered
9281- * this traced target. */
9282- if (match->targets && candidate && cbm_ht_has(match->targets, candidate)) {
9283- match->unresolved_in = true;
9284- }
92859303}
92869304
9287- static void trace_coverage_match_row(trace_coverage_match_t *match, const cbm_coverage_row_t *row,
9288- const trace_coverage_scope_t *scope) {
9305+ static void trace_coverage_match_row(trace_coverage_match_t *match, const cbm_coverage_row_t *row) {
92899306 yyjson_doc *detail = yyjson_read(row->detail, strlen(row->detail), 0);
92909307 yyjson_val *sites = detail ? yyjson_doc_get_root(detail) : NULL;
92919308 if (!yyjson_is_arr(sites)) {
9292- match->unresolved_out = scope->do_outbound;
9293- match->unresolved_in = scope->do_inbound;
9309+ match->unresolved_out = true;
92949310 } else {
92959311 size_t idx;
92969312 size_t max;
92979313 yyjson_val *site;
92989314 yyjson_arr_foreach(sites, idx, max, site) {
9315+ if (!yyjson_is_obj(site) || yyjson_get_bool(yyjson_obj_get(site, "truncated"))) {
9316+ match->unresolved_out = true;
9317+ break;
9318+ }
92999319 trace_coverage_match_site(match, row->rel_path, site);
93009320 }
93019321 }
@@ -9304,32 +9324,69 @@ static void trace_coverage_match_row(trace_coverage_match_t *match, const cbm_co
93049324 }
93059325}
93069326
9307- static bool trace_coverage_scan_rows(cbm_store_t *store, const char *project,
9308- trace_coverage_match_t *match,
9309- const trace_coverage_scope_t *scope) {
9327+ typedef struct {
9328+ trace_coverage_match_t *match;
9329+ CBMHashTable *files;
9330+ bool ok;
9331+ } trace_coverage_query_t;
9332+
9333+ static void trace_coverage_query_file(const char *key, void *value, void *userdata) {
9334+ (void)key;
9335+ const cbm_node_t *caller = value;
9336+ trace_coverage_query_t *query = userdata;
9337+ const char *path = caller->file_path;
9338+ if (!query->ok || query->match->unresolved_out || !path || cbm_ht_has(query->files, path)) {
9339+ return;
9340+ }
9341+ cbm_ht_set(query->files, path, value);
9342+ if (!cbm_ht_has(query->files, path)) {
9343+ query->ok = false;
9344+ return;
9345+ }
93109346 cbm_coverage_row_t *rows = NULL;
9311- int row_count = 0;
9312- if (cbm_store_coverage_get_unresolved_calls(store, project, &rows, &row_count) !=
9313- CBM_STORE_OK) {
9314- return false;
9347+ int count = 0;
9348+ query->ok = cbm_store_coverage_get_unresolved_path(query->match->store, query->match->project,
9349+ path, &rows, &count) == CBM_STORE_OK;
9350+ for (int i = 0; query->ok && i < count; i++) {
9351+ trace_coverage_match_row(query->match, &rows[i]);
93159352 }
9316- for (int i = 0; i < row_count; i++) {
9317- if ((!scope->do_outbound || match->unresolved_out) &&
9318- (!scope->do_inbound || match->unresolved_in)) {
9319- break;
9353+ cbm_store_free_coverage(rows, count);
9354+ }
9355+
9356+ static void trace_coverage_query_candidate(const char *key, void *value, void *userdata) {
9357+ (void)value;
9358+ trace_coverage_query_t *query = userdata;
9359+ if (!query->ok || query->match->unresolved_in) {
9360+ return;
9361+ }
9362+ query->ok =
9363+ cbm_store_coverage_has_unresolved_candidate(query->match->store, query->match->project, key,
9364+ &query->match->unresolved_in) == CBM_STORE_OK;
9365+ }
9366+
9367+ static bool trace_coverage_query_evidence(trace_coverage_match_t *match,
9368+ const trace_coverage_scope_t *scope) {
9369+ trace_coverage_query_t query = {.match = match, .ok = true};
9370+ if (scope->do_outbound) {
9371+ query.files = cbm_ht_create(0);
9372+ if (!query.files) {
9373+ return false;
93209374 }
9321- trace_coverage_match_row(match, &rows[i], scope);
9375+ cbm_ht_foreach(match->callers, trace_coverage_query_file, &query);
9376+ cbm_ht_free(query.files);
93229377 }
9323- cbm_store_free_coverage(rows, row_count);
9324- return true;
9378+ if (scope->do_inbound) {
9379+ cbm_ht_foreach(match->targets, trace_coverage_query_candidate, &query);
9380+ }
9381+ return query.ok;
93259382}
93269383
93279384static bool trace_coverage_meta_available(cbm_store_t *store, const char *project) {
93289385 cbm_coverage_meta_t meta = {0};
93299386 int rc = cbm_store_coverage_meta_get(store, project, &meta);
93309387 bool available = rc == CBM_STORE_OK &&
93319388 meta.coverage_version >= CBM_UNRESOLVED_CALL_COVERAGE_VERSION &&
9332- meta.recording_status && strcmp(meta.recording_status, "unavailable") != 0 ;
9389+ meta.unresolved_calls_complete ;
93339390 cbm_store_coverage_meta_clear(&meta);
93349391 return available;
93359392}
@@ -9347,7 +9404,7 @@ static void trace_call_coverage(cbm_store_t *store, const char *project,
93479404 bool ok = trace_coverage_meta_available(store, project) &&
93489405 (!scope->do_outbound || match.callers) && (!scope->do_inbound || match.targets) &&
93499406 trace_coverage_build_sets(&match, scope) &&
9350- trace_coverage_scan_rows(store, project, &match, scope);
9407+ trace_coverage_query_evidence( &match, scope);
93519408 *unresolved_out = ok ? match.unresolved_out : scope->do_outbound;
93529409 *unresolved_in = ok ? match.unresolved_in : scope->do_inbound;
93539410 cbm_ht_free(match.callers);
0 commit comments