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 { GraphTraverser } from '../graph';
|
||||||
import { VectorManager } from '../vectors';
|
import { VectorManager } from '../vectors';
|
||||||
import { formatContextAsMarkdown, formatContextAsJson } from './formatter';
|
import { formatContextAsMarkdown, formatContextAsJson } from './formatter';
|
||||||
import { logDebug, logWarn } from '../errors';
|
import { logDebug } from '../errors';
|
||||||
import { validatePathWithinRoot } from '../utils';
|
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
|
* 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
|
// Step 4: Always run text search for natural language term matching
|
||||||
if (semanticResults.length === 0 && exactMatches.length === 0) {
|
// This catches file-name and node-name matches that semantic search may miss,
|
||||||
try {
|
// which is critical for template-heavy codebases (e.g., Liquid/Shopify themes)
|
||||||
const textResults = this.queries.searchNodes(query, {
|
// where file names are the primary identifiers.
|
||||||
limit: opts.searchLimit,
|
let textResults: SearchResult[] = [];
|
||||||
kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
|
try {
|
||||||
});
|
const searchTerms = extractSearchTerms(query);
|
||||||
semanticResults = textResults;
|
if (searchTerms.length > 0) {
|
||||||
} catch (error) {
|
// Search each term individually to get broader coverage,
|
||||||
logWarn('Text search failed', { query, error: String(error) });
|
// then boost results that match multiple terms
|
||||||
// Return empty results
|
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>();
|
const seenIds = new Set<string>();
|
||||||
let searchResults: SearchResult[] = [];
|
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) {
|
for (const result of semanticResults) {
|
||||||
if (!seenIds.has(result.node.id)) {
|
if (!seenIds.has(result.node.id)) {
|
||||||
seenIds.add(result.node.id);
|
seenIds.add(result.node.id);
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export class GraphTraverser {
|
|||||||
}
|
}
|
||||||
visited.add(nodeId);
|
visited.add(nodeId);
|
||||||
|
|
||||||
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls']);
|
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls', 'references', 'imports']);
|
||||||
|
|
||||||
for (const edge of incomingEdges) {
|
for (const edge of incomingEdges) {
|
||||||
const callerNode = this.queries.getNodeById(edge.source);
|
const callerNode = this.queries.getNodeById(edge.source);
|
||||||
@@ -287,7 +287,7 @@ export class GraphTraverser {
|
|||||||
}
|
}
|
||||||
visited.add(nodeId);
|
visited.add(nodeId);
|
||||||
|
|
||||||
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['calls']);
|
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['calls', 'references', 'imports']);
|
||||||
|
|
||||||
for (const edge of outgoingEdges) {
|
for (const edge of outgoingEdges) {
|
||||||
const calleeNode = this.queries.getNodeById(edge.target);
|
const calleeNode = this.queries.getNodeById(edge.target);
|
||||||
|
|||||||
+6
-1
@@ -874,7 +874,12 @@ export class ToolHandler {
|
|||||||
return { nodes: [], note: '' };
|
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) {
|
if (exactMatches.length <= 1) {
|
||||||
const node = exactMatches[0]?.node ?? results[0]!.node;
|
const node = exactMatches[0]?.node ?? results[0]!.node;
|
||||||
|
|||||||
Reference in New Issue
Block a user