fix(context-bill): walk both links to one directory so the walk never depends on readdir order

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-25 23:05:30 +00:00
1 parent 9bf11e738e
commit 7f3ab43ed2
2 files changed
+9 -8

No files matched your search

+7 -8
View File
@@ -233,8 +233,9 @@ function encloses(outer: string, inner: string): boolean {
*
* Symlinked files and directories are followed, and every returned path stays
* the lexical path under `dir`, so buildBill and --exact key the same files.
* Each real directory is walked once (cycle guard), and a link to a directory
* that encloses `dir` is never followed.
* Two links to one directory are both walked (each is real scanner load), so
* the result never depends on readdir order. A directory that is already on
* the current walk (a loop) or that encloses `dir` is never entered.
*/
export function walkMd(dir: string): string[] {
const out: string[] = [];
@@ -244,8 +245,7 @@ export function walkMd(dir: string): string[] {
} catch {
return out;
}
const visited = new Set([rootReal]);
const walk = (current: string) => {
const walk = (current: string, ancestors: string[]) => {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(current, { withFileTypes: true });
@@ -267,12 +267,11 @@ export function walkMd(dir: string): string[] {
} catch {
continue;
}
if (visited.has(real) || encloses(real, rootReal)) continue;
visited.add(real);
walk(p);
if (ancestors.includes(real) || encloses(real, rootReal)) continue;
walk(p, [...ancestors, real]);
}
};
walk(dir);
walk(dir, [rootReal]);
return out;
}
+2
View File
@@ -273,7 +273,9 @@ describe("upstream fix b: installed-tree layout (repo-checkout subdir skip)", ()
}
// Every path the bill (and --exact) keys on stays under the skills root.
for (const file of walkMd(skills)) expect(file.startsWith(skills + path.sep)).toBe(true);
// dirlink/SKILL.md links to review/sections, so its file is walked under both names.
expect(walkMd(skills).map((f) => path.relative(skills, f)).sort()).toEqual([
path.join("dirlink", "SKILL.md", "flow.md"),
path.join("office-hours", "SKILL.md"),
path.join("office-hours", "sections", "flow.md"),
path.join("review", "SKILL.md"),