Просмотр исходного кода

feat: Enhance symbol search with co-location boosting and receiver type support

Improves search accuracy by boosting results when multiple query symbols appear in the same file, addressing cases where common names like "run" return too many results. Adds Go method receiver type extraction to qualified names for better searchability (e.g., "scrapeLoop.run"). Optimizes database queries with two-pass approach to handle distinctive vs common symbol names efficiently.
Colby McHenry 2 месяцев назад
Родитель
Сommit
7a3afc9124

+ 27 - 1
src/context/index.ts

@@ -291,10 +291,36 @@ export class ContextBuilder {
     let exactMatches: SearchResult[] = [];
     if (symbolsFromQuery.length > 0) {
       try {
+        // Get more results so we can apply co-location boosting before trimming
         exactMatches = this.queries.findNodesByExactName(symbolsFromQuery, {
-          limit: Math.ceil(opts.searchLimit * 2), // Get more since we'll merge
+          limit: Math.ceil(opts.searchLimit * 5),
           kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
         });
+
+        // Co-location boost: when multiple extracted symbols appear in the same file,
+        // those results are much more likely to be what the user is looking for.
+        // E.g., "scrapeLoop" + "run" both in scrape/scrape.go → boost both.
+        if (exactMatches.length > 1) {
+          // Build a map of files → how many distinct symbol names matched in that file
+          const fileSymbolCounts = new Map<string, Set<string>>();
+          for (const r of exactMatches) {
+            const names = fileSymbolCounts.get(r.node.filePath) || new Set();
+            names.add(r.node.name.toLowerCase());
+            fileSymbolCounts.set(r.node.filePath, names);
+          }
+          // Boost results in files where multiple query symbols co-occur
+          exactMatches = exactMatches.map(r => {
+            const symbolCount = fileSymbolCounts.get(r.node.filePath)?.size || 1;
+            return {
+              ...r,
+              score: symbolCount > 1 ? r.score + (symbolCount - 1) * 20 : r.score,
+            };
+          });
+          exactMatches.sort((a, b) => b.score - a.score);
+        }
+
+        // Trim back to reasonable size
+        exactMatches = exactMatches.slice(0, Math.ceil(opts.searchLimit * 2));
         logDebug('Exact symbol matches', { count: exactMatches.length });
       } catch (error) {
         logDebug('Exact symbol lookup failed', { error: String(error) });

+ 68 - 27
src/db/queries.ts

@@ -630,39 +630,80 @@ export class QueryBuilder {
 
     const { kinds, languages, limit = 50 } = options;
 
-    // Build query with exact matches (case-insensitive)
-    let sql = `
-      SELECT nodes.*,
-        CASE
-          WHEN name COLLATE NOCASE IN (${names.map(() => '?').join(',')}) THEN 1.0
-          ELSE 0.9
-        END as score
-      FROM nodes
-      WHERE name COLLATE NOCASE IN (${names.map(() => '?').join(',')})
-    `;
-
-    // Duplicate names for both SELECT and WHERE clauses
-    const params: (string | number)[] = [...names, ...names];
-
-    if (kinds && kinds.length > 0) {
-      sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
-      params.push(...kinds);
+    // Two-pass approach to handle common names (e.g., "run" has 40+ matches):
+    // Pass 1: Find which files contain distinctive (rare) symbols from the query.
+    // Pass 2: Query each name, boosting results that co-locate with distinctive symbols.
+
+    // Pass 1: Find files containing each queried name, identify distinctive names
+    const nameToFiles = new Map<string, Set<string>>();
+    for (const name of names) {
+      let sql = 'SELECT DISTINCT file_path FROM nodes WHERE name COLLATE NOCASE = ?';
+      const params: (string | number)[] = [name];
+      if (kinds && kinds.length > 0) {
+        sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
+        params.push(...kinds);
+      }
+      sql += ' LIMIT 100';
+      const rows = this.db.prepare(sql).all(...params) as { file_path: string }[];
+      nameToFiles.set(name.toLowerCase(), new Set(rows.map(r => r.file_path)));
     }
 
-    if (languages && languages.length > 0) {
-      sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
-      params.push(...languages);
+    // Distinctive names are those with fewer than 10 file matches (e.g., "scrapeLoop" = 1 file)
+    const distinctiveFiles = new Set<string>();
+    for (const [, files] of nameToFiles) {
+      if (files.size > 0 && files.size < 10) {
+        for (const f of files) distinctiveFiles.add(f);
+      }
     }
 
-    sql += ' ORDER BY score DESC, length(name) ASC LIMIT ?';
-    params.push(limit);
+    // Pass 2: Query each name with per-name limit, scoring by co-location
+    const perNameLimit = Math.max(8, Math.ceil(limit / names.length));
+    const allResults: SearchResult[] = [];
+    const seenIds = new Set<string>();
 
-    const rows = this.db.prepare(sql).all(...params) as (NodeRow & { score: number })[];
+    for (const name of names) {
+      let sql = `
+        SELECT nodes.*, 1.0 as score
+        FROM nodes
+        WHERE name COLLATE NOCASE = ?
+      `;
+      const params: (string | number)[] = [name];
 
-    return rows.map((row) => ({
-      node: rowToNode(row),
-      score: row.score,
-    }));
+      if (kinds && kinds.length > 0) {
+        sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
+        params.push(...kinds);
+      }
+
+      if (languages && languages.length > 0) {
+        sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
+        params.push(...languages);
+      }
+
+      // Fetch enough to find co-located results among common names
+      sql += ' LIMIT ?';
+      params.push(Math.max(perNameLimit * 3, 50));
+
+      const rows = this.db.prepare(sql).all(...params) as (NodeRow & { score: number })[];
+      const nameResults: SearchResult[] = [];
+      for (const row of rows) {
+        const node = rowToNode(row);
+        if (seenIds.has(node.id)) continue;
+        // Boost results in files that also contain distinctive symbols
+        const coLocationBoost = distinctiveFiles.has(node.filePath) ? 20 : 0;
+        nameResults.push({ node, score: row.score + coLocationBoost });
+      }
+
+      // Sort by score (co-located first), take per-name limit
+      nameResults.sort((a, b) => b.score - a.score);
+      for (const r of nameResults.slice(0, perNameLimit)) {
+        seenIds.add(r.node.id);
+        allResults.push(r);
+      }
+    }
+
+    // Sort all results by score so co-located results bubble up
+    allResults.sort((a, b) => b.score - a.score);
+    return allResults.slice(0, limit);
   }
 
   // ===========================================================================

+ 12 - 0
src/extraction/languages/go.ts

@@ -27,4 +27,16 @@ export const goExtractor: LanguageExtractor = {
     }
     return sig;
   },
+  getReceiverType: (node, source) => {
+    // Go method_declaration has a "receiver" field: func (sl *scrapeLoop) run(...)
+    // The receiver is a parameter_list containing a parameter_declaration
+    // with a type that may be a pointer_type (*scrapeLoop) or plain type (scrapeLoop)
+    const receiver = getChildByField(node, 'receiver');
+    if (!receiver) return undefined;
+    // Find the type identifier inside the receiver
+    const text = getNodeText(receiver, source);
+    // Extract type name from patterns like "(sl *Type)", "(sl Type)", "(*Type)", "(Type)"
+    const match = text.match(/\*?\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/);
+    return match?.[1];
+  },
 };

+ 7 - 0
src/extraction/tree-sitter-types.ts

@@ -163,4 +163,11 @@ export interface LanguageExtractor {
    * Returns info about each declared variable, allowing the core to create nodes.
    */
   extractVariables?: (node: SyntaxNode, source: string) => VariableInfo[];
+
+  /**
+   * Extract receiver/owner type name from a method declaration.
+   * Used by Go to get the struct receiver (e.g., "scrapeLoop" from "func (sl *scrapeLoop) run()").
+   * When present, the receiver type is included in the qualified name for better searchability.
+   */
+  getReceiverType?: (node: SyntaxNode, source: string) => string | undefined;
 }

+ 10 - 2
src/extraction/tree-sitter.ts

@@ -505,13 +505,21 @@ export class TreeSitterExtractor {
     const isAsync = this.extractor.isAsync?.(node);
     const isStatic = this.extractor.isStatic?.(node);
 
-    const methodNode = this.createNode('method', name, node, {
+    // For languages with receiver types (Go), include receiver in qualified name
+    // so FTS can match "scrapeLoop.run" → qualified_name "...::scrapeLoop::run"
+    const receiverType = this.extractor.getReceiverType?.(node, this.source);
+    const extraProps: Partial<Node> = {
       docstring,
       signature,
       visibility,
       isAsync,
       isStatic,
-    });
+    };
+    if (receiverType) {
+      extraProps.qualifiedName = `${this.filePath}::${receiverType}::${name}`;
+    }
+
+    const methodNode = this.createNode('method', name, node, extraProps);
     if (!methodNode) return;
 
     // Extract type annotations (parameter types and return type)

+ 37 - 110
src/resolution/frameworks/csharp.ts

@@ -48,7 +48,7 @@ export const aspnetResolver: FrameworkResolver = {
   resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
     // Pattern 1: Controller references
     if (ref.referenceName.endsWith('Controller')) {
-      const result = resolveController(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, CONTROLLER_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -61,7 +61,7 @@ export const aspnetResolver: FrameworkResolver = {
 
     // Pattern 2: Service references (dependency injection)
     if (ref.referenceName.endsWith('Service') || ref.referenceName.startsWith('I') && ref.referenceName.length > 1) {
-      const result = resolveService(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -74,7 +74,7 @@ export const aspnetResolver: FrameworkResolver = {
 
     // Pattern 3: Repository references
     if (ref.referenceName.endsWith('Repository')) {
-      const result = resolveRepository(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, REPO_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -87,7 +87,7 @@ export const aspnetResolver: FrameworkResolver = {
 
     // Pattern 4: Model/Entity references
     if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
-      const result = resolveModel(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, MODEL_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -100,7 +100,7 @@ export const aspnetResolver: FrameworkResolver = {
 
     // Pattern 5: ViewModel references
     if (ref.referenceName.endsWith('ViewModel') || ref.referenceName.endsWith('Dto')) {
-      const result = resolveViewModel(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, VIEWMODEL_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -213,111 +213,38 @@ export const aspnetResolver: FrameworkResolver = {
   },
 };
 
-// Helper functions
+// Directory patterns
+const CONTROLLER_DIRS = ['/Controllers/'];
+const SERVICE_DIRS = ['/Services/', '/Service/', '/Application/'];
+const REPO_DIRS = ['/Repositories/', '/Repository/', '/Data/', '/Infrastructure/'];
+const MODEL_DIRS = ['/Models/', '/Model/', '/Entities/', '/Entity/', '/Domain/'];
+const VIEWMODEL_DIRS = ['/ViewModels/', '/ViewModel/', '/DTOs/', '/Dto/'];
 
-function resolveController(name: string, context: ResolutionContext): string | null {
-  const allFiles = context.getAllFiles();
+const CLASS_KINDS = new Set(['class']);
+const SERVICE_KINDS = new Set(['class', 'interface']);
 
-  for (const file of allFiles) {
-    if (file.endsWith('.cs') && file.includes('/Controllers/')) {
-      const nodes = context.getNodesInFile(file);
-      const controllerNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (controllerNode) {
-        return controllerNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveService(name: string, context: ResolutionContext): string | null {
-  const serviceDirs = ['Services', 'Service', 'Application'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.cs') && serviceDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const serviceNode = nodes.find(
-        (n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
-      );
-      if (serviceNode) {
-        return serviceNode.id;
-      }
-    }
-  }
-
-  // Search all C# files for interfaces (often services are injected via interface)
-  for (const file of allFiles) {
-    if (file.endsWith('.cs')) {
-      const nodes = context.getNodesInFile(file);
-      const serviceNode = nodes.find(
-        (n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
-      );
-      if (serviceNode) {
-        return serviceNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveRepository(name: string, context: ResolutionContext): string | null {
-  const repoDirs = ['Repositories', 'Repository', 'Data', 'Infrastructure'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.cs') && repoDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const repoNode = nodes.find(
-        (n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
-      );
-      if (repoNode) {
-        return repoNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveModel(name: string, context: ResolutionContext): string | null {
-  const modelDirs = ['Models', 'Model', 'Entities', 'Entity', 'Domain'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.cs') && modelDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const modelNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (modelNode) {
-        return modelNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveViewModel(name: string, context: ResolutionContext): string | null {
-  const viewModelDirs = ['ViewModels', 'ViewModel', 'DTOs', 'Dto'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.cs') && viewModelDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const vmNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (vmNode) {
-        return vmNode.id;
-      }
-    }
-  }
-
-  return null;
+/**
+ * Resolve a symbol by name using indexed queries instead of scanning all files.
+ */
+function resolveByNameAndKind(
+  name: string,
+  kinds: Set<string>,
+  preferredDirPatterns: string[],
+  context: ResolutionContext,
+): string | null {
+  const candidates = context.getNodesByName(name);
+  if (candidates.length === 0) return null;
+
+  const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
+  if (kindFiltered.length === 0) return null;
+
+  // Prefer candidates in framework-conventional directories
+  const preferred = kindFiltered.filter((n) =>
+    preferredDirPatterns.some((d) => n.filePath.includes(d))
+  );
+
+  if (preferred.length > 0) return preferred[0]!.id;
+
+  // Fall back to any match
+  return kindFiltered[0]!.id;
 }

+ 52 - 62
src/resolution/frameworks/express.ts

@@ -61,7 +61,7 @@ export const expressResolver: FrameworkResolver = {
     const controllerMatch = ref.referenceName.match(/^(\w+)Controller\.(\w+)$/);
     if (controllerMatch) {
       const [, controller, method] = controllerMatch;
-      const result = resolveController(controller!, method!, context);
+      const result = resolveControllerMethod(controller!, method!, context);
       if (result) {
         return {
           original: ref,
@@ -76,7 +76,7 @@ export const expressResolver: FrameworkResolver = {
     const serviceMatch = ref.referenceName.match(/^(\w+)(Service|Helper|Utils?)\.(\w+)$/);
     if (serviceMatch) {
       const [, name, suffix, method] = serviceMatch;
-      const result = resolveService(name! + suffix!, method!, context);
+      const result = resolveServiceMethod(name! + suffix!, method!, context);
       if (result) {
         return {
           original: ref,
@@ -154,93 +154,83 @@ function isMiddlewareName(name: string): boolean {
 }
 
 /**
- * Resolve middleware reference
+ * Resolve middleware reference using name-based lookup
  */
 function resolveMiddleware(
   name: string,
   context: ResolutionContext
 ): string | null {
-  // Look in middleware directories
-  const middlewareDirs = ['middleware', 'middlewares', 'src/middleware', 'src/middlewares'];
-
-  for (const dir of middlewareDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if (file.startsWith(dir) || file.includes('/middleware/')) {
-        const nodes = context.getNodesInFile(file);
-        const match = nodes.find(
-          (n) =>
-            n.name.toLowerCase() === name.toLowerCase() ||
-            n.name.toLowerCase() === name.replace(/Middleware$/i, '').toLowerCase()
-        );
-        if (match) {
-          return match.id;
-        }
-      }
-    }
+  // Try exact name first
+  const candidates = context.getNodesByName(name);
+  const match = candidates.find((n) =>
+    n.name.toLowerCase() === name.toLowerCase() ||
+    n.name.toLowerCase() === name.replace(/Middleware$/i, '').toLowerCase()
+  );
+  if (match) return match.id;
+
+  // Try without Middleware suffix
+  const baseName = name.replace(/Middleware$/i, '');
+  if (baseName !== name) {
+    const baseCandidates = context.getNodesByName(baseName);
+    const MIDDLEWARE_DIRS = ['/middleware/', '/middlewares/'];
+    const preferred = baseCandidates.filter((n) =>
+      MIDDLEWARE_DIRS.some((d) => n.filePath.includes(d))
+    );
+    if (preferred.length > 0) return preferred[0]!.id;
+    if (baseCandidates.length > 0) return baseCandidates[0]!.id;
   }
 
   return null;
 }
 
 /**
- * Resolve controller method
+ * Resolve controller method using name-based lookup
  */
-function resolveController(
+function resolveControllerMethod(
   controller: string,
   method: string,
   context: ResolutionContext
 ): string | null {
-  const controllerDirs = ['controllers', 'src/controllers', 'app/controllers'];
-
-  for (const dir of controllerDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if (
-        (file.startsWith(dir) || file.includes('/controllers/')) &&
-        file.toLowerCase().includes(controller.toLowerCase())
-      ) {
-        const nodes = context.getNodesInFile(file);
-        const methodNode = nodes.find(
-          (n) => (n.kind === 'method' || n.kind === 'function') && n.name === method
-        );
-        if (methodNode) {
-          return methodNode.id;
-        }
-      }
-    }
+  // Look for the method name directly
+  const methodCandidates = context.getNodesByName(method);
+  const methodNodes = methodCandidates.filter(
+    (n) => (n.kind === 'method' || n.kind === 'function') &&
+      n.filePath.toLowerCase().includes(controller.toLowerCase())
+  );
+
+  if (methodNodes.length > 0) return methodNodes[0]!.id;
+
+  // Fall back: look for controller class, then find the method in its file
+  const controllerName = controller + 'Controller';
+  const controllerCandidates = context.getNodesByName(controllerName);
+  for (const ctrl of controllerCandidates) {
+    const nodesInFile = context.getNodesInFile(ctrl.filePath);
+    const methodNode = nodesInFile.find(
+      (n) => (n.kind === 'method' || n.kind === 'function') && n.name === method
+    );
+    if (methodNode) return methodNode.id;
   }
 
   return null;
 }
 
 /**
- * Resolve service/helper
+ * Resolve service/helper method using name-based lookup
  */
-function resolveService(
+function resolveServiceMethod(
   serviceName: string,
   method: string,
   context: ResolutionContext
 ): string | null {
-  const serviceDirs = ['services', 'src/services', 'helpers', 'src/helpers', 'utils', 'src/utils'];
-
-  for (const dir of serviceDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if (
-        (file.startsWith(dir) || file.includes('/services/') || file.includes('/helpers/') || file.includes('/utils/')) &&
-        file.toLowerCase().includes(serviceName.toLowerCase().replace(/(service|helper|utils?)$/i, ''))
-      ) {
-        const nodes = context.getNodesInFile(file);
-        const methodNode = nodes.find(
-          (n) => (n.kind === 'method' || n.kind === 'function') && n.name === method
-        );
-        if (methodNode) {
-          return methodNode.id;
-        }
-      }
-    }
-  }
+  // Look for the method in files matching the service name
+  const methodCandidates = context.getNodesByName(method);
+  const stripped = serviceName.replace(/(Service|Helper|Utils?)$/i, '').toLowerCase();
+  const methodNodes = methodCandidates.filter(
+    (n) => (n.kind === 'method' || n.kind === 'function') &&
+      n.filePath.toLowerCase().includes(stripped)
+  );
+
+  if (methodNodes.length > 0) return methodNodes[0]!.id;
 
   return null;
 }

+ 37 - 117
src/resolution/frameworks/java.ts

@@ -50,7 +50,7 @@ export const springResolver: FrameworkResolver = {
   resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
     // Pattern 1: Service references (dependency injection)
     if (ref.referenceName.endsWith('Service')) {
-      const result = resolveService(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -63,7 +63,7 @@ export const springResolver: FrameworkResolver = {
 
     // Pattern 2: Repository references
     if (ref.referenceName.endsWith('Repository')) {
-      const result = resolveRepository(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, REPO_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -76,7 +76,7 @@ export const springResolver: FrameworkResolver = {
 
     // Pattern 3: Controller references
     if (ref.referenceName.endsWith('Controller')) {
-      const result = resolveController(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, CONTROLLER_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -89,7 +89,7 @@ export const springResolver: FrameworkResolver = {
 
     // Pattern 4: Entity/Model references
     if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
-      const result = resolveEntity(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, ENTITY_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -102,7 +102,7 @@ export const springResolver: FrameworkResolver = {
 
     // Pattern 5: Component references
     if (ref.referenceName.endsWith('Component') || ref.referenceName.endsWith('Config')) {
-      const result = resolveComponent(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, COMPONENT_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -176,118 +176,38 @@ export const springResolver: FrameworkResolver = {
   },
 };
 
-// Helper functions
+// Directory patterns
+const SERVICE_DIRS = ['/service/', '/services/'];
+const REPO_DIRS = ['/repository/', '/repositories/'];
+const CONTROLLER_DIRS = ['/controller/', '/controllers/'];
+const ENTITY_DIRS = ['/entity/', '/entities/', '/model/', '/models/', '/domain/'];
+const COMPONENT_DIRS = ['/component/', '/components/', '/config/'];
 
-function resolveService(name: string, context: ResolutionContext): string | null {
-  const allFiles = context.getAllFiles();
+const CLASS_KINDS = new Set(['class']);
+const SERVICE_KINDS = new Set(['class', 'interface']);
 
-  for (const file of allFiles) {
-    if (file.endsWith('.java') && (file.includes('/service/') || file.includes('/services/'))) {
-      const nodes = context.getNodesInFile(file);
-      const serviceNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (serviceNode) {
-        return serviceNode.id;
-      }
-    }
-  }
-
-  // Also check interface definitions
-  for (const file of allFiles) {
-    if (file.endsWith('.java')) {
-      const nodes = context.getNodesInFile(file);
-      const serviceNode = nodes.find(
-        (n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
-      );
-      if (serviceNode) {
-        return serviceNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveRepository(name: string, context: ResolutionContext): string | null {
-  const allFiles = context.getAllFiles();
-
-  for (const file of allFiles) {
-    if (file.endsWith('.java') && (file.includes('/repository/') || file.includes('/repositories/'))) {
-      const nodes = context.getNodesInFile(file);
-      const repoNode = nodes.find(
-        (n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
-      );
-      if (repoNode) {
-        return repoNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveController(name: string, context: ResolutionContext): string | null {
-  const allFiles = context.getAllFiles();
-
-  for (const file of allFiles) {
-    if (file.endsWith('.java') && (file.includes('/controller/') || file.includes('/controllers/'))) {
-      const nodes = context.getNodesInFile(file);
-      const controllerNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (controllerNode) {
-        return controllerNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveEntity(name: string, context: ResolutionContext): string | null {
-  const allFiles = context.getAllFiles();
-
-  // Check entity/model directories first
-  for (const file of allFiles) {
-    if (file.endsWith('.java') && (
-      file.includes('/entity/') ||
-      file.includes('/entities/') ||
-      file.includes('/model/') ||
-      file.includes('/models/') ||
-      file.includes('/domain/')
-    )) {
-      const nodes = context.getNodesInFile(file);
-      const entityNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (entityNode) {
-        return entityNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveComponent(name: string, context: ResolutionContext): string | null {
-  const allFiles = context.getAllFiles();
-
-  for (const file of allFiles) {
-    if (file.endsWith('.java') && (
-      file.includes('/component/') ||
-      file.includes('/components/') ||
-      file.includes('/config/')
-    )) {
-      const nodes = context.getNodesInFile(file);
-      const componentNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (componentNode) {
-        return componentNode.id;
-      }
-    }
-  }
-
-  return null;
+/**
+ * Resolve a symbol by name using indexed queries instead of scanning all files.
+ */
+function resolveByNameAndKind(
+  name: string,
+  kinds: Set<string>,
+  preferredDirPatterns: string[],
+  context: ResolutionContext,
+): string | null {
+  const candidates = context.getNodesByName(name);
+  if (candidates.length === 0) return null;
+
+  const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
+  if (kindFiltered.length === 0) return null;
+
+  // Prefer candidates in framework-conventional directories
+  const preferred = kindFiltered.filter((n) =>
+    preferredDirPatterns.some((d) => n.filePath.includes(d))
+  );
+
+  if (preferred.length > 0) return preferred[0]!.id;
+
+  // Fall back to any match
+  return kindFiltered[0]!.id;
 }

+ 6 - 6
src/resolution/frameworks/laravel.ts

@@ -216,12 +216,12 @@ function resolveControllerMethod(
     }
   }
 
-  // Try subdirectories (namespaced controllers)
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith(`${controller}.php`) && file.includes('Controllers')) {
-      const nodes = context.getNodesInFile(file);
-      const methodNode = nodes.find(
+  // Try name-based lookup for namespaced controllers
+  const controllerCandidates = context.getNodesByName(controller);
+  for (const ctrl of controllerCandidates) {
+    if (ctrl.kind === 'class' && ctrl.filePath.includes('Controllers')) {
+      const nodesInFile = context.getNodesInFile(ctrl.filePath);
+      const methodNode = nodesInFile.find(
         (n) => n.kind === 'method' && n.name === method
       );
       if (methodNode) {

+ 39 - 124
src/resolution/frameworks/python.ts

@@ -34,7 +34,7 @@ export const djangoResolver: FrameworkResolver = {
   resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
     // Pattern 1: Model references
     if (ref.referenceName.endsWith('Model') || /^[A-Z][a-z]+$/.test(ref.referenceName)) {
-      const result = resolveModel(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, MODEL_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -47,7 +47,7 @@ export const djangoResolver: FrameworkResolver = {
 
     // Pattern 2: View references
     if (ref.referenceName.endsWith('View') || ref.referenceName.endsWith('ViewSet')) {
-      const result = resolveView(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, VIEW_KINDS, VIEW_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -60,7 +60,7 @@ export const djangoResolver: FrameworkResolver = {
 
     // Pattern 3: Form references
     if (ref.referenceName.endsWith('Form')) {
-      const result = resolveForm(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, FORM_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -140,7 +140,7 @@ export const flaskResolver: FrameworkResolver = {
   resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
     // Pattern 1: Blueprint references
     if (ref.referenceName.endsWith('_bp') || ref.referenceName.endsWith('_blueprint')) {
-      const result = resolveBlueprint(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, VARIABLE_KINDS, [], context);
       if (result) {
         return {
           original: ref,
@@ -215,7 +215,7 @@ export const fastapiResolver: FrameworkResolver = {
   resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
     // Pattern 1: Router references
     if (ref.referenceName.endsWith('_router') || ref.referenceName === 'router') {
-      const result = resolveRouter(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, VARIABLE_KINDS, ROUTER_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -228,7 +228,7 @@ export const fastapiResolver: FrameworkResolver = {
 
     // Pattern 2: Dependency references
     if (ref.referenceName.startsWith('get_') || ref.referenceName.startsWith('Depends')) {
-      const result = resolveDependency(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, FUNCTION_KINDS, DEP_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -274,126 +274,41 @@ export const fastapiResolver: FrameworkResolver = {
   },
 };
 
-// Helper functions
-
-function resolveModel(name: string, context: ResolutionContext): string | null {
-  const modelDirs = ['models', 'app/models', 'src/models'];
-
-  for (const dir of modelDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if (file.startsWith(dir) && file.endsWith('.py')) {
-        const nodes = context.getNodesInFile(file);
-        const modelNode = nodes.find(
-          (n) => n.kind === 'class' && n.name === name
-        );
-        if (modelNode) {
-          return modelNode.id;
-        }
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveView(name: string, context: ResolutionContext): string | null {
-  const viewDirs = ['views', 'app/views', 'src/views', 'api/views'];
-
-  for (const dir of viewDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if (file.startsWith(dir) && file.endsWith('.py')) {
-        const nodes = context.getNodesInFile(file);
-        const viewNode = nodes.find(
-          (n) => (n.kind === 'class' || n.kind === 'function') && n.name === name
-        );
-        if (viewNode) {
-          return viewNode.id;
-        }
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveForm(name: string, context: ResolutionContext): string | null {
-  const formDirs = ['forms', 'app/forms', 'src/forms'];
-
-  for (const dir of formDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if (file.startsWith(dir) && file.endsWith('.py')) {
-        const nodes = context.getNodesInFile(file);
-        const formNode = nodes.find(
-          (n) => n.kind === 'class' && n.name === name
-        );
-        if (formNode) {
-          return formNode.id;
-        }
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveBlueprint(name: string, context: ResolutionContext): string | null {
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.py')) {
-      const nodes = context.getNodesInFile(file);
-      const bpNode = nodes.find(
-        (n) => n.kind === 'variable' && n.name === name
-      );
-      if (bpNode) {
-        return bpNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveRouter(name: string, context: ResolutionContext): string | null {
-  const routerDirs = ['routers', 'api', 'routes', 'endpoints'];
-
-  for (const dir of routerDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if ((file.startsWith(dir) || file.includes('/routers/')) && file.endsWith('.py')) {
-        const nodes = context.getNodesInFile(file);
-        const routerNode = nodes.find(
-          (n) => n.kind === 'variable' && n.name === name
-        );
-        if (routerNode) {
-          return routerNode.id;
-        }
-      }
-    }
-  }
+// Directory patterns
+const MODEL_DIRS = ['models', 'app/models', 'src/models'];
+const VIEW_DIRS = ['views', 'app/views', 'src/views', 'api/views'];
+const FORM_DIRS = ['forms', 'app/forms', 'src/forms'];
+const ROUTER_DIRS = ['/routers/', '/api/', '/routes/', '/endpoints/'];
+const DEP_DIRS = ['/dependencies/', '/deps/', '/core/'];
 
-  return null;
-}
+const CLASS_KINDS = new Set(['class']);
+const VIEW_KINDS = new Set(['class', 'function']);
+const VARIABLE_KINDS = new Set(['variable']);
+const FUNCTION_KINDS = new Set(['function']);
 
-function resolveDependency(name: string, context: ResolutionContext): string | null {
-  const depDirs = ['dependencies', 'deps', 'core'];
-
-  for (const dir of depDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if ((file.startsWith(dir) || file.includes('/dependencies/')) && file.endsWith('.py')) {
-        const nodes = context.getNodesInFile(file);
-        const depNode = nodes.find(
-          (n) => n.kind === 'function' && n.name === name
-        );
-        if (depNode) {
-          return depNode.id;
-        }
-      }
-    }
+/**
+ * Resolve a symbol by name using indexed queries instead of scanning all files.
+ */
+function resolveByNameAndKind(
+  name: string,
+  kinds: Set<string>,
+  preferredDirPatterns: string[],
+  context: ResolutionContext,
+): string | null {
+  const candidates = context.getNodesByName(name);
+  if (candidates.length === 0) return null;
+
+  const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
+  if (kindFiltered.length === 0) return null;
+
+  // Prefer candidates in framework-conventional directories
+  if (preferredDirPatterns.length > 0) {
+    const preferred = kindFiltered.filter((n) =>
+      preferredDirPatterns.some((d) => n.filePath.includes(d))
+    );
+    if (preferred.length > 0) return preferred[0]!.id;
   }
 
-  return null;
+  // Fall back to any match
+  return kindFiltered[0]!.id;
 }

+ 55 - 82
src/resolution/frameworks/react.ts

@@ -183,116 +183,89 @@ function isPascalCase(str: string): boolean {
  * Check if name is a built-in type
  */
 function isBuiltInType(name: string): boolean {
-  const builtIns = [
-    'Array', 'Boolean', 'Date', 'Error', 'Function', 'JSON', 'Math', 'Number',
-    'Object', 'Promise', 'RegExp', 'String', 'Symbol', 'Map', 'Set', 'WeakMap', 'WeakSet',
-    'React', 'Component', 'Fragment', 'Suspense', 'StrictMode',
-  ];
-  return builtIns.includes(name);
+  return BUILT_IN_TYPES.has(name);
 }
 
+const BUILT_IN_TYPES = new Set([
+  'Array', 'Boolean', 'Date', 'Error', 'Function', 'JSON', 'Math', 'Number',
+  'Object', 'Promise', 'RegExp', 'String', 'Symbol', 'Map', 'Set', 'WeakMap', 'WeakSet',
+  'React', 'Component', 'Fragment', 'Suspense', 'StrictMode',
+]);
+
+const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
+
 /**
- * Resolve a component reference
+ * Resolve a component reference using name-based lookup
  */
 function resolveComponent(
   name: string,
   fromFile: string,
   context: ResolutionContext
 ): string | null {
-  // Look for component in common locations
-  const componentDirs = [
-    'components',
-    'src/components',
-    'app/components',
-    'pages',
-    'src/pages',
-    'views',
-    'src/views',
-  ];
-
-  // First, check same directory
+  const candidates = context.getNodesByName(name);
+  if (candidates.length === 0) return null;
+
+  const components = candidates.filter((n) => COMPONENT_KINDS.has(n.kind));
+  if (components.length === 0) return null;
+
+  // Prefer same directory
   const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
-  const sameDir = context.getAllFiles().filter((f) => f.startsWith(fromDir));
-  for (const file of sameDir) {
-    if (file.toLowerCase().includes(name.toLowerCase())) {
-      const nodes = context.getNodesInFile(file);
-      const component = nodes.find(
-        (n) => (n.kind === 'component' || n.kind === 'function' || n.kind === 'class') && n.name === name
-      );
-      if (component) {
-        return component.id;
-      }
-    }
-  }
+  const sameDir = components.filter((n) => n.filePath.startsWith(fromDir));
+  if (sameDir.length > 0) return sameDir[0]!.id;
 
-  // Then check component directories
-  for (const dir of componentDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if (file.startsWith(dir) && file.toLowerCase().includes(name.toLowerCase())) {
-        const nodes = context.getNodesInFile(file);
-        const component = nodes.find(
-          (n) => (n.kind === 'component' || n.kind === 'function' || n.kind === 'class') && n.name === name
-        );
-        if (component) {
-          return component.id;
-        }
-      }
-    }
-  }
+  // Prefer component directories
+  const COMPONENT_DIRS = ['/components/', '/src/components/', '/app/components/', '/pages/', '/src/pages/', '/views/', '/src/views/'];
+  const preferred = components.filter((n) =>
+    COMPONENT_DIRS.some((d) => n.filePath.includes(d))
+  );
+  if (preferred.length > 0) return preferred[0]!.id;
 
-  return null;
+  return components[0]!.id;
 }
 
 /**
- * Resolve a custom hook reference
+ * Resolve a custom hook reference using name-based lookup
  */
 function resolveHook(name: string, context: ResolutionContext): string | null {
-  const hookDirs = ['hooks', 'src/hooks', 'lib/hooks', 'utils/hooks'];
+  const candidates = context.getNodesByName(name);
+  if (candidates.length === 0) return null;
 
-  for (const dir of hookDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if (file.startsWith(dir) || file.includes('/hooks/')) {
-        const nodes = context.getNodesInFile(file);
-        const hook = nodes.find((n) => n.kind === 'function' && n.name === name);
-        if (hook) {
-          return hook.id;
-        }
-      }
-    }
-  }
+  const hooks = candidates.filter((n) => n.kind === 'function' && n.name.startsWith('use'));
+  if (hooks.length === 0) return null;
 
-  // Also check all files for the hook
-  const allNodes = context.getNodesByName(name);
-  const hookNode = allNodes.find((n) => n.kind === 'function' && n.name.startsWith('use'));
-  if (hookNode) {
-    return hookNode.id;
-  }
+  // Prefer hooks directories
+  const HOOK_DIRS = ['/hooks/', '/src/hooks/', '/lib/hooks/', '/utils/hooks/'];
+  const preferred = hooks.filter((n) =>
+    HOOK_DIRS.some((d) => n.filePath.includes(d))
+  );
+  if (preferred.length > 0) return preferred[0]!.id;
 
-  return null;
+  return hooks[0]!.id;
 }
 
 /**
- * Resolve a context reference
+ * Resolve a context reference using name-based lookup
  */
 function resolveContext(name: string, context: ResolutionContext): string | null {
-  const contextDirs = ['context', 'contexts', 'src/context', 'src/contexts', 'providers', 'src/providers'];
-
-  for (const dir of contextDirs) {
-    const allFiles = context.getAllFiles();
-    for (const file of allFiles) {
-      if (file.startsWith(dir) || file.includes('/context/') || file.includes('/contexts/')) {
-        const nodes = context.getNodesInFile(file);
-        const contextNode = nodes.find((n) => n.name === name || n.name === name.replace(/Context$|Provider$/, ''));
-        if (contextNode) {
-          return contextNode.id;
-        }
-      }
+  const candidates = context.getNodesByName(name);
+  if (candidates.length === 0) {
+    // Try without Context/Provider suffix
+    const baseName = name.replace(/Context$|Provider$/, '');
+    if (baseName !== name) {
+      const baseCandidates = context.getNodesByName(baseName);
+      if (baseCandidates.length > 0) return baseCandidates[0]!.id;
     }
+    return null;
   }
 
-  return null;
+  // Prefer context directories
+  const CONTEXT_DIRS = ['/context/', '/contexts/', '/src/context/', '/src/contexts/', '/providers/', '/src/providers/'];
+  const preferred = candidates.filter((n) =>
+    CONTEXT_DIRS.some((d) => n.filePath.includes(d))
+  );
+  if (preferred.length > 0) return preferred[0]!.id;
+
+  return candidates[0]!.id;
 }
 
 /**

+ 14 - 28
src/resolution/frameworks/ruby.ts

@@ -189,7 +189,7 @@ export const railsResolver: FrameworkResolver = {
 // Helper functions
 
 function resolveModel(name: string, context: ResolutionContext): string | null {
-  // Convert CamelCase to snake_case for file lookup
+  // Try direct file path lookup first (Rails convention: CamelCase -> snake_case.rb)
   const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
   const possiblePaths = [
     `app/models/${snakeName}.rb`,
@@ -208,25 +208,18 @@ function resolveModel(name: string, context: ResolutionContext): string | null {
     }
   }
 
-  // Search all model files
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.includes('app/models/') && file.endsWith('.rb')) {
-      const nodes = context.getNodesInFile(file);
-      const modelNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (modelNode) {
-        return modelNode.id;
-      }
-    }
-  }
+  // Fall back to name-based lookup
+  const candidates = context.getNodesByName(name);
+  const modelNode = candidates.find(
+    (n) => n.kind === 'class' && n.filePath.includes('app/models/')
+  );
+  if (modelNode) return modelNode.id;
 
   return null;
 }
 
 function resolveController(name: string, context: ResolutionContext): string | null {
-  // Convert CamelCase to snake_case
+  // Try direct file path lookup first
   const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
   const possiblePaths = [
     `app/controllers/${snakeName}.rb`,
@@ -246,19 +239,12 @@ function resolveController(name: string, context: ResolutionContext): string | n
     }
   }
 
-  // Search all controller files
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.includes('controllers/') && file.endsWith('.rb')) {
-      const nodes = context.getNodesInFile(file);
-      const controllerNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (controllerNode) {
-        return controllerNode.id;
-      }
-    }
-  }
+  // Fall back to name-based lookup
+  const candidates = context.getNodesByName(name);
+  const controllerNode = candidates.find(
+    (n) => n.kind === 'class' && n.filePath.includes('controllers/')
+  );
+  if (controllerNode) return controllerNode.id;
 
   return null;
 }

+ 34 - 86
src/resolution/frameworks/rust.ts

@@ -18,7 +18,7 @@ export const rustResolver: FrameworkResolver = {
   resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
     // Pattern 1: Handler references
     if (ref.referenceName.endsWith('_handler') || ref.referenceName.startsWith('handle_')) {
-      const result = resolveHandler(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, FUNCTION_KINDS, HANDLER_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -31,7 +31,7 @@ export const rustResolver: FrameworkResolver = {
 
     // Pattern 2: Service/Repository trait implementations
     if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository')) {
-      const result = resolveService(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -44,7 +44,7 @@ export const rustResolver: FrameworkResolver = {
 
     // Pattern 3: Struct references (PascalCase)
     if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
-      const result = resolveStruct(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, STRUCT_KINDS, MODEL_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -153,91 +153,39 @@ export const rustResolver: FrameworkResolver = {
   },
 };
 
-// Helper functions
+// Directory patterns
+const HANDLER_DIRS = ['/handlers/', '/handler/', '/api/', '/routes/', '/controllers/'];
+const SERVICE_DIRS = ['/services/', '/service/', '/repository/', '/domain/'];
+const MODEL_DIRS = ['/models/', '/model/', '/entities/', '/entity/', '/domain/', '/types/'];
 
-function resolveHandler(name: string, context: ResolutionContext): string | null {
-  const handlerDirs = ['handlers', 'handler', 'api', 'routes', 'controllers'];
+const FUNCTION_KINDS = new Set(['function']);
+const SERVICE_KINDS = new Set(['struct', 'trait']);
+const STRUCT_KINDS = new Set(['struct']);
 
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.rs') && handlerDirs.some((d) => file.includes(`/${d}/`) || file.includes(`/${d}.rs`))) {
-      const nodes = context.getNodesInFile(file);
-      const handlerNode = nodes.find(
-        (n) => n.kind === 'function' && n.name === name
-      );
-      if (handlerNode) {
-        return handlerNode.id;
-      }
-    }
-  }
-
-  // Search all Rust files
-  for (const file of allFiles) {
-    if (file.endsWith('.rs')) {
-      const nodes = context.getNodesInFile(file);
-      const handlerNode = nodes.find(
-        (n) => n.kind === 'function' && n.name === name
-      );
-      if (handlerNode) {
-        return handlerNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveService(name: string, context: ResolutionContext): string | null {
-  const serviceDirs = ['services', 'service', 'repository', 'domain'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.rs') && serviceDirs.some((d) => file.includes(`/${d}/`) || file.includes(`/${d}.rs`))) {
-      const nodes = context.getNodesInFile(file);
-      const serviceNode = nodes.find(
-        (n) => (n.kind === 'struct' || n.kind === 'trait') && n.name === name
-      );
-      if (serviceNode) {
-        return serviceNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveStruct(name: string, context: ResolutionContext): string | null {
-  const modelDirs = ['models', 'model', 'entities', 'entity', 'domain', 'types'];
-
-  const allFiles = context.getAllFiles();
-
-  // Check model directories first
-  for (const file of allFiles) {
-    if (file.endsWith('.rs') && modelDirs.some((d) => file.includes(`/${d}/`) || file.includes(`/${d}.rs`))) {
-      const nodes = context.getNodesInFile(file);
-      const structNode = nodes.find(
-        (n) => n.kind === 'struct' && n.name === name
-      );
-      if (structNode) {
-        return structNode.id;
-      }
-    }
-  }
-
-  // Search all Rust files
-  for (const file of allFiles) {
-    if (file.endsWith('.rs')) {
-      const nodes = context.getNodesInFile(file);
-      const structNode = nodes.find(
-        (n) => n.kind === 'struct' && n.name === name
-      );
-      if (structNode) {
-        return structNode.id;
-      }
-    }
-  }
-
-  return null;
+/**
+ * Resolve a symbol by name using indexed queries instead of scanning all files.
+ */
+function resolveByNameAndKind(
+  name: string,
+  kinds: Set<string>,
+  preferredDirPatterns: string[],
+  context: ResolutionContext,
+): string | null {
+  const candidates = context.getNodesByName(name);
+  if (candidates.length === 0) return null;
+
+  const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
+  if (kindFiltered.length === 0) return null;
+
+  // Prefer candidates in framework-conventional directories
+  const preferred = kindFiltered.filter((n) =>
+    preferredDirPatterns.some((d) => n.filePath.includes(d))
+  );
+
+  if (preferred.length > 0) return preferred[0]!.id;
+
+  // Fall back to any match
+  return kindFiltered[0]!.id;
 }
 
 function resolveModule(name: string, context: ResolutionContext): string | null {

+ 10 - 32
src/resolution/frameworks/svelte.ts

@@ -201,47 +201,25 @@ function isPascalCase(str: string): boolean {
 }
 
 /**
- * Resolve a Svelte component reference to its .svelte file
+ * Resolve a Svelte component reference using name-based lookup
  */
 function resolveComponent(
   name: string,
   fromFile: string,
   context: ResolutionContext
 ): string | null {
-  // Look for matching .svelte files
-  const allFiles = context.getAllFiles();
-  const svelteFiles = allFiles.filter((f) => f.endsWith('.svelte'));
-
-  // Check for exact name match (Button -> Button.svelte)
-  for (const file of svelteFiles) {
-    const fileName = file.split(/[/\\]/).pop() || '';
-    const componentName = fileName.replace(/\.svelte$/, '');
-    if (componentName === name) {
-      const nodes = context.getNodesInFile(file);
-      const component = nodes.find((n) => n.kind === 'component' && n.name === name);
-      if (component) {
-        return component.id;
-      }
-    }
-  }
+  // Look for component nodes by name
+  const candidates = context.getNodesByName(name);
+  const components = candidates.filter((n) => n.kind === 'component');
+
+  if (components.length === 0) return null;
 
-  // Check same directory first for better specificity
+  // Prefer same directory
   const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
-  for (const file of svelteFiles) {
-    if (file.startsWith(fromDir)) {
-      const fileName = file.split(/[/\\]/).pop() || '';
-      const componentName = fileName.replace(/\.svelte$/, '');
-      if (componentName === name) {
-        const nodes = context.getNodesInFile(file);
-        const component = nodes.find((n) => n.kind === 'component');
-        if (component) {
-          return component.id;
-        }
-      }
-    }
-  }
+  const sameDir = components.filter((n) => n.filePath.startsWith(fromDir));
+  if (sameDir.length > 0) return sameDir[0]!.id;
 
-  return null;
+  return components[0]!.id;
 }
 
 /**

+ 49 - 213
src/resolution/frameworks/swift.ts

@@ -35,7 +35,7 @@ export const swiftUIResolver: FrameworkResolver = {
   resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
     // Pattern 1: View references (SwiftUI views are PascalCase ending in View)
     if (ref.referenceName.endsWith('View') && /^[A-Z]/.test(ref.referenceName)) {
-      const result = resolveView(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, VIEW_KINDS, VIEW_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -48,7 +48,7 @@ export const swiftUIResolver: FrameworkResolver = {
 
     // Pattern 2: ViewModel/ObservableObject references
     if (ref.referenceName.endsWith('ViewModel') || ref.referenceName.endsWith('Store') || ref.referenceName.endsWith('Manager')) {
-      const result = resolveViewModel(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, VIEWMODEL_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -61,7 +61,7 @@ export const swiftUIResolver: FrameworkResolver = {
 
     // Pattern 3: Model references
     if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
-      const result = resolveModel(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, MODEL_KINDS, MODEL_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -153,7 +153,7 @@ export const uikitResolver: FrameworkResolver = {
   resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
     // Pattern 1: ViewController references
     if (ref.referenceName.endsWith('ViewController')) {
-      const result = resolveViewController(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, VC_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -166,7 +166,7 @@ export const uikitResolver: FrameworkResolver = {
 
     // Pattern 2: UIView subclass references
     if (ref.referenceName.endsWith('View') && !ref.referenceName.endsWith('ViewController')) {
-      const result = resolveUIView(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, UIVIEW_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -179,7 +179,7 @@ export const uikitResolver: FrameworkResolver = {
 
     // Pattern 3: Cell references
     if (ref.referenceName.endsWith('Cell')) {
-      const result = resolveCell(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, CELL_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -192,7 +192,7 @@ export const uikitResolver: FrameworkResolver = {
 
     // Pattern 4: Delegate/DataSource references
     if (ref.referenceName.endsWith('Delegate') || ref.referenceName.endsWith('DataSource')) {
-      const result = resolveProtocol(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, PROTOCOL_KINDS, [], context);
       if (result) {
         return {
           original: ref,
@@ -286,7 +286,7 @@ export const vaporResolver: FrameworkResolver = {
   resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
     // Pattern 1: Controller references
     if (ref.referenceName.endsWith('Controller')) {
-      const result = resolveVaporController(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, VAPOR_CONTROLLER_KINDS, VAPOR_CONTROLLER_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -299,7 +299,7 @@ export const vaporResolver: FrameworkResolver = {
 
     // Pattern 2: Model references (Fluent)
     if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
-      const result = resolveFluentModel(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, FLUENT_MODEL_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -312,7 +312,7 @@ export const vaporResolver: FrameworkResolver = {
 
     // Pattern 3: Middleware references
     if (ref.referenceName.endsWith('Middleware')) {
-      const result = resolveVaporMiddleware(ref.referenceName, context);
+      const result = resolveByNameAndKind(ref.referenceName, VAPOR_CONTROLLER_KINDS, VAPOR_MIDDLEWARE_DIRS, context);
       if (result) {
         return {
           original: ref,
@@ -382,210 +382,46 @@ export const vaporResolver: FrameworkResolver = {
   },
 };
 
-// Helper functions for SwiftUI
+// Directory patterns
+const VIEW_DIRS = ['/Views/', '/View/', '/Screens/', '/Components/', '/UI/'];
+const VIEWMODEL_DIRS = ['/ViewModels/', '/ViewModel/', '/Stores/', '/Managers/', '/Services/'];
+const MODEL_DIRS = ['/Models/', '/Model/', '/Entities/', '/Domain/'];
+const VC_DIRS = ['/ViewControllers/', '/ViewController/', '/Controllers/', '/Screens/'];
+const UIVIEW_DIRS = ['/Views/', '/View/', '/UI/', '/Components/'];
+const CELL_DIRS = ['/Cells/', '/Cell/', '/Views/', '/TableViewCells/', '/CollectionViewCells/'];
+const VAPOR_CONTROLLER_DIRS = ['/Controllers/', '/Controller/', '/Routes/'];
+const FLUENT_MODEL_DIRS = ['/Models/', '/Model/', '/Entities/', '/Database/'];
+const VAPOR_MIDDLEWARE_DIRS = ['/Middleware/', '/Middlewares/'];
+
+const VIEW_KINDS = new Set(['struct', 'component']);
+const CLASS_KINDS = new Set(['class']);
+const MODEL_KINDS = new Set(['struct', 'class']);
+const PROTOCOL_KINDS = new Set(['protocol']);
+const VAPOR_CONTROLLER_KINDS = new Set(['class', 'struct']);
 
-function resolveView(name: string, context: ResolutionContext): string | null {
-  const viewDirs = ['Views', 'View', 'Screens', 'Components', 'UI'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.swift') && viewDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const viewNode = nodes.find(
-        (n) => (n.kind === 'struct' || n.kind === 'component') && n.name === name
-      );
-      if (viewNode) {
-        return viewNode.id;
-      }
-    }
-  }
-
-  // Search all Swift files
-  for (const file of allFiles) {
-    if (file.endsWith('.swift')) {
-      const nodes = context.getNodesInFile(file);
-      const viewNode = nodes.find(
-        (n) => (n.kind === 'struct' || n.kind === 'component') && n.name === name
-      );
-      if (viewNode) {
-        return viewNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveViewModel(name: string, context: ResolutionContext): string | null {
-  const vmDirs = ['ViewModels', 'ViewModel', 'Stores', 'Managers', 'Services'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.swift') && vmDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const vmNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (vmNode) {
-        return vmNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveModel(name: string, context: ResolutionContext): string | null {
-  const modelDirs = ['Models', 'Model', 'Entities', 'Domain'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.swift') && modelDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const modelNode = nodes.find(
-        (n) => (n.kind === 'struct' || n.kind === 'class') && n.name === name
-      );
-      if (modelNode) {
-        return modelNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-// Helper functions for UIKit
-
-function resolveViewController(name: string, context: ResolutionContext): string | null {
-  const vcDirs = ['ViewControllers', 'ViewController', 'Controllers', 'Screens'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.swift') && (vcDirs.some((d) => file.includes(`/${d}/`)) || file.includes(name))) {
-      const nodes = context.getNodesInFile(file);
-      const vcNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (vcNode) {
-        return vcNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveUIView(name: string, context: ResolutionContext): string | null {
-  const viewDirs = ['Views', 'View', 'UI', 'Components'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.swift') && viewDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const viewNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (viewNode) {
-        return viewNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveCell(name: string, context: ResolutionContext): string | null {
-  const cellDirs = ['Cells', 'Cell', 'Views', 'TableViewCells', 'CollectionViewCells'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.swift') && cellDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const cellNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (cellNode) {
-        return cellNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveProtocol(name: string, context: ResolutionContext): string | null {
-  const allFiles = context.getAllFiles();
-
-  for (const file of allFiles) {
-    if (file.endsWith('.swift')) {
-      const nodes = context.getNodesInFile(file);
-      const protocolNode = nodes.find(
-        (n) => n.kind === 'protocol' && n.name === name
-      );
-      if (protocolNode) {
-        return protocolNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-// Helper functions for Vapor
-
-function resolveVaporController(name: string, context: ResolutionContext): string | null {
-  const controllerDirs = ['Controllers', 'Controller', 'Routes'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.swift') && controllerDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const controllerNode = nodes.find(
-        (n) => (n.kind === 'class' || n.kind === 'struct') && n.name === name
-      );
-      if (controllerNode) {
-        return controllerNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveFluentModel(name: string, context: ResolutionContext): string | null {
-  const modelDirs = ['Models', 'Model', 'Entities', 'Database'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.swift') && modelDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const modelNode = nodes.find(
-        (n) => n.kind === 'class' && n.name === name
-      );
-      if (modelNode) {
-        return modelNode.id;
-      }
-    }
-  }
-
-  return null;
-}
-
-function resolveVaporMiddleware(name: string, context: ResolutionContext): string | null {
-  const middlewareDirs = ['Middleware', 'Middlewares'];
-
-  const allFiles = context.getAllFiles();
-  for (const file of allFiles) {
-    if (file.endsWith('.swift') && middlewareDirs.some((d) => file.includes(`/${d}/`))) {
-      const nodes = context.getNodesInFile(file);
-      const mwNode = nodes.find(
-        (n) => (n.kind === 'class' || n.kind === 'struct') && n.name === name
-      );
-      if (mwNode) {
-        return mwNode.id;
-      }
-    }
+/**
+ * Resolve a symbol by name using indexed queries instead of scanning all files.
+ */
+function resolveByNameAndKind(
+  name: string,
+  kinds: Set<string>,
+  preferredDirPatterns: string[],
+  context: ResolutionContext,
+): string | null {
+  const candidates = context.getNodesByName(name);
+  if (candidates.length === 0) return null;
+
+  const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
+  if (kindFiltered.length === 0) return null;
+
+  // Prefer candidates in framework-conventional directories
+  if (preferredDirPatterns.length > 0) {
+    const preferred = kindFiltered.filter((n) =>
+      preferredDirPatterns.some((d) => n.filePath.includes(d))
+    );
+    if (preferred.length > 0) return preferred[0]!.id;
   }
 
-  return null;
+  // Fall back to any match
+  return kindFiltered[0]!.id;
 }

+ 23 - 1
src/search/query-utils.ts

@@ -25,17 +25,39 @@ export const STOP_WORDS = new Set([
   'over', 'only', 'new', 'out', 'its', 'so', 'up', 'as', 'if',
   // Code-specific noise
   'code', 'file', 'files', 'function', 'method', 'class', 'type',
-  'build', 'run', 'test', 'fix', 'bug', 'call', 'called', 'set', 'add',
+  'build', 'fix', 'bug', 'called', 'set', 'add',
 ]);
 
 /**
  * Extract meaningful search terms from a natural language query.
  * Splits camelCase, PascalCase, snake_case, SCREAMING_SNAKE, and dot.notation
  * into individual tokens before filtering.
+ *
+ * Preserves original compound identifiers (e.g., "scrapeLoop") alongside
+ * their split parts so that FTS can match both the full symbol name and
+ * individual words within it.
  */
 export function extractSearchTerms(query: string): string[] {
   const tokens = new Set<string>();
 
+  // First, extract and preserve compound identifiers before splitting
+  // CamelCase: scrapeLoop, UserService, getCallGraph
+  const compoundPattern = /\b([a-zA-Z][a-zA-Z0-9]*(?:[A-Z][a-z]+)+|[A-Z][a-z]+(?:[A-Z][a-z]*)+)\b/g;
+  let match;
+  while ((match = compoundPattern.exec(query)) !== null) {
+    if (match[1] && match[1].length >= 3) {
+      tokens.add(match[1].toLowerCase()); // preserve full compound: "scrapeloop"
+    }
+  }
+
+  // snake_case: scrape_loop, user_service
+  const snakePattern = /\b([a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+)+)\b/g;
+  while ((match = snakePattern.exec(query)) !== null) {
+    if (match[1] && match[1].length >= 3) {
+      tokens.add(match[1].toLowerCase());
+    }
+  }
+
   // Split camelCase / PascalCase: "getUserName" → "get User Name"
   const camelSplit = query
     .replace(/([a-z])([A-Z])/g, '$1 $2')