Skip to content

Commit b27777c

Browse files
fix: rank files explicitly named in the task into context (#37)
Files named verbatim in the task text (tests/auth.test.ts, server.ts) never reached contextFiles because test files are excluded from ranking candidates, so agents only saw them via testRoutes[].relatedFiles. - extract file-path mentions from the task text and match them against repository paths (exact, path-suffix, or mention-suffix with a / boundary); ambiguous bare filenames matching more than 5 files are ignored - mentioned files bypass the test/lockfile/benchmark candidate filters and score +12 with an inspectable 'explicitly named in the task' reason, ranking them high-confidence - add JavaScript/TypeScript reserved words (async, await, throw, class, null, undefined, ...) to the stop list so generic code tokens no longer count as content matches - add an evaluation case locking in explicit-mention ranking Fixes #22 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent d86ab8c commit b27777c

5 files changed

Lines changed: 249 additions & 7 deletions

File tree

benchmarks/cases.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,9 @@
2222
{
2323
"task": "Improve the contributor setup guide and pull request instructions",
2424
"expected": ["CONTRIBUTING.md"]
25+
},
26+
{
27+
"task": "packages/core/test/rank.test.ts fails because files explicitly named in the task drop out of context ranking",
28+
"expected": ["packages/core/test/rank.test.ts", "packages/core/src/rank.ts"]
2529
}
2630
]

