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:
co-authored by
Claude Fable 5
parent
70b1be6a21
commit
8b82fe71f8
+39
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user