@@ -896,6 +896,85 @@ static void add_git_context_json(yyjson_mut_doc *doc, yyjson_mut_val *obj, const
896896 cbm_git_context_free (& ctx );
897897}
898898
899+ static void add_index_evidence_json (yyjson_mut_doc * doc , yyjson_mut_val * root ,
900+ const cbm_project_t * proj ) {
901+ yyjson_mut_val * evidence = yyjson_mut_obj (doc );
902+ yyjson_mut_val * snap = yyjson_mut_obj (doc );
903+ const char * indexed_head = (proj && proj -> indexed_git_head && proj -> indexed_git_head [0 ])
904+ ? proj -> indexed_git_head
905+ : NULL ;
906+ cbm_git_context_t ctx = {0 };
907+ int git_rc = cbm_git_context_resolve (proj ? proj -> root_path : NULL , & ctx );
908+ bool dirty = false;
909+ int dirty_rc = (proj && git_rc == 0 && ctx .is_git )
910+ ? cbm_git_tracked_dirty (proj -> root_path , & dirty )
911+ : CBM_NOT_FOUND ;
912+
913+ add_git_context_string (doc , snap , "indexed_at" , proj ? proj -> indexed_at : NULL );
914+ add_git_context_string (doc , snap , "indexed_git_head" , indexed_head );
915+ add_git_context_string (
916+ doc , snap , "current_git_head" ,
917+ (git_rc == 0 && ctx .is_git && ctx .head_sha && ctx .head_sha [0 ]) ? ctx .head_sha : NULL );
918+ const char * repo_state = "unavailable" ;
919+ if (git_rc == 0 && ctx .root_exists && !ctx .is_git ) {
920+ repo_state = "not_git" ;
921+ } else if (git_rc == 0 && ctx .is_git && dirty_rc == 0 ) {
922+ repo_state = dirty ? "dirty" : "clean" ;
923+ }
924+ yyjson_mut_obj_add_str (doc , snap , "repository_state" , repo_state );
925+ if (indexed_head && git_rc == 0 && ctx .is_git && ctx .head_sha && ctx .head_sha [0 ]) {
926+ yyjson_mut_obj_add_bool (doc , snap , "snapshot_matches_current_head" ,
927+ strcmp (indexed_head , ctx .head_sha ) == 0 );
928+ } else {
929+ yyjson_mut_obj_add_null (doc , snap , "snapshot_matches_current_head" );
930+ }
931+ if (indexed_head && git_rc == 0 && ctx .is_git && ctx .head_sha && ctx .head_sha [0 ] &&
932+ dirty_rc == 0 ) {
933+ yyjson_mut_obj_add_bool (doc , snap , "snapshot_matches_working_tree" ,
934+ strcmp (indexed_head , ctx .head_sha ) == 0 && !dirty );
935+ } else {
936+ yyjson_mut_obj_add_null (doc , snap , "snapshot_matches_working_tree" );
937+ }
938+ const char * freshness = "unknown" ;
939+ if (indexed_head && git_rc == 0 && ctx .is_git && ctx .head_sha && ctx .head_sha [0 ] &&
940+ dirty_rc == 0 ) {
941+ if (strcmp (indexed_head , ctx .head_sha ) != 0 ) {
942+ freshness = "head_changed" ;
943+ } else {
944+ freshness = dirty ? "working_tree_changed" : "current" ;
945+ }
946+ } else if (git_rc == 0 && ctx .root_exists && !ctx .is_git ) {
947+ freshness = "unknown" ;
948+ }
949+ yyjson_mut_obj_add_str (doc , snap , "freshness" , freshness );
950+ yyjson_mut_obj_add_val (doc , evidence , "index_snapshot" , snap );
951+
952+ yyjson_mut_val * cov = yyjson_mut_obj (doc );
953+ int discovered = proj ? proj -> files_discovered : 0 ;
954+ int indexed = proj ? proj -> files_indexed : 0 ;
955+ int excluded = proj ? proj -> files_excluded : 0 ;
956+ int failed = proj ? proj -> files_failed : 0 ;
957+ yyjson_mut_obj_add_int (doc , cov , "files_discovered" , discovered );
958+ yyjson_mut_obj_add_int (doc , cov , "files_indexed" , indexed );
959+ yyjson_mut_obj_add_int (doc , cov , "files_excluded" , excluded );
960+ yyjson_mut_obj_add_int (doc , cov , "files_failed" , failed );
961+ yyjson_mut_obj_add_str (doc , cov , "coverage_status" ,
962+ failed > 0 || excluded > 0 ? "partial"
963+ : discovered > 0 && indexed == discovered ? "complete"
964+ : "unknown" );
965+ yyjson_mut_obj_add_val (doc , evidence , "coverage" , cov );
966+ yyjson_mut_val * limits = yyjson_mut_arr (doc );
967+ yyjson_mut_val * lim = yyjson_mut_obj (doc );
968+ yyjson_mut_obj_add_str (doc , lim , "code" , "UNTRACKED_FILES_NOT_COMPARED" );
969+ yyjson_mut_obj_add_str (doc , lim , "message" ,
970+ "Working-tree freshness compares current HEAD and tracked "
971+ "modifications; untracked files are not compared." );
972+ yyjson_mut_arr_add_val (limits , lim );
973+ yyjson_mut_obj_add_val (doc , evidence , "limitations" , limits );
974+ yyjson_mut_obj_add_val (doc , root , "evidence" , evidence );
975+ cbm_git_context_free (& ctx );
976+ }
977+
899978/* Build a helpful error listing available projects. Caller must free() result. */
900979static char * build_project_list_error (const char * reason ) {
901980 char dir_path [CBM_SZ_1K ];
@@ -1781,9 +1860,8 @@ static char *handle_index_status(cbm_mcp_server_t *srv, const char *args) {
17811860 yyjson_mut_obj_add_strcpy (doc , root , "root_path" ,
17821861 proj_info .root_path ? proj_info .root_path : "" );
17831862 add_git_context_json (doc , root , proj_info .root_path );
1784- safe_str_free (& proj_info .name );
1785- safe_str_free (& proj_info .indexed_at );
1786- safe_str_free (& proj_info .root_path );
1863+ add_index_evidence_json (doc , root , & proj_info );
1864+ cbm_project_free_fields (& proj_info );
17871865 }
17881866 if (nodes == 0 ) {
17891867 yyjson_mut_obj_add_str (
@@ -2349,6 +2427,79 @@ static int pick_resolved_node(const cbm_node_t *nodes, int count, bool *ambiguou
23492427 return best ;
23502428}
23512429
2430+ static const char * edge_evidence_status (const char * strategy , double confidence , int candidates ) {
2431+ if (!strategy || !strategy [0 ]) {
2432+ return "unavailable" ;
2433+ }
2434+ if (candidates > 1 ) {
2435+ return "ambiguous" ;
2436+ }
2437+ if (strstr (strategy , "heur" ) || strstr (strategy , "fuzzy" ) || confidence < 0.8 ) {
2438+ return "inferred" ;
2439+ }
2440+ return "verified" ;
2441+ }
2442+
2443+ static const char * edge_resolution_strategy (const char * strategy ) {
2444+ if (!strategy || !strategy [0 ]) {
2445+ return "unknown" ;
2446+ }
2447+ if (strstr (strategy , "lsp" )) {
2448+ return "hybrid_lsp" ;
2449+ }
2450+ if (strstr (strategy , "import" ) || strstr (strategy , "same_module" ) ||
2451+ strstr (strategy , "receiver" )) {
2452+ return "direct_ast" ;
2453+ }
2454+ if (strstr (strategy , "fuzzy" ) || strstr (strategy , "heur" )) {
2455+ return "heuristic" ;
2456+ }
2457+ return strategy ;
2458+ }
2459+
2460+ static yyjson_mut_val * trace_edges_to_json_array (yyjson_mut_doc * doc , cbm_traverse_result_t * tr ) {
2461+ yyjson_mut_val * arr = yyjson_mut_arr (doc );
2462+ for (int i = 0 ; i < tr -> edge_count ; i ++ ) {
2463+ yyjson_mut_val * item = yyjson_mut_obj (doc );
2464+ yyjson_mut_obj_add_str (doc , item , "from" ,
2465+ tr -> edges [i ].from_name ? tr -> edges [i ].from_name : "" );
2466+ yyjson_mut_obj_add_str (doc , item , "to" , tr -> edges [i ].to_name ? tr -> edges [i ].to_name : "" );
2467+ yyjson_mut_obj_add_str (doc , item , "type" , tr -> edges [i ].type ? tr -> edges [i ].type : "" );
2468+ yyjson_mut_val * edge = yyjson_mut_obj (doc );
2469+ const char * props = tr -> edges [i ].properties_json ;
2470+ yyjson_doc * pdoc = props ? yyjson_read (props , strlen (props ), 0 ) : NULL ;
2471+ yyjson_val * proot = pdoc ? yyjson_doc_get_root (pdoc ) : NULL ;
2472+ yyjson_val * v = proot ? yyjson_obj_get (proot , "strategy" ) : NULL ;
2473+ const char * strategy = yyjson_is_str (v ) ? yyjson_get_str (v ) : NULL ;
2474+ v = proot ? yyjson_obj_get (proot , "confidence" ) : NULL ;
2475+ bool has_conf = yyjson_is_num (v );
2476+ double conf = has_conf ? yyjson_get_num (v ) : 0.0 ;
2477+ v = proot ? yyjson_obj_get (proot , "candidates" ) : NULL ;
2478+ int candidates = yyjson_is_int (v ) ? (int )yyjson_get_int (v ) : 0 ;
2479+ yyjson_mut_obj_add_str (doc , edge , "resolution_strategy" ,
2480+ edge_resolution_strategy (strategy ));
2481+ if (has_conf ) {
2482+ yyjson_mut_obj_add_real (doc , edge , "confidence" , conf );
2483+ } else {
2484+ yyjson_mut_obj_add_null (doc , edge , "confidence" );
2485+ }
2486+ if (candidates > 0 ) {
2487+ yyjson_mut_obj_add_int (doc , edge , "candidate_count" , candidates );
2488+ } else {
2489+ yyjson_mut_obj_add_null (doc , edge , "candidate_count" );
2490+ }
2491+ yyjson_mut_obj_add_null (doc , edge , "source_location" );
2492+ yyjson_mut_obj_add_str (doc , edge , "evidence_status" ,
2493+ edge_evidence_status (strategy , conf , candidates ));
2494+ yyjson_mut_obj_add_val (doc , item , "edge" , edge );
2495+ yyjson_mut_arr_add_val (arr , item );
2496+ if (pdoc ) {
2497+ yyjson_doc_free (pdoc );
2498+ }
2499+ }
2500+ return arr ;
2501+ }
2502+
23522503static char * handle_trace_call_path (cbm_mcp_server_t * srv , const char * args ) {
23532504 char * func_name = cbm_mcp_get_string_arg (args , "function_name" );
23542505 char * project = cbm_mcp_get_string_arg (args , "project" );
@@ -2486,6 +2637,16 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
24862637 yyjson_mut_obj_add_val (doc , root , "callers" ,
24872638 bfs_to_json_array (doc , & tr_in , risk_labels , include_tests ));
24882639 }
2640+ yyjson_mut_val * edge_evidence = yyjson_mut_obj (doc );
2641+ if (do_outbound ) {
2642+ yyjson_mut_obj_add_val (doc , edge_evidence , "outbound" ,
2643+ trace_edges_to_json_array (doc , & tr_out ));
2644+ }
2645+ if (do_inbound ) {
2646+ yyjson_mut_obj_add_val (doc , edge_evidence , "inbound" ,
2647+ trace_edges_to_json_array (doc , & tr_in ));
2648+ }
2649+ yyjson_mut_obj_add_val (doc , root , "edge_evidence" , edge_evidence );
24892650
24902651 /* Serialize BEFORE freeing traversal results (yyjson borrows strings) */
24912652 char * json = yy_doc_to_str (doc );
0 commit comments