fix(KB-143): fix parseFileScopeFromPrompt regex and add tests

- Replace single-pass multiline regex with heading match + slice approach to avoid lazy quantifier issues
- Fix section extraction to correctly handle File Scope as both mid-file and last section
- Add tests for File Scope followed by another heading, at end of file, missing section, missing PROMPT.md, and glob patterns
This commit is contained in:
Dustin Byrne
2026-03-28 00:14:07 -04:00
parent 5e4ee94424
commit 9c5091e752
2 changed files with 118 additions and 6 deletions

View File

@@ -757,6 +757,115 @@ describe("TaskStore", () => {
});
});
describe("parseFileScopeFromPrompt", () => {
it("returns paths when File Scope is followed by another heading", async () => {
const task = await store.createTask({ description: "Mid-file scope" });
const dir = join(rootDir, ".kb", "tasks", task.id);
await writeFile(
join(dir, "PROMPT.md"),
`# ${task.id}: Mid-file scope
## File Scope
- \`packages/core/src/store.ts\`
- \`packages/core/src/store.test.ts\`
## Steps
### Step 0: Preflight
- [ ] Check things
`,
);
const paths = await store.parseFileScopeFromPrompt(task.id);
expect(paths).toEqual([
"packages/core/src/store.ts",
"packages/core/src/store.test.ts",
]);
});
it("returns all paths when File Scope is the last section", async () => {
const task = await store.createTask({
description: "End-of-file scope",
});
const dir = join(rootDir, ".kb", "tasks", task.id);
await writeFile(
join(dir, "PROMPT.md"),
`# ${task.id}: End-of-file scope
## Steps
### Step 0: Preflight
- [ ] Check things
## File Scope
- \`packages/core/src/store.ts\`
- \`packages/core/src/store.test.ts\`
- \`packages/core/src/utils.ts\`
`,
);
const paths = await store.parseFileScopeFromPrompt(task.id);
expect(paths).toEqual([
"packages/core/src/store.ts",
"packages/core/src/store.test.ts",
"packages/core/src/utils.ts",
]);
});
it("returns empty array when no File Scope section exists", async () => {
const task = await store.createTask({ description: "No scope" });
const dir = join(rootDir, ".kb", "tasks", task.id);
await writeFile(
join(dir, "PROMPT.md"),
`# ${task.id}: No scope
## Steps
### Step 0: Preflight
- [ ] Check things
`,
);
const paths = await store.parseFileScopeFromPrompt(task.id);
expect(paths).toEqual([]);
});
it("returns empty array when PROMPT.md does not exist", async () => {
const task = await store.createTask({ description: "No prompt" });
const dir = join(rootDir, ".kb", "tasks", task.id);
const { unlink } = await import("node:fs/promises");
await unlink(join(dir, "PROMPT.md"));
const paths = await store.parseFileScopeFromPrompt(task.id);
expect(paths).toEqual([]);
});
it("handles glob patterns in backtick-quoted paths", async () => {
const task = await store.createTask({ description: "Glob scope" });
const dir = join(rootDir, ".kb", "tasks", task.id);
await writeFile(
join(dir, "PROMPT.md"),
`# ${task.id}: Glob scope
## File Scope
- \`packages/core/*\`
- \`packages/cli/src/commands/dashboard.ts\`
- \`packages/engine/src/**/*.ts\`
`,
);
const paths = await store.parseFileScopeFromPrompt(task.id);
expect(paths).toEqual([
"packages/core/*",
"packages/cli/src/commands/dashboard.ts",
"packages/engine/src/**/*.ts",
]);
});
});
describe("columnMovedAt", () => {
it("createTask sets columnMovedAt", async () => {
const before = new Date().toISOString();

View File

@@ -500,13 +500,16 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const content = await readFile(promptPath, "utf-8");
// Find the ## File Scope section
const fileScopeMatch = content.match(
/^##\s+File\s+Scope\s*\n([\s\S]*?)(?=\n##\s|\n#\s|$)/m,
);
if (!fileScopeMatch) return [];
// Find the ## File Scope section.
// We locate the heading then slice to the next heading (or end of file)
// to avoid multiline `$` anchor issues with lazy quantifiers.
const headingMatch = content.match(/^##\s+File\s+Scope\s*$/m);
if (!headingMatch) return [];
const section = fileScopeMatch[1];
const startIdx = headingMatch.index! + headingMatch[0].length;
const rest = content.slice(startIdx);
const nextHeading = rest.search(/\n##?\s/);
const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
const paths: string[] = [];
const backtickRegex = /`([^`]+)`/g;
let match;