feat: Add stem variants to search term extraction for broader definition matching

Expands symbol lookup with morphological variants (e.g., "caching"→"cache", "eviction"→"evict") to find related class definitions that FTS prefix matching would otherwise miss. Includes comprehensive stemming rules for common English suffixes (-ing, -tion, -ed, -er, etc.) and integrates stem expansion into definition prefix search for improved symbol discovery.
This commit is contained in:
Colby McHenry
2026-04-06 15:56:31 -05:00
parent c626dfa989
commit f668b2cd1c
2 changed files with 96 additions and 2 deletions
+9 -1
View File
@@ -26,7 +26,7 @@ import { VectorManager } from '../vectors';
import { formatContextAsMarkdown, formatContextAsJson } from './formatter';
import { logDebug } from '../errors';
import { validatePathWithinRoot } from '../utils';
import { isTestFile, extractSearchTerms, scorePathRelevance } from '../search/query-utils';
import { isTestFile, extractSearchTerms, scorePathRelevance, getStemVariants } from '../search/query-utils';
/**
* Extract likely symbol names from a natural language query
@@ -352,10 +352,18 @@ export class ContextBuilder {
// Step 2b: Search for extracted symbols as definition (class/interface) prefixes.
// When the user writes "REST", "bulk", or "allocation", they usually mean classes
// like RestController, BulkRequest, AllocationService — not nodes named exactly that.
// Also tries stem variants: "caching" → "cache" finds Cache, CacheBuilder.
if (symbolsFromQuery.length > 0) {
const definitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'trait',
'protocol', 'enum', 'type_alias'];
// Expand symbols with stem variants for broader definition matching
const expandedSymbols = new Set(symbolsFromQuery);
for (const sym of symbolsFromQuery) {
for (const variant of getStemVariants(sym)) {
expandedSymbols.add(variant);
}
}
for (const sym of expandedSymbols) {
// Title-case the symbol: "REST" → "Rest", "bulk" → "Bulk", "allocation" → "Allocation"
const titleCased = sym.charAt(0).toUpperCase() + sym.slice(1).toLowerCase();
if (titleCased === sym) continue; // already title-case (e.g., "Engine") — handled by exact match