fix: the findings of the release review of 6.3.0
The review of v6.2.0..f669b87 (BLOCK: 1 BLOCKER, 12 MAJOR, 1 MINOR), each fixed behind a test that failed first, or, for the two missing tests, a test shown to fail on a stub of the branch it covers. - Review steps appended by /trekexecute Phase 7.2 are held to the step rules on any plan version: plan-validator --steps-from <N> (on a plan below 1.8 a template review step validated and was executed). - The guard refuses where no one can be asked also when its rules will not load; the wave merge path names that refusal instead of MERGE_REFUSED. Tests for the classifier-error refusal and post-approval-log's own check. - release-check: the version files may change only in their version after the review (JSON compared without it), README diff lines are read inside hunks only, an unreadable diff is exit 2, and npm test runs without NODE_TEST_CONTEXT. - What counts as a step of only tests: a note in parentheses is not a path, and a data file under specs/ is not a test. End-to-end: rides on the last step that builds behaviour (validator hint, trekplan, template). - A removed line is cited as <rev>:<path>:<line> (findings-schema hint, trekreview re-ask). - skill-lint counts a trailing newline as no line; regression-check takes commandEnv from lib/util/command-env.mjs; CLAUDE.md names four mode-read exemptions. - /security-review, older than v6.2.0: the criteria runner refuses git --output, which writes a file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f669b8775f
commit
bef9c5b3ac
18 changed files with 450 additions and 43 deletions
|
|
@ -19,7 +19,8 @@
|
|||
// stops (1), undecided stops (2)
|
||||
// coverage the review is of THIS history: its `scope_sha_end` is HEAD or an
|
||||
// ancestor of HEAD, and the commits after it touch only the release
|
||||
// bookkeeping (version files, CHANGELOG, the README version badge)
|
||||
// bookkeeping (the CHANGELOG, the version in the version files,
|
||||
// the README version badge)
|
||||
// door the CHANGELOG section for the version has a `### Doors` list with
|
||||
// one `- one-way: …` or `- two-way: …` line per delivery
|
||||
//
|
||||
|
|
@ -37,6 +38,7 @@ import { spawnSync } from 'node:child_process';
|
|||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { decideVerdict } from '../lib/review/verdict-gate.mjs';
|
||||
import { commandEnv } from '../lib/util/command-env.mjs';
|
||||
import { parseDocument } from '../lib/util/frontmatter.mjs';
|
||||
|
||||
const PLUGIN_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
|
@ -44,12 +46,27 @@ const PLUGIN_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|||
export const EXIT = Object.freeze({ READY: 0, NOT_READY: 1, UNDECIDED: 2 });
|
||||
export const CHECK_IDS = Object.freeze(['tree', 'suite', 'review', 'coverage', 'door']);
|
||||
|
||||
// Files a release commit may change after the reviewed commit. README.md is
|
||||
// allowed only for lines that carry the version badge.
|
||||
// Files a release commit may change after the reviewed commit, and how much of
|
||||
// each (release review of 6.3.0, 2026-10-10: a changed test script or hook
|
||||
// wiring passed as bookkeeping): the CHANGELOG freely; the version files only
|
||||
// in their version (the JSON with it taken out must be equal); README.md only
|
||||
// on the lines that carry the version badge.
|
||||
export const RELEASE_BOOKKEEPING = Object.freeze([
|
||||
'CHANGELOG.md', 'package.json', 'package-lock.json', '.claude-plugin/plugin.json', 'README.md',
|
||||
]);
|
||||
const BADGE_RE = /badge\/version-/;
|
||||
const VERSION_FILES = Object.freeze(['package.json', 'package-lock.json', '.claude-plugin/plugin.json']);
|
||||
|
||||
// The JSON of a version file with its version taken out: the top-level
|
||||
// `version`, and in a lockfile the root package's `packages[""].version`.
|
||||
export function withoutVersion(text) {
|
||||
const o = JSON.parse(text);
|
||||
if (o && typeof o === 'object') {
|
||||
delete o.version;
|
||||
if (o.packages && o.packages[''] && typeof o.packages[''] === 'object') delete o.packages[''].version;
|
||||
}
|
||||
return JSON.stringify(o);
|
||||
}
|
||||
const DOOR_LINE_RE = /^- (one-way|two-way): \S/;
|
||||
|
||||
const git = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
|
||||
|
|
@ -100,6 +117,19 @@ export function parseDoors(section) {
|
|||
return { found: true, entries, bad };
|
||||
}
|
||||
|
||||
// The +/- lines of a unified diff, read inside its hunks only: a content line
|
||||
// that starts with "++ " or "-- " looks like a file header but is not one.
|
||||
export function changedLines(diff) {
|
||||
const out = [];
|
||||
let inHunk = false;
|
||||
for (const l of diff.split('\n')) {
|
||||
if (l.startsWith('diff --git ')) inHunk = false;
|
||||
else if (l.startsWith('@@')) inHunk = true;
|
||||
else if (inHunk && /^[+-]/.test(l)) out.push(l);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function checkTree(repo) {
|
||||
const r = git(repo, ['status', '--porcelain']);
|
||||
if (r.status !== 0) return { id: 'tree', status: 'error', detail: `git status failed: ${r.stderr.trim()}` };
|
||||
|
|
@ -112,7 +142,8 @@ function checkTree(repo) {
|
|||
|
||||
function checkSuite(repo, treeClean) {
|
||||
if (!treeClean) return { id: 'suite', status: 'fail', detail: 'suite not run: the tree has uncommitted changes, so it would not measure HEAD' };
|
||||
const r = spawnSync('npm', ['test'], { cwd: repo, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
||||
// commandEnv: under a parent `node --test` the suite would report to it and exit 0 whatever it found.
|
||||
const r = spawnSync('npm', ['test'], { cwd: repo, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, env: commandEnv() });
|
||||
if (r.error) return { id: 'suite', status: 'error', detail: `npm test could not start: ${r.error.message} — not measured` };
|
||||
const out = `${r.stdout ?? ''}\n${r.stderr ?? ''}`;
|
||||
const summary = out.split('\n').filter((l) => /^ℹ (tests|pass|fail|skipped) /.test(l)).map((l) => l.slice(2));
|
||||
|
|
@ -150,10 +181,20 @@ function checkCoverage(repo, reviewText) {
|
|||
if (names.status !== 0) return { id: 'coverage', status: 'error', detail: `git diff failed: ${names.stderr.trim()}` };
|
||||
const changed = names.stdout.split('\n').filter(Boolean);
|
||||
const outside = changed.filter((f) => !RELEASE_BOOKKEEPING.includes(f));
|
||||
for (const file of changed.filter((f) => VERSION_FILES.includes(f))) {
|
||||
const before = git(repo, ['show', `${full}:${file}`]);
|
||||
const after = git(repo, ['show', `HEAD:${file}`]);
|
||||
if (before.status !== 0 || after.status !== 0) { outside.push(`${file} (added or removed after the review)`); continue; }
|
||||
let same;
|
||||
try { same = withoutVersion(before.stdout) === withoutVersion(after.stdout); } catch (e) {
|
||||
return { id: 'coverage', status: 'error', detail: `${file} is not JSON (${e.message}) — not measured` };
|
||||
}
|
||||
if (!same) outside.push(`${file} (lines other than the version)`);
|
||||
}
|
||||
if (changed.includes('README.md')) {
|
||||
const d = git(repo, ['diff', '-U0', full, 'HEAD', '--', 'README.md']);
|
||||
const moved = d.stdout.split('\n').filter((l) => /^[+-]/.test(l) && !/^(\+\+\+|---)( |$)/.test(l));
|
||||
if (moved.some((l) => !BADGE_RE.test(l))) outside.push('README.md (lines other than the version badge)');
|
||||
if (d.status !== 0) return { id: 'coverage', status: 'error', detail: `git diff of README.md failed: ${d.stderr.trim()} — not measured` };
|
||||
if (changedLines(d.stdout).some((l) => !BADGE_RE.test(l))) outside.push('README.md (lines other than the version badge)');
|
||||
}
|
||||
if (outside.length > 0) {
|
||||
return { id: 'coverage', status: 'fail', detail: `changed after the reviewed commit ${full.slice(0, 12)} and not covered by the review: ${outside.join(', ')}`, items: outside };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue