fix: close what an independent review found in the trial 2 fixes
- The pre-compact hook read only a checkpoint that started with `git commit`,
so every plan in the new `git add … && git commit` form rebuilt nothing, and
an empty match also skipped the session-state refresh. It now finds the
commit anywhere in the span and always refreshes; first test for the hook.
- A sub-bullet that carries its own command stays a criterion (folding it
left the command unrun); the shallowest bullet sets the base indent.
- Only a MINOR/SUGGESTION /code-review finding outside the range is released
from forbidding ALLOW: a MAJOR one may be a break cited at an unchanged
caller.
- `reuse` is an improvement category; the session-spec template stages too;
a negated bash 3.2 constraint, an env prefix and `bash -c` are handled.
- Stale comment, Declined-to-Judge wording and rule-key counts brought in line.
Tests fail on 7c11c2a first (9 of 24).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7c11c2aa9f
commit
b0d96a2d53
13 changed files with 194 additions and 54 deletions
|
|
@ -388,14 +388,17 @@ export function runContract(reviewerPayloads, options = {}) {
|
|||
// Engine findings on files outside the review's SHA range (engine-findings.mjs
|
||||
// `out_of_scope`). /security-review diffs against origin's default branch,
|
||||
// wider than this review: a real claim nobody here judged, so it forbids
|
||||
// ALLOW. /code-review was given exactly this range, so its citation outside
|
||||
// it is about code the review did not change: declined, listed, never
|
||||
// dropped — but it does not lock the verdict on WARN (voyage-lekerepo
|
||||
// trial 2, 2026-10-08: one remark on a frozen file held ALLOW back for good).
|
||||
// ALLOW. /code-review was given exactly this range: its MINOR or SUGGESTION
|
||||
// finding outside it (an improvement to code the review did not change) is
|
||||
// declined and listed, never dropped, but does not hold ALLOW back
|
||||
// (voyage-lekerepo trial 2, 2026-10-08). A MAJOR+ one still forbids ALLOW:
|
||||
// a changed signature that breaks an unchanged caller is cited AT the
|
||||
// unchanged file (independent review, 2026-10-08).
|
||||
const outOfScope = (options.engineOutOfScope ?? [])
|
||||
.map((f) => ({ ...f, suppressed_reason: 'engine:out-of-scope' }));
|
||||
const outOfScopeBlocking = outOfScope.filter((f) => f.engine !== 'code-review');
|
||||
const outOfScopeNoted = outOfScope.filter((f) => f.engine === 'code-review');
|
||||
const minorOnly = (f) => f.engine === 'code-review' && (f.severity === 'MINOR' || f.severity === 'SUGGESTION');
|
||||
const outOfScopeBlocking = outOfScope.filter((f) => !minorOnly(f));
|
||||
const outOfScopeNoted = outOfScope.filter(minorOnly);
|
||||
const unverified = [...judged.unverified, ...reasoned.unverified, ...outOfScopeBlocking];
|
||||
// What the coordinator set aside without ruling on it, one line each, so a
|
||||
// reader sees every finding that did not reach a verdict tier. Borrowed:
|
||||
|
|
|
|||
|
|
@ -33,10 +33,11 @@
|
|||
// - File paths are made repo-relative (an absolute path under `repoRoot`
|
||||
// loses that prefix) before they are compared with the scope.
|
||||
// - A finding on a file outside the review scope is NOT dropped silently: it
|
||||
// lands in `out_of_scope`, which the coordinator counts as unverified
|
||||
// (`engine:out-of-scope`) — it forbids ALLOW and is listed under
|
||||
// Declined to Judge (`/security-review` diffs against origin's default
|
||||
// branch, which can be wider than the review's SHA range).
|
||||
// lands in `out_of_scope`, which the coordinator lists under Declined to
|
||||
// Judge (`engine:out-of-scope`). It forbids ALLOW unless it is a MINOR or
|
||||
// SUGGESTION from /code-review (coordinator-contract.mjs runContract);
|
||||
// `/security-review` diffs against origin's default branch, which can be
|
||||
// wider than the review's SHA range.
|
||||
// - An empty scope list is an error, never "everything is out of scope".
|
||||
//
|
||||
// CLI: node lib/review/engine-findings.mjs [--scope <file-list.txt>] [--repo-root <dir>] <engine-findings.json>
|
||||
|
|
@ -79,7 +80,7 @@ function parseLine(v) {
|
|||
const tier = (key) => SEVERITY_VALUES.indexOf(getRule(key).severity);
|
||||
|
||||
/** /code-review categories that name an improvement, not a defect. */
|
||||
export const IMPROVEMENT_CATEGORIES = Object.freeze(new Set(['simplification', 'efficiency', 'style', 'readability']));
|
||||
export const IMPROVEMENT_CATEGORIES = Object.freeze(new Set(['simplification', 'efficiency', 'reuse', 'style', 'readability']));
|
||||
|
||||
/**
|
||||
* Convert one engine's transcribed findings into a coordinator payload.
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
// Code's own `/code-review` and `/security-review` enter the coordinator under
|
||||
// them when no narrower key fits (wrap, not a new engine). A third engine key,
|
||||
// CODE_REVIEW_IMPROVEMENT (MINOR), takes a /code-review finding whose category
|
||||
// is an improvement rather than a defect (simplification, efficiency, style,
|
||||
// readability): the second voyage-lekerepo trial (2026-10-08) saw a pure
|
||||
// is an improvement rather than a defect (simplification, efficiency, reuse,
|
||||
// style, readability): the second voyage-lekerepo trial (2026-10-08) saw a pure
|
||||
// simplification proposal enter as MAJOR and drive the verdict.
|
||||
//
|
||||
// `memory_route` decides where a finding class that recurs across two
|
||||
|
|
@ -128,7 +128,7 @@ export const RULE_CATALOGUE = Object.freeze([
|
|||
rule_key: 'CODE_REVIEW_IMPROVEMENT',
|
||||
severity: 'MINOR',
|
||||
category: 'maintenance',
|
||||
description: "An improvement (simplification, efficiency, style, readability) reported by Claude Code's /code-review, not a defect.",
|
||||
description: "An improvement (simplification, efficiency, reuse, style, readability) reported by Claude Code's /code-review, not a defect.",
|
||||
memory_route: null,
|
||||
}),
|
||||
Object.freeze({
|
||||
|
|
|
|||
|
|
@ -274,7 +274,9 @@ export function validateBriefContent(text, opts = {}) {
|
|||
// on a Mac with a newer bash installed that is 5.x, so the constraint is
|
||||
// never measured (voyage-lekerepo trial 2, 2026-10-08).
|
||||
const constraints = (extractSection(body, 'Constraints') ?? '') + (extractSection(body, 'Non-Functional Requirements') ?? '');
|
||||
const needsBash32 = /\bbash\s*3\.2\b/i.test(constraints);
|
||||
// Line by line, and a line that negates it ("not required") does not count.
|
||||
const needsBash32 = constraints.split('\n').some((l) =>
|
||||
/\bbash\s*3\.2\b/i.test(l) && !/\b(?:not|no longer|never|no need)\b/i.test(l));
|
||||
for (const c of parseSuccessCriteria(body)) {
|
||||
if (c.subpoints && !c.command) {
|
||||
warnings.push(issue(
|
||||
|
|
@ -285,11 +287,15 @@ export function validateBriefContent(text, opts = {}) {
|
|||
));
|
||||
}
|
||||
if (!c.command) continue;
|
||||
if (needsBash32 && /^bash\s/.test(c.command)) {
|
||||
const bare = c.command.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)+/, '');
|
||||
if (needsBash32 && /^bash\s/.test(bare)) {
|
||||
const script = bare.split(/\s+/)[1] ?? '';
|
||||
warnings.push(issue(
|
||||
'BRIEF_SC_BASH_NOT_PINNED',
|
||||
`Success criterion ${c.label} runs \`${c.command}\`, but the brief requires bash 3.2: \`bash\` is whichever bash comes first on PATH, which may be newer`,
|
||||
`Write it as \`/bin/bash ${c.command.replace(/^bash\s+/, '')}\` (/bin/bash tests/<script>.sh) so the check runs under the system bash.`,
|
||||
script.startsWith('tests/')
|
||||
? `Write it as \`/bin/bash ${bare.replace(/^bash\s+/, '')}\` (/bin/bash tests/<script>.sh) so the check runs under the system bash.`
|
||||
: 'Put the check in a script under tests/ and run it as `/bin/bash tests/<script>.sh`, so it runs under the system bash.',
|
||||
));
|
||||
}
|
||||
const r = runnableCheck(c, { cwd: opts.cwd });
|
||||
|
|
|
|||
|
|
@ -164,11 +164,15 @@ function parseSection(markdown, heading, prefix) {
|
|||
const criteria = [];
|
||||
const push = (text, command, reason) =>
|
||||
criteria.push({ label: `${prefix}${criteria.length + 1}`, text, command, reason });
|
||||
// A bullet indented deeper than the section's first bullet is a sub-point of
|
||||
// the criterion above it, never a criterion of its own: one criterion with
|
||||
// ten sub-points became SC4..SC13, each `not-a-command` (voyage-lekerepo
|
||||
// trial 2, 2026-10-08). The parent counts them in `subpoints`.
|
||||
let baseIndent = null;
|
||||
// A bullet indented deeper than the section's shallowest bullet, carrying no
|
||||
// command of its own, is a sub-point of the criterion above it, never a
|
||||
// criterion of its own: one criterion with ten sub-points became SC4..SC13,
|
||||
// each `not-a-command` (voyage-lekerepo trial 2, 2026-10-08). The parent
|
||||
// counts them in `subpoints`. A sub-bullet that DOES carry a command stays a
|
||||
// criterion: folding it would leave its command unrun (independent review).
|
||||
const indentOf = (text) => text.match(/^\s*/)[0].replace(/\t/g, ' ').length;
|
||||
const bulletIndents = lines.filter((l) => !l.fenced && !l.marker && BULLET.test(l.text)).map((l) => indentOf(l.text));
|
||||
const baseIndent = bulletIndents.length ? Math.min(...bulletIndents) : 0;
|
||||
let lastBullet = null;
|
||||
for (const line of lines) {
|
||||
if (line.marker) continue;
|
||||
|
|
@ -181,17 +185,16 @@ function parseSection(markdown, heading, prefix) {
|
|||
}
|
||||
const bullet = line.text.match(BULLET);
|
||||
if (!bullet) continue;
|
||||
const indent = line.text.match(/^\s*/)[0].replace(/\t/g, ' ').length;
|
||||
if (baseIndent === null) baseIndent = indent;
|
||||
if (indent > baseIndent && lastBullet) {
|
||||
lastBullet.subpoints = (lastBullet.subpoints ?? 0) + 1;
|
||||
continue;
|
||||
}
|
||||
const nested = indentOf(line.text) > baseIndent;
|
||||
const text = bullet[1].trim();
|
||||
if (text === '') continue;
|
||||
const { command, reason } = firstCommand(text);
|
||||
if (nested && lastBullet && !command) {
|
||||
lastBullet.subpoints = (lastBullet.subpoints ?? 0) + 1;
|
||||
continue;
|
||||
}
|
||||
push(text, command, reason);
|
||||
lastBullet = criteria[criteria.length - 1];
|
||||
if (!nested) lastBullet = criteria[criteria.length - 1];
|
||||
}
|
||||
return criteria;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue