Skip to content

Commit 9b10ba2

Browse files
committed
fix(mcp): answer a bare name with every arity of a function
Two query paths read a bare function name, and neither could see an arity fence. get_code_snippet returned one body. Before arity was part of identity, fetch/1, fetch/2 and fetch/3 were one node and it returned whichever survived the collision, with no ambiguity signal. Now they are three nodes, and a bare `fetch` answers that the name is ambiguous, with one suggestion per arity. pick_resolved_node gains `fold_arity`, and get_code_snippet passes false. trace_call_path wants the opposite. "Who calls fetch" wants the callers of every arity, and bfs_union_same_name already unions the seeds it is given, so it passes true and candidates whose QNs are equal once the fence is removed fold into one logical symbol, the same way a body-less `.d.ts` stub already folds into its implementation. Two genuinely different same-named functions live in different containers, so their stripped QNs still differ and they are still reported ambiguous. The flag gates both tallies, the top-score tie and the rival-definition count, because either one alone decides the answer. Honouring it in only one left ambiguity resting on whether two clauses happened to have the same line count. For the same reason a fenced QN counts as a rival definition even with a one-line span. The body-less rule exists for ambient declarations; a fence is minted only from a definition head the extractor actually saw, so `def size(x), do: byte_size(x)` is a whole definition that merely occupies one line. Judging it by span would go back to deciding the answer by how many lines a body happens to take up. cbm_store_find_nodes_by_qn_suffix gains a third alternative matching the fenced forms of the same dotted tail. Without it, get_code_snippet("fetch"), the bare-name convention every agent is told to use, reached no arity of an overloaded function. '#' is not a LIKE metacharacter, so it is matched literally. On a three-arity fixture, get_code_snippet("fetch") answered with one arity's body and status "ok" before this commit and answers status "ambiguous" with one suggestion per arity after it, whatever the relative body lengths. trace_call_path is deliberately unchanged: it keeps folding the arities into one union, which is why the two paths need a flag between them. tests/test_mcp.c: snippet_bare_name_every_arity_is_ambiguous_whatever_the_spans pins that the answer does not depend on relative body length, and snippet_arity_fold_still_traces_every_arity_under_one_bare_name pins the trace side of the same flag. tests/test_store_nodes.c: store_find_by_qn_suffix_matches_arity_fence pins the probe. Its content dependency is the module/arity identity commit, which mints the fenced QNs this commit teaches the MCP layer to read. Signed-off-by: Chad <4307099+BobbieBarker@users.noreply.github.com>
1 parent 4f5cb98 commit 9b10ba2

4 files changed

Lines changed: 348 additions & 19 deletions

File tree

‎src/mcp/mcp.c‎

Lines changed: 114 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8393,34 +8393,86 @@ static long node_resolution_score(const cbm_node_t *n) {
83938393
return label_rank * (long)RES_LABEL_WEIGHT + span;
83948394
}
83958395