packages/action/dist/index.mjs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,33 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
1212
"and",
1313
"any",
1414
"are",
15+
"async",
16+
"await",
1517
"been",
1618
"being",
1719
"both",
20+
"break",
1821
"but",
1922
"can",
2023
"cannot",
24+
"case",
25+
"catch",
26+
"class",
2127
"const",
28+
"continue",
2229
"could",
30+
"debugger",
2331
"default",
32+
"delete",
2433
"did",
2534
"doe",
2635
"does",
2736
"down",
37+
"else",
38+
"enum",
39+
"extends",
40+
"false",
41+
"finally",
2842
"each",
2943
"even",
3044
"export",
@@ -41,23 +55,29 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
4155
"him",
4256
"his",
4357
"how",
58+
"implements",
4459
"import",
4560
"index",
61+
"instanceof",
4662
"instead",
63+
"interface",
4764
"into",
4865
"its",
4966
"just",
67+
"let",
5068
"main",
5169
"may",
5270
"might",
5371
"more",
5472
"most",
5573
"must",
5674
"name",
75+
"namespace",
5776
"new",
5877
"node",
5978
"not",
6079
"now",
80+
"null",
6181
"off",
6282
"only",
6383
"other",
@@ -66,15 +86,22 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
6686
"over",
6787
"package",
6888
"packages",
89+
"private",
90+
"protected",
91+
"public",
92+
"readonly",
6993
"return",
7094
"run",
7195
"same",
7296
"she",
7397
"should",
7498
"some",
7599
"src",
100+
"static",
76101
"still",
77102
"such",
103+
"super",
104+
"switch",
78105
"than",
79106
"that",
80107
"the",
@@ -86,11 +113,17 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
86113
"they",
87114
"this",
88115
"those",
116+
"throw",
89117
"true",
118+
"try",
90119
"type",
120+
"typeof",
91121
"under",
122+
"undefined",
92123
"uses",
124+
"var",
93125
"very",
126+
"void",
94127
"was",
95128
"were",
96129
"what",
@@ -103,16 +136,29 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
103136
"will",
104137
"with",
105138
"would",
139+
"yield",
106140
"you",
107141
"your"
108142
]);
143+
var FILE_MENTION_PATTERN = /[A-Za-z0-9_@$][A-Za-z0-9_.$/\\-]*\.[A-Za-z][A-Za-z0-9]*/g;
109144
function extractTaskSignals(input) {
110145
const tokens = tokenizeText([input.issueText ?? "", extractDiffContentLines(input.diffText ?? "")].join("\n"));
111146
return {
112147
tokens,
113-
changedFiles: new Set(input.changedFiles ?? [])
148+
changedFiles: new Set(input.changedFiles ?? []),
149+
fileMentions: extractFileMentions(input.issueText ?? "")
114150
};
115151
}
152+
function extractFileMentions(text) {
153+
const mentions = /* @__PURE__ */ new Set();
154+
for (const match of text.matchAll(FILE_MENTION_PATTERN)) {
155+
const cleaned = match[0].replace(/\\/g, "/").replace(/^\.\.?\//, "");
156+
if (cleaned.length >= 4) {
157+
mentions.add(cleaned);
158+
}
159+
}
160+
return mentions;
161+
}
116162
function extractDiffContentLines(diffText) {
117163
if (!diffText) {
118164
return "";
@@ -155,14 +201,16 @@ var DEPLOYMENT_TERMS = [
155201
"502"
156202
];
157203
var LOCKFILES = /* @__PURE__ */ new Set(["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"]);
204+
var MAX_FILES_PER_MENTION = 5;
158205
function rankContextFiles(repo, input, limit = 8) {
159206
const signals = extractTaskSignals({
160207
issueText: input.issueText ?? "",
161208
diffText: input.diffText ?? "",
162209
changedFiles: repo.changedFiles
163210
});
211+
const mentionedPaths = matchMentionedPaths(signals.fileMentions, repo.files.map((file) => file.path));
164212
const taskTargetsEvaluation = hasAny(signals.tokens, ["benchmark", "benchmarks", "evaluation", "evaluate"]);
165-
const candidates = repo.files.filter((file) => file.isSource && !file.isTest && !LOCKFILES.has(file.path.split("/").pop() ?? "") && (!file.path.startsWith("benchmarks/") || taskTargetsEvaluation));
213+
const candidates = repo.files.filter((file) => mentionedPaths.has(file.path) || file.isSource && !file.isTest && !LOCKFILES.has(file.path.split("/").pop() ?? "") && (!file.path.startsWith("benchmarks/") || taskTargetsEvaluation));
166214
const contentTokensByPath = new Map(candidates.map((file) => [file.path, tokenizeText(file.textSample)]));
167215
const commonTokens = findCommonTokens(contentTokensByPath);
168216
const taskTargetsDocumentation = hasAny(signals.tokens, ["docs", "documentation", "readme", "guide", "copy"]);
@@ -176,6 +224,10 @@ function rankContextFiles(repo, input, limit = 8) {
176224
score += 20;
177225
reasons.push("changed file");
178226
}
227+
if (mentionedPaths.has(file.path)) {
228+
score += 12;
229+
reasons.push("explicitly named in the task");
230+
}
179231
const pathTokens = tokenizePath(file.path);
180232
const pathOverlap = [...pathTokens].filter((token2) => signals.tokens.has(token2));
181233
if (pathOverlap.length > 0) {
@@ -234,6 +286,18 @@ function confidenceForScore(score, isChanged) {
234286
function hasAny(tokens, values) {
235287
return values.some((value) => tokens.has(value));
236288
}
289+
function matchMentionedPaths(mentions, repoPaths) {
290+
const matched = /* @__PURE__ */ new Set();
291+
for (const mention of mentions) {
292+
const matches = repoPaths.filter((path) => path === mention || path.endsWith(`/${mention}`) || mention.endsWith(`/${path}`));
293+
if (matches.length > 0 && matches.length <= MAX_FILES_PER_MENTION) {
294+
for (const path of matches) {
295+
matched.add(path);
296+
}
297+
}
298+
}
299+
return matched;
300+
}
237301
function findCommonTokens(contentTokensByPath) {
238302
const fileCount = contentTokensByPath.size;
239303
if (fileCount < 4) {

packages/core/src/rank.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const DEPLOYMENT_TERMS = [
55
"deploy", "deployment", "vercel", "netlify", "docker", "kubernetes", "hosting", "serverless", "production", "404", "500", "502"
66
];
77
const LOCKFILES = new Set(["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"]);
8+
const MAX_FILES_PER_MENTION = 5;
89

910
export function rankContextFiles(
1011
repo: RepoMap,
@@ -17,12 +18,14 @@ export function rankContextFiles(
1718
changedFiles: repo.changedFiles
1819
});
1920

21+
const mentionedPaths = matchMentionedPaths(signals.fileMentions, repo.files.map((file) => file.path));
2022
const taskTargetsEvaluation = hasAny(signals.tokens, ["benchmark", "benchmarks", "evaluation", "evaluate"]);
2123
const candidates = repo.files.filter((file) =>
22-
file.isSource &&
23-
!file.isTest &&
24-
!LOCKFILES.has(file.path.split("/").pop() ?? "") &&
25-
(!file.path.startsWith("benchmarks/") || taskTargetsEvaluation)
24+
mentionedPaths.has(file.path) ||
25+
(file.isSource &&
26+
!file.isTest &&
27+
!LOCKFILES.has(file.path.split("/").pop() ?? "") &&
28+
(!file.path.startsWith("benchmarks/") || taskTargetsEvaluation))
2629
);
2730
const contentTokensByPath = new Map(candidates.map((file) => [file.path, tokenizeText(file.textSample)]));
2831
const commonTokens = findCommonTokens(contentTokensByPath);
@@ -41,6 +44,11 @@ export function rankContextFiles(
4144
reasons.push("changed file");
4245
}
4346

47+
if (mentionedPaths.has(file.path)) {
48+
score += 12;
49+
reasons.push("explicitly named in the task");
50+
}
51+
4452
const pathTokens = tokenizePath(file.path);
4553
const pathOverlap = [...pathTokens].filter((token) => signals.tokens.has(token));
4654
if (pathOverlap.length > 0) {
@@ -110,6 +118,23 @@ function hasAny(tokens: Set<string>, values: string[]): boolean {
110118
return values.some((value) => tokens.has(value));
111119
}
112120

121+
function matchMentionedPaths(mentions: Set<string>, repoPaths: string[]): Set<string> {
122+
const matched = new Set<string>();
123+
124+
for (const mention of mentions) {
125+
const matches = repoPaths.filter(
126+
(path) => path === mention || path.endsWith(`/${mention}`) || mention.endsWith(`/${path}`)
127+
);
128+
if (matches.length > 0 && matches.length <= MAX_FILES_PER_MENTION) {
129+
for (const path of matches) {
130+
matched.add(path);
131+
}
132+
}
133+
}
134+
135+
return matched;
136+
}
137+
113138
function findCommonTokens(contentTokensByPath: Map<string, Set<string>>): Set<string> {
114139
const fileCount = contentTokensByPath.size;
115140
if (fileCount < 4) {

0 commit comments

Comments
 (0)