Merge pull request #76 from colbymchenry/fix/liquid-callers-and-context-relevance
fix: Fix Liquid template callers and context relevance
This commit is contained in:
+49
-15
@@ -23,9 +23,9 @@ import { QueryBuilder } from '../db/queries';
|
||||
import { GraphTraverser } from '../graph';
|
||||
import { VectorManager } from '../vectors';
|
||||
import { formatContextAsMarkdown, formatContextAsJson } from './formatter';
|
||||
import { logDebug, logWarn } from '../errors';
|
||||
import { logDebug } from '../errors';
|
||||
import { validatePathWithinRoot } from '../utils';
|
||||
import { isTestFile } from '../search/query-utils';
|
||||
import { isTestFile, extractSearchTerms } from '../search/query-utils';
|
||||
|
||||
/**
|
||||
* Extract likely symbol names from a natural language query
|
||||
@@ -298,21 +298,47 @@ export class ContextBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Fall back to text search if no semantic results
|
||||
if (semanticResults.length === 0 && exactMatches.length === 0) {
|
||||
try {
|
||||
const textResults = this.queries.searchNodes(query, {
|
||||
limit: opts.searchLimit,
|
||||
kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
|
||||
});
|
||||
semanticResults = textResults;
|
||||
} catch (error) {
|
||||
logWarn('Text search failed', { query, error: String(error) });
|
||||
// Return empty results
|
||||
// Step 4: Always run text search for natural language term matching
|
||||
// This catches file-name and node-name matches that semantic search may miss,
|
||||
// which is critical for template-heavy codebases (e.g., Liquid/Shopify themes)
|
||||
// where file names are the primary identifiers.
|
||||
let textResults: SearchResult[] = [];
|
||||
try {
|
||||
const searchTerms = extractSearchTerms(query);
|
||||
if (searchTerms.length > 0) {
|
||||
// Search each term individually to get broader coverage,
|
||||
// then boost results that match multiple terms
|
||||
const termResultsMap = new Map<string, { result: SearchResult; termHits: number }>();
|
||||
for (const term of searchTerms) {
|
||||
const termResults = this.queries.searchNodes(term, {
|
||||
limit: opts.searchLimit * 2,
|
||||
kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
|
||||
});
|
||||
for (const r of termResults) {
|
||||
const existing = termResultsMap.get(r.node.id);
|
||||
if (existing) {
|
||||
existing.termHits++;
|
||||
existing.result.score = Math.max(existing.result.score, r.score);
|
||||
} else {
|
||||
termResultsMap.set(r.node.id, { result: r, termHits: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Boost results matching multiple terms and sort
|
||||
textResults = Array.from(termResultsMap.values())
|
||||
.map(({ result, termHits }) => ({
|
||||
...result,
|
||||
score: result.score + (termHits - 1) * 5,
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, opts.searchLimit * 2);
|
||||
}
|
||||
logDebug('Text search results', { count: textResults.length });
|
||||
} catch (error) {
|
||||
logDebug('Text search failed', { query, error: String(error) });
|
||||
}
|
||||
|
||||
// Step 5: Merge results, prioritizing exact matches
|
||||
// Step 5: Merge results, prioritizing exact matches, then text (path-boosted), then semantic
|
||||
const seenIds = new Set<string>();
|
||||
let searchResults: SearchResult[] = [];
|
||||
|
||||
@@ -324,7 +350,15 @@ export class ContextBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Add semantic/text results
|
||||
// Add text search results (includes path relevance scoring from searchNodes)
|
||||
for (const result of textResults) {
|
||||
if (!seenIds.has(result.node.id)) {
|
||||
seenIds.add(result.node.id);
|
||||
searchResults.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Add semantic results
|
||||
for (const result of semanticResults) {
|
||||
if (!seenIds.has(result.node.id)) {
|
||||
seenIds.add(result.node.id);
|
||||
|
||||
@@ -248,7 +248,7 @@ export class GraphTraverser {
|
||||
}
|
||||
visited.add(nodeId);
|
||||
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls']);
|
||||
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls', 'references', 'imports']);
|
||||
|
||||
for (const edge of incomingEdges) {
|
||||
const callerNode = this.queries.getNodeById(edge.source);
|
||||
@@ -287,7 +287,7 @@ export class GraphTraverser {
|
||||
}
|
||||
visited.add(nodeId);
|
||||
|
||||
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['calls']);
|
||||
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['calls', 'references', 'imports']);
|
||||
|
||||
for (const edge of outgoingEdges) {
|
||||
const calleeNode = this.queries.getNodeById(edge.target);
|
||||
|
||||
+6
-1
@@ -874,7 +874,12 @@ export class ToolHandler {
|
||||
return { nodes: [], note: '' };
|
||||
}
|
||||
|
||||
const exactMatches = results.filter(r => r.node.name === symbol);
|
||||
// Match by exact name, OR by file basename without extension
|
||||
// (e.g., "product-card" matches file node named "product-card.liquid")
|
||||
const exactMatches = results.filter(r =>
|
||||
r.node.name === symbol ||
|
||||
(r.node.kind === 'file' && r.node.name.replace(/\.[^.]+$/, '') === symbol)
|
||||
);
|
||||
|
||||
if (exactMatches.length <= 1) {
|
||||
const node = exactMatches[0]?.node ?? results[0]!.node;
|
||||
|
||||
Reference in New Issue
Block a user