8396-
/* A "real" callable definition: a Function/Method node with a non-empty body
8397-
* span (end_line > start_line). A body-less node (start_line == end_line) is an
8398-
* ambient declaration / signature stub — e.g. a TypeScript `.d.ts` declaration
8399-
* — which is a *fragment* of one logical symbol, not a distinct definition. The
8400-
* distinction lets pick_resolved_node union a stub with its real implementation
8401-
* (#546) while still treating two genuinely-different same-named functions as
8402-
* ambiguous rather than conflating their caller sets. */
8403-
static bool node_is_real_callable_def(const cbm_node_t *n) {
8396+
/* Copy a QN without its '#<digits>' arity fence (internal/cbm/helpers.c).
8397+
* Declared extern rather than via helpers.h, which would pull the extraction
8398+
* layer's header into the MCP TU — the pattern used for
8399+
* cbm_kind_in_set_free_cache below. */
8400+
extern size_t cbm_qn_strip_arity(char *out, size_t cap, const char *qn);
8401+
8402+
/* True when `qn` ends in an all-digit '#' fence ("...fetch#3"). A fence is
8403+
* minted only from a definition head the extractor actually saw, which is what
8404+
* makes it evidence below. Rust's cfg twin ("add#cfg(test)") is not one. */
8405+
static bool qn_has_arity_fence(const char *qn) {
8406+
if (!qn) {
8407+
return false;
8408+
}
8409+
const char *hash = strrchr(qn, '#');
8410+
if (!hash || !hash[SKIP_ONE]) {
8411+
return false;
8412+
}
8413+
for (const char *p = hash + SKIP_ONE; *p; p++) {
8414+
if (*p < '0' || *p > '9') {
8415+
return false;
8416+
}
8417+
}
8418+
return true;
8419+
}
8420+
8421+
/* Does this candidate count as a RIVAL definition of the name — a distinct
8422+
* implementation rather than a fragment of one logical symbol?
8423+
*
8424+
* A Function/Method node with a non-empty body span (end_line > start_line) is
8425+
* one. A body-less node (start_line == end_line) normally is not: it is an
8426+
* ambient declaration / signature stub — a TypeScript `.d.ts` declaration —
8427+
* which is why pick_resolved_node may union it with its real implementation
8428+
* (#546/#650) while still reporting two genuinely-different same-named
8429+
* functions as ambiguous rather than conflating their caller sets.
8430+
*
8431+
* An arity-fenced QN is the exception, and only where arities are being counted
8432+
* separately (fold_arity == false). The fence says the extractor saw a whole
8433+
* definition head, so `def size(x), do: byte_size(x)` — a complete one-line
8434+
* Elixir clause — is a real definition that merely occupies one line. Judging it
8435+
* by span would put the answer straight back where the flag took it from:
8436+
* decided by how many lines a body happens to take up. */
8437+
static bool node_is_rival_def(const cbm_node_t *n, bool fold_arity) {
84048438
if (!n->label) {
84058439
return false;
84068440
}
84078441
if (strcmp(n->label, "Function") != 0 && strcmp(n->label, "Method") != 0) {
84088442
return false;
84098443
}
8410-
return (long)n->end_line - (long)n->start_line > 0;
8444+
if ((long)n->end_line - (long)n->start_line > 0) {
8445+
return true;
8446+
}
8447+
return !fold_arity && qn_has_arity_fence(n->qualified_name);
84118448
}
84128449

84138450
/* Pick the best-resolving node among name matches. Sets *ambiguous when the
84148451
* matches can't be reduced to one logical symbol, so resolution never silently
84158452
* traces (or conflates) the wrong same-named node:
84168453
* 1. the top score is shared by >1 candidate (a genuine rank/span tie), or
8417-
* 2. two or more *real* callable definitions share the name — distinct
8454+
* 2. two or more RIVAL definitions share the name (node_is_rival_def) — distinct
84188455
* implementations, not a definition plus its body-less stub(s).
84198456
* Rule 2 completes rule 1: without it, two same-named functions whose bodies
84208457
* differ in length score differently, dodge the tie, and get their caller sets
84218458
* unioned by bfs_union_same_name (#546) into one confidently-conflated answer.
84228459
* Body-less .d.ts stubs still union with their implementation (#650). */
8423-
static int pick_resolved_node(const cbm_node_t *nodes, int count, bool *ambiguous) {
8460+
/* `fold_arity` says whether candidates that differ ONLY by their QN's arity
8461+
* fence count as one logical symbol. It gates BOTH tallies -- the top-score tie
8462+
* and the real-definition count -- because either one alone decides the answer,
8463+
* and a flag honoured by only one of them makes the outcome depend on the other:
8464+
* folding real definitions unconditionally left ambiguity resting on whether two
8465+
* clauses happened to score equally, i.e. on whether their bodies happened to
8466+
* have the same line count.
8467+
*
8468+
* trace_path passes true: "who calls fetch" wants the callers of every arity,
8469+
* and bfs_union_same_name already unions the seeds it is given. get_code_snippet
8470+
* passes false: it returns ONE body, and fetch/1 and fetch/3 have different
8471+
* bodies, so listing all three fenced QNs as suggestions is the honest answer --
8472+
* the answer it used to give was whichever arity's node survived the collision,
8473+
* with no ambiguity signal at all. */
8474+
static int pick_resolved_node(const cbm_node_t *nodes, int count, bool fold_arity,
8475+
bool *ambiguous) {
84248476
*ambiguous = false;
84258477
if (count <= 1) {
84268478
return 0;
@@ -8436,11 +8488,58 @@ static int pick_resolved_node(const cbm_node_t *nodes, int count, bool *ambiguou
84368488
}
84378489
int top_count = 0;
84388490
int real_def_count = 0;
8491+
/* With fold_arity, a tie between two arities of one function is not a tie
8492+
* at all: the seeds get unioned, so which one ranks first is immaterial. */
8493+
const char *top_stripped_of = NULL;
8494+
char top_stripped[CBM_SZ_512];
8495+
char top_probe[CBM_SZ_512];
8496+
/* Under fold_arity, arities of one function are ONE logical symbol. Elixir's
8497+
* fetch/1 and fetch/3 are distinct NODES (that is the whole point of the
8498+
* arity fence) but they are the same name in the same container, and a
8499+
* caller that unions its seeds wants all of them, so two candidates whose
8500+
* QNs are equal once the fence is removed are folded here, the same way a
8501+
* .d.ts stub folds into its implementation above. Two genuinely different
8502+
* same-named functions live in different containers, so their stripped QNs
8503+
* still differ and they are still reported ambiguous.
8504+
*
8505+
* Without fold_arity the fold does not happen at all: each arity is counted,
8506+
* so a bare name naming several of them is ambiguous and gets one suggestion
8507+
* per arity. That is the honest answer for a caller returning a single body,
8508+
* and it is a deliberate difference from the folding branch, not an accident
8509+
* of which arity happened to rank first. */
8510+
const char *first_stripped_of = NULL;
8511+
char first_stripped[CBM_SZ_512];
8512+
char stripped[CBM_SZ_512];
84398513
for (int i = 0; i < count; i++) {
84408514
if (node_resolution_score(&nodes[i]) == best_score) {
8441-
top_count++;
8515+
if (!fold_arity) {
8516+
top_count++;
8517+
} else {
8518+
cbm_qn_strip_arity(top_probe, sizeof(top_probe), nodes[i].qualified_name);
8519+
if (!top_stripped_of) {
8520+
(void)snprintf(top_stripped, sizeof(top_stripped), "%s", top_probe);
8521+
top_stripped_of = top_stripped;
8522+
top_count++;
8523+
} else if (strcmp(top_stripped, top_probe) != 0) {
8524+
top_count++;
8525+
}
8526+
}
84428527
}
8443-
if (node_is_real_callable_def(&nodes[i])) {
8528+
if (!node_is_rival_def(&nodes[i], fold_arity)) {
8529+
continue;
8530+
}
8531+
if (!fold_arity) {
8532+
/* Every arity is its own definition with its own body, so a caller
8533+
* that returns one body must be told there is more than one. */
8534+
real_def_count++;
8535+
continue;
8536+
}
8537+
cbm_qn_strip_arity(stripped, sizeof(stripped), nodes[i].qualified_name);
8538+
if (!first_stripped_of) {
8539+
(void)snprintf(first_stripped, sizeof(first_stripped), "%s", stripped);
8540+
first_stripped_of = first_stripped;
8541+
real_def_count++;
8542+
} else if (strcmp(first_stripped, stripped) != 0) {
84448543
real_def_count++;
84458544
}
84468545
}
@@ -9188,7 +9287,7 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
91889287
* ambiguity (rather than silently tracing nodes[0]) on a genuine tie — e.g.
91899288
* a C main() vs a same-named shell-script main(). */
91909289
bool trace_ambiguous = false;
9191-
int sel = pick_resolved_node(nodes, node_count, &trace_ambiguous);
9290+
int sel = pick_resolved_node(nodes, node_count, true, &trace_ambiguous);
91929291
if (trace_ambiguous) {
91939292
char *result = snippet_suggestions(func_name, nodes, node_count);
91949293
free(func_name);
@@ -12456,7 +12555,7 @@ static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) {
1245612555
* instead of forcing a disambiguation round trip; only a genuine tie still
1245712556
* returns suggestions. */
1245812557
bool snip_ambiguous = false;
12459-
int ssel = pick_resolved_node(suffix_nodes, suffix_count, &snip_ambiguous);
12558+
int ssel = pick_resolved_node(suffix_nodes, suffix_count, false, &snip_ambiguous);
1246012559
if (!snip_ambiguous) {
1246112560
copy_node(&suffix_nodes[ssel], &node);
1246212561
cbm_store_free_nodes(suffix_nodes, suffix_count);

‎src/store/store.c‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4383,17 +4383,27 @@ int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const
43834383
if (!s || !s->db) {
43844384
return CBM_STORE_ERR;
43854385
}
4386-
/* Match QNs ending with ".suffix" or exactly equal to suffix */
4386+
/* Match QNs ending with ".suffix" or exactly equal to suffix.
4387+
*
4388+
* A QN may carry an arity fence ("...Store.fetch#3"), so a third
4389+
* alternative matches the fenced forms of the same dotted tail. Without it
4390+
* get_code_snippet("fetch") -- the bare-name convention every agent is
4391+
* told to use -- reaches no arity of an overloaded function at all. The
4392+
* '#' is matched literally: it is not a LIKE metacharacter. */
43874393
char like_pattern[CBM_SZ_512];
4388-
snprintf(like_pattern, sizeof(like_pattern), "%%.%s", suffix);
4394+
(void)snprintf(like_pattern, sizeof(like_pattern), "%%.%s", suffix);
4395+
char arity_pattern[CBM_SZ_512];
4396+
(void)snprintf(arity_pattern, sizeof(arity_pattern), "%%.%s#%%", suffix);
43894397

43904398
const char *sql_with_project =
43914399
"SELECT id, project, label, name, qualified_name, file_path, "
43924400
"start_line, end_line, properties FROM nodes "
4393-
"WHERE project = ?1 AND (qualified_name LIKE ?2 OR qualified_name = ?3)";
4401+
"WHERE project = ?1 AND (qualified_name LIKE ?2 OR qualified_name = ?3 "
4402+
"OR qualified_name LIKE ?4)";
43944403
const char *sql_any = "SELECT id, project, label, name, qualified_name, file_path, "
43954404
"start_line, end_line, properties FROM nodes "
4396-
"WHERE (qualified_name LIKE ?1 OR qualified_name = ?2)";
4405+
"WHERE (qualified_name LIKE ?1 OR qualified_name = ?2 "
4406+
"OR qualified_name LIKE ?3)";
43974407

43984408
sqlite3_stmt *stmt = NULL;
43994409
int rc =
@@ -4407,9 +4417,11 @@ int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const
44074417
bind_text(stmt, SKIP_ONE, project);
44084418
bind_text(stmt, ST_COL_2, like_pattern);
44094419
bind_text(stmt, ST_COL_3, suffix);
4420+
bind_text(stmt, ST_COL_4, arity_pattern);
44104421
} else {
44114422
bind_text(stmt, SKIP_ONE, like_pattern);
44124423
bind_text(stmt, ST_COL_2, suffix);
4424+
bind_text(stmt, ST_COL_3, arity_pattern);
44134425
}
44144426

44154427
int cap = ST_INIT_CAP_8;

0 commit comments

Comments
 (0)