fix(review): the instruction set holds nested rules files and a link's own name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d81bd39754
commit
d4daa9d787
4 changed files with 287 additions and 19 deletions
|
|
@ -14,11 +14,17 @@
|
|||
// - the same names in each ancestor directory of each changed path (a
|
||||
// changed path that is absolute, holds `..`, `.`, an empty segment or a
|
||||
// backslash names no directory of the repo and adds nothing);
|
||||
// - `.claude/rules/*.md`.
|
||||
// - every `.md` file under the root's `.claude/rules/` at any depth
|
||||
// (`.claude/rules/sub/x.md` included), the test `isInstructionFile` uses;
|
||||
// - the same under `<dir>/.claude/rules/` for each such ancestor directory
|
||||
// `<dir>` of a changed path (`pkg/.claude/rules/x.md` only when a changed
|
||||
// path lies under `pkg/`).
|
||||
// An entry with mode 120000 (a symlink) has its target read with
|
||||
// `git cat-file`. A target resolved against the link's directory that stays
|
||||
// inside the repo and is a tracked regular file replaces the link; any other
|
||||
// target (absolute, out of the repo, missing, another link) drops the entry.
|
||||
// inside the repo and is a tracked regular file enters the set next to the
|
||||
// link, which keeps its own name (CLAUDE.md -> AGENTS.md gives both paths);
|
||||
// any other target (absolute, out of the repo, missing, another link) drops
|
||||
// the entry, link name included.
|
||||
// Every path is repo-relative posix. A git failure, or a sha that is not a
|
||||
// full commit id, gives `{ok:false, reason}`.
|
||||
//
|
||||
|
|
@ -60,7 +66,9 @@ const SYMLINK_MODE = '120000';
|
|||
const REGULAR_MODES = new Set(['100644', '100755']);
|
||||
const TREE_ENTRY_RE = /^(\d{6}) ([a-z]+) ([0-9a-f]{40}|[0-9a-f]{64})$/;
|
||||
const NAMES = ['CLAUDE.md', 'AGENTS.md'];
|
||||
const RULES_RE = /^\.claude\/rules\/[^/]+\.md$/;
|
||||
const RULES_DIR = '.claude/rules/';
|
||||
// The `.md` test of isInstructionFile below, for the part after `<d>/.claude/rules/`.
|
||||
const RULES_REST_RE = /^(?:[^/]+\/)*[^/]+\.md$/;
|
||||
|
||||
// The argv git helper of lib/review/reviewed-commit.mjs:64-66 (copied, not exported there).
|
||||
function git(repo, args) {
|
||||
|
|
@ -103,22 +111,36 @@ function treeAt(repo, sha) {
|
|||
}
|
||||
|
||||
/**
|
||||
* The path an instruction entry stands for: itself when it is a regular file;
|
||||
* for a symlink, its tracked regular target inside the repo; else null.
|
||||
* The paths an instruction entry stands for: `[path]` when it is a regular
|
||||
* file; for a symlink whose target is a tracked regular file inside the repo,
|
||||
* `[link, target]`; else `[]`.
|
||||
*/
|
||||
function resolveEntry(repo, entries, path) {
|
||||
const e = entries.get(path);
|
||||
if (!e || e.type !== 'blob') return { ok: true, path: null };
|
||||
if (REGULAR_MODES.has(e.mode)) return { ok: true, path };
|
||||
if (e.mode !== SYMLINK_MODE) return { ok: true, path: null };
|
||||
if (!e || e.type !== 'blob') return { ok: true, paths: [] };
|
||||
if (REGULAR_MODES.has(e.mode)) return { ok: true, paths: [path] };
|
||||
if (e.mode !== SYMLINK_MODE) return { ok: true, paths: [] };
|
||||
const r = git(repo, ['cat-file', 'blob', e.hash]);
|
||||
if (r.error || r.status !== 0) return gitFailed(`cat-file ${path}`, r);
|
||||
const target = r.stdout;
|
||||
if (target === '' || target.includes('\0') || target.includes('\\') || posix.isAbsolute(target)) return { ok: true, path: null };
|
||||
if (target === '' || target.includes('\0') || target.includes('\\') || posix.isAbsolute(target)) return { ok: true, paths: [] };
|
||||
const resolved = posix.normalize(posix.join(posix.dirname(path), target));
|
||||
if (!plainPath(resolved)) return { ok: true, path: null };
|
||||
if (!plainPath(resolved)) return { ok: true, paths: [] };
|
||||
const t = entries.get(resolved);
|
||||
return { ok: true, path: t && t.type === 'blob' && REGULAR_MODES.has(t.mode) ? resolved : null };
|
||||
return { ok: true, paths: t && t.type === 'blob' && REGULAR_MODES.has(t.mode) ? [path, resolved] : [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Is `p` a `.md` file under `<d>/.claude/rules/` at any depth, for some `d`
|
||||
* in `dirs` (the root '' or a directory on a changed path)?
|
||||
*/
|
||||
function rulesFileOnPath(p, dirs) {
|
||||
for (let i = p.indexOf(RULES_DIR); i !== -1; i = p.indexOf(RULES_DIR, i + 1)) {
|
||||
if (i > 0 && p[i - 1] !== '/') continue;
|
||||
const d = i === 0 ? '' : p.slice(0, i - 1);
|
||||
if (dirs.has(d) && RULES_REST_RE.test(p.slice(i + RULES_DIR.length))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -159,12 +181,12 @@ export function instructionSet(repo, sha, changedPaths = []) {
|
|||
}
|
||||
const candidates = [];
|
||||
for (const d of dirs) for (const n of NAMES) candidates.push(d === '' ? n : `${d}/${n}`);
|
||||
for (const p of tree.entries.keys()) if (RULES_RE.test(p)) candidates.push(p);
|
||||
for (const p of tree.entries.keys()) if (rulesFileOnPath(p, dirs)) candidates.push(p);
|
||||
const files = new Set();
|
||||
for (const c of candidates) {
|
||||
const r = resolveEntry(repo, tree.entries, c);
|
||||
if (!r.ok) return r;
|
||||
if (r.path !== null) files.add(r.path);
|
||||
for (const f of r.paths) files.add(f);
|
||||
}
|
||||
return { ok: true, files: [...files].sort() };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue