fix(explore): stop NL-question words from hijacking the named-symbol tier when they collide with real callables (#1252)

handleExplore's named-symbol seeding treats every identifier-shaped query
token as "a symbol the agent named" and grants its definition the
named-FIRST sort tier. Natural-language questions broke this assumption:
ordinary words exact-matched unrelated callables ("check" ->
WalCheckpointValve.check, "serve" -> query-worker serve, "initialize" ->
DatabaseConnection.initialize), and those collisions outranked — and within
the per-repo file budget fully displaced — the corroborated answer files,
forcing the agent back to Read/Grep. The >3-def single-pick fallback had
the same hole: on grpc, the #1064 flagship query "add a parameter to
NewClient" itself tiered balancerStateAggregator.add's file to slot #1.

Guard: a shape-precise token (camelCase, PascalCase, snake_case,
qualified) seeds unconditionally — it is an unambiguous symbol reference.
A bare lowercase word seeds only defs whose file another query token
co-names (that token is itself an exact symbol name defined in the same
file — the "check drain fire" sibling-bag shape), which an incidental
English-word collision never is. Applied by filtering cands ahead of both
branches so the overloaded-name fallback is covered too.

Validated per the retrieval playbook: deterministic probes on this repo
(collision queries fixed; sibling-bag and single-camelCase retained), and
baseline-vs-fixed probes on the #1064 repos — Alamofire and excalidraw
byte-identical, grpc improved (the add-collision file drops out and
clientconn.go + dialoptions.go lead). Full suite green.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-10 16:20:28 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 70b1be6a21
commit 8b82fe71f8
3 changed files with 165 additions and 1 deletions
+39 -1
View File
@@ -2611,6 +2611,36 @@ export class ToolHandler {
const lc = ct.toLowerCase();
return n.filePath.toLowerCase().includes(lc) || n.qualifiedName.toLowerCase().includes(lc);
});
// NL-stopword guard: this seeding treats every token as "a symbol the
// agent named", but explore also takes natural-language questions, whose
// ordinary English words collide with real callables — "…check the latest
// version…" exact-matched a lone `check()` method, which then earned the
// named-FIRST sort tier and displaced the corroborated answer files from
// the whole render budget (the agent fell back to Read). A shape-precise
// token (camelCase, PascalCase, snake_case, qualified) is an unambiguous
// symbol reference and seeds unconditionally; a BARE lowercase word seeds
// only where the query corroborates the file — another query token is
// itself a symbol defined in that same file (the "check drain fire"
// sibling-bag case), which an incidental English-word collision never is.
const lcTokens = new Set(tokens.map((x) => x.toLowerCase()));
const isPreciseToken = (x: string) =>
/[._$]|::|\//.test(x) || /[a-z][A-Z]/.test(x) || /^[A-Z]/.test(x);
const fileNameSets = new Map<string, Set<string>>();
const coNamedInFile = (t: string, fp: string): boolean => {
let names = fileNameSets.get(fp);
if (!names) {
names = new Set<string>();
try {
for (const n of cg.getNodesInFile(fp)) names.add(n.name.toLowerCase());
} catch { /* unreadable file entry — treat as uncorroborated */ }
fileNameSets.set(fp, names);
}
const self = t.toLowerCase();
for (const o of lcTokens) {
if (o !== self && names.has(o)) return true;
}
return false;
};
for (const t of tokens) {
// Enumerate ALL defs of a bare token via the direct index, not FTS — a
// 50+-overload name (tokio `poll`) ranks the wanted def (`Harness::poll`)
@@ -2619,9 +2649,17 @@ export class ToolHandler {
// codegraph_node's findSymbolMatches.) Qualified tokens keep findAllSymbols.
const isQual = /[.\/]|::/.test(t);
const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
const cands = raw
let cands = raw
.filter((n) => CALLABLE.has(n.kind) && !isTestPath(n.filePath))
.sort((a, b) => (bodyLines(b) > 1 ? 1 : 0) - (bodyLines(a) > 1 ? 1 : 0) || bodyLines(b) - bodyLines(a));
// Bare lowercase words only seed defs their query-siblings corroborate
// (see the NL-stopword guard above). Filtering CANDS (not picks) applies
// the guard uniformly to both branches below, including the >3-def
// single-pick fallback — an uncorroborated bare `run` must not tier its
// most-substantive namesake any more than a 1-def `check` may.
if (!isPreciseToken(t)) {
cands = cands.filter((n) => coNamedInFile(t, n.filePath));
}
// A specific name (<=3 defs) injects all its defs. An overloaded name
// (`validate` = 10, `request` = 44) would flood the subgraph, so inject
// only: the overloads whose file/class the query ALSO names (the agent