@@ -12906,6 +12906,154 @@ static char *snippet_budget_floor(bool json_format, int max_output_tokens) {
1290612906 return json;
1290712907}
1290812908
12909+ /* Doc -> code candidates (doc_link_candidates): on a Section, the functions,
12910+ * whole files and the home folder it is possibly about; on a function or
12911+ * method, the sections that possibly describe it; on a file (get_file_outline),
12912+ * the sections that possibly describe the file or a folder holding it --
12913+ * highest p first. Candidates with a judged probability for the agent to
12914+ * verify, never links. A row whose other end no longer resolves (a delta
12915+ * update renamed or removed it) is skipped. */
12916+ enum { MCP_DOC_CANDIDATES_MAX = 5, MCP_DOC_CANDIDATES_SECTION_MAX = 10 };
12917+ static const char MCP_DOC_CANDIDATES_NOTE[] =
12918+ "doc<->code candidates by shared terms or the doc's folder, not links: p is the judged "
12919+ "probability the section is about the target; verify";
12920+
12921+ /* The evidence fields of a candidate row as shown: kind, position, and (on
12922+ * the section side) the shared terms and name tokens. */
12923+ static void doc_candidate_evidence(yyjson_mut_doc *doc, yyjson_mut_val *o, const char *evidence,
12924+ bool counts) {
12925+ yyjson_doc *ev = evidence ? yyjson_read(evidence, strlen(evidence), 0) : NULL;
12926+ yyjson_val *root = ev ? yyjson_doc_get_root(ev) : NULL;
12927+ static const char *const STRS[] = {"kind", "position", NULL};
12928+ for (int k = 0; root && STRS[k]; k++) {
12929+ yyjson_val *v = yyjson_obj_get(root, STRS[k]);
12930+ if (v && yyjson_is_str(v)) {
12931+ yyjson_mut_obj_add_strcpy(doc, o, STRS[k], yyjson_get_str(v));
12932+ }
12933+ }
12934+ static const char *const INTS[] = {"shared_terms", "name_tokens", NULL};
12935+ for (int k = 0; counts && root && INTS[k]; k++) {
12936+ yyjson_val *v = yyjson_obj_get(root, INTS[k]);
12937+ if (v && yyjson_is_int(v)) {
12938+ yyjson_mut_obj_add_int(doc, o, INTS[k], (int)yyjson_get_int(v));
12939+ }
12940+ }
12941+ yyjson_doc_free(ev);
12942+ }
12943+
12944+ /* rows -> an array of shown candidates; the other end of each row is its
12945+ * target (by_section) or its section. Returns how many resolved. */
12946+ static int doc_candidates_array(yyjson_mut_doc *doc, yyjson_mut_val *arr, cbm_store_t *store,
12947+ const char *project, const cbm_doc_candidate_t *rows, int n,
12948+ bool by_section) {
12949+ int added = 0;
12950+ for (int i = 0; i < n; i++) {
12951+ cbm_node_t other = {0};
12952+ if (cbm_store_find_node_by_qn(store, project,
12953+ by_section ? rows[i].target_qn : rows[i].section_qn,
12954+ &other) != CBM_STORE_OK) {
12955+ continue;
12956+ }
12957+ yyjson_mut_val *o = yyjson_mut_obj(doc);
12958+ yyjson_mut_obj_add_strcpy(doc, o, by_section ? "qualified_name" : "section",
12959+ other.qualified_name ? other.qualified_name : "");
12960+ if (by_section) {
12961+ yyjson_mut_obj_add_strcpy(doc, o, "label", other.label ? other.label : "");
12962+ }
12963+ yyjson_mut_obj_add_strcpy(doc, o, "file_path", other.file_path ? other.file_path : "");
12964+ yyjson_mut_obj_add_int(doc, o, "start_line", other.start_line);
12965+ if (by_section) {
12966+ yyjson_mut_obj_add_int(doc, o, "end_line", other.end_line);
12967+ }
12968+ yyjson_mut_obj_add_real(doc, o, "p", (double)(int)(rows[i].p * 1000.0 + 0.5) / 1000.0);
12969+ doc_candidate_evidence(doc, o, rows[i].evidence, by_section);
12970+ yyjson_mut_arr_append(arr, o);
12971+ added++;
12972+ free_node_contents(&other);
12973+ }
12974+ return added;
12975+ }
12976+
12977+ static void add_doc_candidates(yyjson_mut_doc *doc, yyjson_mut_val *root, cbm_store_t *store,
12978+ const cbm_node_t *node) {
12979+ if (!store || !node->label || !node->qualified_name || !node->project) {
12980+ return;
12981+ }
12982+ bool section = strcmp(node->label, "Section") == 0;
12983+ if (!section && strcmp(node->label, "Function") != 0 && strcmp(node->label, "Method") != 0) {
12984+ return;
12985+ }
12986+ cbm_doc_candidate_t *rows = NULL;
12987+ int n = 0;
12988+ if (cbm_store_doc_candidates_get(store, node->project, section ? node->qualified_name : NULL,
12989+ section ? NULL : node->qualified_name,
12990+ section ? MCP_DOC_CANDIDATES_SECTION_MAX
12991+ : MCP_DOC_CANDIDATES_MAX,
12992+ &rows, &n) != CBM_STORE_OK) {
12993+ return;
12994+ }
12995+ yyjson_mut_val *arr = yyjson_mut_arr(doc);
12996+ int added = doc_candidates_array(doc, arr, store, node->project, rows, n, section);
12997+ cbm_store_doc_candidates_free(rows, n);
12998+ if (added > 0) {
12999+ yyjson_mut_obj_add_val(doc, root, section ? "possibly_about" : "possibly_described_in",
13000+ arr);
13001+ yyjson_mut_obj_add_str(doc, root, "candidates_note", MCP_DOC_CANDIDATES_NOTE);
13002+ }
13003+ }
13004+
13005+ /* get_file_outline: the sections possibly about the file as a whole or about
13006+ * a folder holding it (NULL: none). */
13007+ static yyjson_mut_val *file_doc_candidates(yyjson_mut_doc *doc, cbm_store_t *store,
13008+ const char *project, const char *path) {
13009+ cbm_doc_candidate_t *rows = NULL;
13010+ int n = 0;
13011+ if (cbm_store_doc_candidates_for_path(store, project, path, MCP_DOC_CANDIDATES_MAX, &rows,
13012+ &n) != CBM_STORE_OK) {
13013+ return NULL;
13014+ }
13015+ yyjson_mut_val *arr = yyjson_mut_arr(doc);
13016+ int added = doc_candidates_array(doc, arr, store, project, rows, n, false);
13017+ cbm_store_doc_candidates_free(rows, n);
13018+ return added > 0 ? arr : NULL;
13019+ }
13020+
13021+ static const char *mut_str(yyjson_mut_val *o, const char *key) {
13022+ yyjson_mut_val *v = yyjson_mut_obj_get(o, key);
13023+ return v && yyjson_mut_is_str(v) ? yyjson_mut_get_str(v) : "";
13024+ }
13025+
13026+ /* The tree form of file_doc_candidates. */
13027+ static void file_doc_candidates_tree(cbm_sb_t *sb, cbm_store_t *store, const char *project,
13028+ const char *path) {
13029+ yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
13030+ yyjson_mut_val *arr = doc ? file_doc_candidates(doc, store, project, path) : NULL;
13031+ if (arr) {
13032+ static const char *const columns[] = {"section", "file_path", "line", "p", "kind"};
13033+ cbm_tree_table_header(sb, "possibly_described_in", (int)yyjson_mut_arr_size(arr), columns,
13034+ (int)(sizeof(columns) / sizeof(columns[0])));
13035+ size_t i = 0;
13036+ size_t max = 0;
13037+ yyjson_mut_val *o = NULL;
13038+ yyjson_mut_arr_foreach(arr, i, max, o) {
13039+ char line[CBM_SZ_32];
13040+ char p[CBM_SZ_32];
13041+ snprintf(line, sizeof(line), "%d",
13042+ (int)yyjson_mut_get_int(yyjson_mut_obj_get(o, "start_line")));
13043+ snprintf(p, sizeof(p), "%.2f", yyjson_mut_get_real(yyjson_mut_obj_get(o, "p")));
13044+ cbm_tree_row_begin(sb);
13045+ cbm_tree_cell_str(sb, mut_str(o, "section"), true);
13046+ cbm_tree_cell_str(sb, mut_str(o, "file_path"), false);
13047+ cbm_tree_cell_str(sb, line, false);
13048+ cbm_tree_cell_str(sb, p, false);
13049+ cbm_tree_cell_str(sb, mut_str(o, "kind"), false);
13050+ cbm_tree_row_end(sb);
13051+ }
13052+ cbm_tree_scalar_str(sb, "candidates_note", MCP_DOC_CANDIDATES_NOTE);
13053+ }
13054+ yyjson_mut_doc_free(doc);
13055+ }
13056+
1290913057static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node,
1291013058 const char *match_method, bool include_neighbors,
1291113059 cbm_node_t *alternatives, int alt_count, const char *args) {
@@ -13122,6 +13270,8 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node,
1312213270 yyjson_mut_obj_add_val(doc, root_obj, "alternatives", arr);
1312313271 }
1312413272
13273+ add_doc_candidates(doc, root_obj, store, node);
13274+
1312513275 bool bounded_default = max_output_tokens > 0;
1312613276 size_t snippet_byte_budget =
1312713277 bounded_default ? (size_t)max_output_tokens * (size_t)MCP_OUTPUT_BYTES_PER_TOKEN_ESTIMATE
@@ -13387,6 +13537,9 @@ static char *handle_get_file_outline(cbm_mcp_server_t *srv, const char *args) {
1338713537 cbm_tree_scalar_int(&sb, "limit", limit);
1338813538 cbm_tree_scalar_int(&sb, "returned", row_count);
1338913539 cbm_tree_scalar_bool(&sb, "has_more", (int64_t)offset + row_count < total);
13540+ if (offset == 0) {
13541+ file_doc_candidates_tree(&sb, store, project, normalized_path);
13542+ }
1339013543 payload = cbm_sb_finish(&sb);
1339113544 } else {
1339213545 yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
@@ -13422,6 +13575,12 @@ static char *handle_get_file_outline(cbm_mcp_server_t *srv, const char *args) {
1342213575 yyjson_mut_obj_add_int(doc, object, "limit", limit);
1342313576 yyjson_mut_obj_add_int(doc, object, "returned", row_count);
1342413577 yyjson_mut_obj_add_bool(doc, object, "has_more", (int64_t)offset + row_count < total);
13578+ yyjson_mut_val *described =
13579+ offset == 0 ? file_doc_candidates(doc, store, project, normalized_path) : NULL;
13580+ if (described) {
13581+ yyjson_mut_obj_add_val(doc, object, "possibly_described_in", described);
13582+ yyjson_mut_obj_add_str(doc, object, "candidates_note", MCP_DOC_CANDIDATES_NOTE);
13583+ }
1342513584 payload = yy_doc_to_str(doc);
1342613585 yyjson_mut_doc_free(doc);
1342713586 }
0 commit comments