Skip to content

Commit a263d38

Browse files
committed
fix(agent): count each query word once in lexical search
1 parent 77ef1bd commit a263d38

3 files changed

Lines changed: 60 additions & 1 deletion

File tree

‎src/agent/lexical-rank.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import {
4+
lexicalFields,
5+
scoreLexical,
6+
tokenizeLexical,
7+
} from "./lexical-rank.js";
8+
9+
describe("scoreLexical", () => {
10+
test("counts a word repeated in the query once", () => {
11+
const fields = lexicalFields(
12+
"mcp__linear__list_release_notes",
13+
"List release notes in the workspace",
14+
);
15+
// Same raw query on both sides so only the token list differs; the raw
16+
// query feeds a separate whole-query substring bonus.
17+
const rawQuery = "release notes release";
18+
expect(
19+
scoreLexical(fields, tokenizeLexical("release notes release"), rawQuery),
20+
).toBe(scoreLexical(fields, tokenizeLexical("release notes"), rawQuery));
21+
});
22+
});

‎src/agent/lexical-rank.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export function scoreLexical(
3030
const name = fields.name.toLowerCase();
3131
const blob = fields.blob.toLowerCase();
3232
let total = 0;
33-
for (const token of queryTokens) {
33+
// A model repeats a word because its sentence needs it ("release ID or
34+
// release slug"), not to weight it; counting each copy multiplies the
35+
// bonus for every tool that matches that one word.
36+
for (const token of new Set(queryTokens)) {
3437
if (fields.nameTokens.includes(token)) total += 3;
3538
else if (fields.blobTokens.has(token)) total += 1;
3639
else if (name.includes(token)) total += 0.75;

‎src/agent/tool-search.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,33 @@ const linearCatalog: ToolDefinition[] = [
112112

113113
const linearIndex = createToolIndex(() => linearCatalog);
114114

115+
// Trimmed from the live Linear MCP catalog: release-named tools competing with
116+
// list_issues, the only one that lists a release's issues.
117+
const linearReleaseCatalog: ToolDefinition[] = [
118+
mcpDef(
119+
"mcp__linear__list_issues",
120+
'[linear] List issues in the user\'s Linear workspace, including active Triage Intelligence suggestions for issues in triage. For issues assigned to me, use "me" as the assignee. For issues created by me, use "me" as the creator. Use "null" for no assignee.',
121+
),
122+
mcpDef(
123+
"mcp__linear__get_release",
124+
"[linear] Retrieve details of a release by ID or slug.",
125+
),
126+
mcpDef(
127+
"mcp__linear__get_release_note",
128+
"[linear] Retrieve release notes by ID or slug, including markdown content.",
129+
),
130+
mcpDef(
131+
"mcp__linear__list_release_notes",
132+
"[linear] List release notes in the workspace, optionally filtered by pipeline or covered release.",
133+
),
134+
mcpDef(
135+
"mcp__linear__list_release_pipelines",
136+
"[linear] List release pipelines in the workspace.",
137+
),
138+
];
139+
140+
const linearReleaseIndex = createToolIndex(() => linearReleaseCatalog);
141+
115142
const wideCatalog: ToolDefinition[] = Array.from({ length: 12 }, (_, i) =>
116143
mcpDef(
117144
`mcp__linear__op_${String(i).padStart(2, "0")}`,
@@ -311,6 +338,13 @@ describe("createToolIndex", () => {
311338
expect(ranked.length).toBeGreaterThanOrEqual(1);
312339
expect(ranked.length).toBeLessThanOrEqual(2);
313340
});
341+
342+
test("a word repeated in the query does not bury the tool that answers it", () => {
343+
const ranked = linearReleaseIndex.search(
344+
"Linear list issues filtered by release ID or release slug",
345+
);
346+
expect(ranked).toContain("mcp__linear__list_issues");
347+
});
314348
});
315349

316350
describe("createToolSearchTool skills", () => {

0 commit comments

Comments
 (0)