fix(retrieval): multi-hump field-name queries reach their definers (#1319)

Three compounding defects (#1196) made a query bag of object-literal
keys (`profileInfo isTrialEligible quotaInfo billingMethod`) return
unrelated results while the defining files never surfaced:

1. Step 5b title-cased interior humps (profileInfo -> Profileinfo) and
   then compared case-SENSITIVELY, dropping every row SQLite's
   case-insensitive LIKE had just recovered. The hump lookup is now
   case-insensitive with an explicit uppercase-at-match requirement.
2. Step 5b/5c's kind whitelist held only type-like kinds — dead code on
   method-centric codebases. Callable kinds (function/method/component)
   are fetched as a SEPARATE LIKE batch so hot single-word terms can't
   crowd classes out of the length-ordered 200-row batch.
3. explore's named-symbol seeding was exact-name only; a field token
   seeded nothing. A camelCase token with ZERO exact defs now seeds its
   camel-infix definers (callables, hump-boundary or prefix, shortest
   first, capped at 3) — bare lowercase words keep the #1252 stopword
   guard untouched.

The reporter's acceptance query is a pinned e2e test (definer files
present, exact-name seeding unaffected). excalidraw probe: the
canonical flow query (mutateElement renderStaticScene) is byte-
identical; NL queries shift toward more-central callables
(useUIAppState/getDefaultAppState over observer periphery).

Fixes #1196

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-16 16:11:24 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent a5a8942d1c
commit 1de7e8f8b5
5 changed files with 184 additions and 11 deletions
+40 -11
View File
@@ -749,6 +749,13 @@ export class ContextBuilder {
if (symbolsFromQuery.length > 0) {
const camelDefinitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'trait',
'protocol', 'enum', 'type_alias'];
// Callable kinds participate too: in service-layer codebases the
// camel-infix definers of a queried FIELD are methods/functions
// (`profileInfo` → `getProfileInfoV2`), not classes — the type-only
// whitelist made this whole step dead code there (#1196). Fetched as a
// SEPARATE LIKE batch so one hot single-word term can't crowd classes
// out of the length-ordered 200-row batch.
const camelCallableKinds: NodeKind[] = ['function', 'method', 'component'];
const camelSearchedTerms = new Set<string>();
const searchIdSet = new Set(searchResults.map(r => r.node.id));
// Track per-node term hits for multi-term boosting
@@ -766,18 +773,32 @@ export class ContextBuilder {
// have hundreds of substring matches. The LIKE scan cost is the same
// regardless of LIMIT (SQLite scans all matches to sort), so we fetch
// generously and let path-relevance scoring pick the best ones.
const likeResults = this.queries.findNodesByNameSubstring(titleCased, {
limit: 200,
kinds: camelDefinitionKinds,
excludePrefix: true,
});
const likeResults = [
...this.queries.findNodesByNameSubstring(titleCased, {
limit: 200,
kinds: camelDefinitionKinds,
excludePrefix: true,
}),
...this.queries.findNodesByNameSubstring(titleCased, {
limit: 200,
kinds: camelCallableKinds,
excludePrefix: true,
}),
];
// Filter to CamelCase boundaries, score by path relevance, and take top N
const termCandidates: SearchResult[] = [];
for (const r of likeResults) {
const name = r.node.name;
const idx = name.indexOf(titleCased);
// Case-INSENSITIVE hump lookup: title-casing lowercases interior
// humps (`profileInfo` → `Profileinfo`), which SQLite's LIKE still
// matched but a case-sensitive indexOf here silently dropped —
// making every multi-hump query term unfindable by this step
// (#1196). The match must still LAND on an uppercase char, so a
// plain lowercase infix can't slip through.
const idx = name.toLowerCase().indexOf(termKey);
if (idx <= 0) continue;
if (!/[A-Z]/.test(name.charAt(idx))) continue;
// Accept CamelCase boundary (lowercase before match) OR
// acronym boundary (uppercase before match, e.g., RPCProtocol)
if (!/[a-zA-Z]/.test(name.charAt(idx - 1))) continue;
@@ -841,11 +862,19 @@ export class ContextBuilder {
const titleCased = sym.charAt(0).toUpperCase() + sym.slice(1).toLowerCase();
if (titleCased.length < 3) continue;
const likeResults = this.queries.findNodesByNameSubstring(titleCased, {
limit: 200,
kinds: camelDefinitionKinds,
excludePrefix: false,
});
const likeResults = [
...this.queries.findNodesByNameSubstring(titleCased, {
limit: 200,
kinds: camelDefinitionKinds,
excludePrefix: false,
}),
// Same separate callable batch as Step 5b (#1196).
...this.queries.findNodesByNameSubstring(titleCased, {
limit: 200,
kinds: camelCallableKinds,
excludePrefix: false,
}),
];
for (const r of likeResults) {
if (searchIdSet.has(r.node.id)) continue;