fix(review): the evidence writer takes a verdict only for the branch head
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d4daa9d787
commit
5b9cbeba28
2 changed files with 296 additions and 5 deletions
|
|
@ -14,10 +14,18 @@
|
|||
// - verification: progress.json `plan_verification.summary` passed/total;
|
||||
// - review: the verdict gate (`decideVerdictFile` with the repo, R6 first)
|
||||
// on {project}/review.md, and its `scope_sha_end` (`reviewedSha`); a review
|
||||
// the gate does not decide (exit 2) has a null verdict;
|
||||
// the gate does not decide (exit 2) has a null verdict. The gate is asked
|
||||
// only when `git rev-parse --verify HEAD` in --repo is the branch head (R6
|
||||
// is HEAD-based); from any other HEAD the verdict is null ("not measured"
|
||||
// on the question) and `review.sha` is still written;
|
||||
// - door: the plan's `## Doors` list, else the CHANGELOG's first section when
|
||||
// it holds exactly one door line, else null.
|
||||
// A field that cannot be derived is null, and the guard prints "not measured".
|
||||
// A branch name git would read as something else is refused before the head
|
||||
// is read ({"written": false}, "branch name <b> is ambiguous ..."): when
|
||||
// `refs/tags/<b>` or `refs/<b>` resolves, or an entry of any kind
|
||||
// `<git dir>/<b>` or `<git common dir>/<b>` exists (so `voyage`, the evidence
|
||||
// directory's parent, is refused). The guard's reader keeps the same rule.
|
||||
// Before writing, evidence files in that directory older than 30 days are
|
||||
// removed (regular files named `<hex>.json` only). The file is written
|
||||
// atomically (a temp file in the same directory, then rename).
|
||||
|
|
@ -100,6 +108,26 @@ function git(repo, args) {
|
|||
return spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
||||
}
|
||||
|
||||
/**
|
||||
* True when git would read `branch` as something other than refs/heads/<branch>:
|
||||
* `refs/tags/<b>` or `refs/<b>` resolves (argv only, never a shell), or an
|
||||
* entry of any kind (file, directory, link) `<git dir>/<b>` or
|
||||
* `<git common dir>/<b>` exists. The guard's reader
|
||||
* (hooks/lib/merge-evidence.mjs) shows no evidence for the same names.
|
||||
*/
|
||||
function ambiguousName(repoDir, gitDir, common, branch) {
|
||||
for (const ref of [`refs/tags/${branch}`, `refs/${branch}`]) {
|
||||
if (git(repoDir, ['rev-parse', '--verify', '--quiet', ref]).status === 0) return true;
|
||||
}
|
||||
for (const dir of [gitDir, common]) {
|
||||
try { lstatSync(join(dir, branch)); return true; } catch (e) {
|
||||
// No entry is ENOENT/ENOTDIR; any other error cannot tell, so it counts as ambiguous.
|
||||
if (e?.code !== 'ENOENT' && e?.code !== 'ENOTDIR') return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const readText = (p) => { try { return existsSync(p) ? readFileSync(p, 'utf8') : null; } catch { return null; } };
|
||||
|
||||
/** Remove evidence files older than MAX_AGE_MS: regular files named `<hex>.json` only. */
|
||||
|
|
@ -131,13 +159,24 @@ export function writeEvidence({ repo, branch, projectDir, now = Date.now() } = {
|
|||
const repoDir = resolve(repo);
|
||||
const project = resolve(projectDir);
|
||||
|
||||
const head = git(repoDir, ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`]);
|
||||
const sha = head.status === 0 ? head.stdout.trim() : '';
|
||||
if (!SHA_RE.test(sha)) return { written: false, reason: `no branch ${branch} in ${repoDir}` };
|
||||
const cd = git(repoDir, ['rev-parse', '--git-common-dir']);
|
||||
const rawCommon = cd.status === 0 ? cd.stdout.trim() : '';
|
||||
if (!rawCommon) return { written: false, reason: `git rev-parse --git-common-dir failed in ${repoDir}` };
|
||||
const common = isAbsolute(rawCommon) ? rawCommon : resolve(repoDir, rawCommon);
|
||||
const gd = git(repoDir, ['rev-parse', '--git-dir']);
|
||||
const rawGitDir = gd.status === 0 ? gd.stdout.trim() : '';
|
||||
if (!rawGitDir) return { written: false, reason: `git rev-parse --git-dir failed in ${repoDir}` };
|
||||
const gitDir = isAbsolute(rawGitDir) ? rawGitDir : resolve(repoDir, rawGitDir);
|
||||
if (ambiguousName(repoDir, gitDir, common, branch)) {
|
||||
return { written: false, reason: `branch name ${branch} is ambiguous (a tag or another ref has that name)` };
|
||||
}
|
||||
|
||||
const head = git(repoDir, ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`]);
|
||||
const sha = head.status === 0 ? head.stdout.trim() : '';
|
||||
if (!SHA_RE.test(sha)) return { written: false, reason: `no branch ${branch} in ${repoDir}` };
|
||||
// The gate's R6 check is HEAD-based: it is asked only when HEAD is the branch head.
|
||||
const at = git(repoDir, ['rev-parse', '--verify', 'HEAD']);
|
||||
const headIsBranch = at.status === 0 && at.stdout.trim() === sha;
|
||||
const top = git(repoDir, ['rev-parse', '--show-toplevel']);
|
||||
const topDir = top.status === 0 && top.stdout.trim() ? top.stdout.trim() : repoDir;
|
||||
|
||||
|
|
@ -152,7 +191,7 @@ export function writeEvidence({ repo, branch, projectDir, now = Date.now() } = {
|
|||
const reviewPath = join(project, 'review.md');
|
||||
const reviewText = readText(reviewPath);
|
||||
let verdict = null;
|
||||
if (reviewText !== null) {
|
||||
if (reviewText !== null && headIsBranch) {
|
||||
try {
|
||||
const gate = decideVerdictFile(reviewPath, { repo: repoDir });
|
||||
if (gate.exit === EXIT.PASS) verdict = gate.verdict ?? null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue