fix: round 9 of the release review of 6.3.0, for 6.3.1

Round 9 (0f400d3..a10018f, WARN: 0 BLOCKER, 12 MAJOR): seven findings and
three brief notes, by operator choice. Each finding began with a test that
failed on a10018f on its assertion; suite 2230 (2228/0/2).

- A check refused for its comment (a quote, $, backtick or backslash) keeps
  its command and its reason, and every reader refuses it out loud: a nested
  ## Verification bullet stays a criterion (PLAN_VERIFICATION_NOT_RUNNABLE)
  and a brief SC warns (BRIEF_SC_NOT_RUNNABLE). It was folded into a
  sub-point or read as prose, a regression from a10018f. Readers that took a
  command as runnable (the end-to-end lists) skip a refused one.
- The verdict gate reads the last fenced block's info string and body from
  one reading, and exits 2 unless the info string is exactly json: a last
  block opened as ```json {x} held a BLOCKER the gate passed with exit 0
  (also in 6.3.0). A last block never closed is exit 2 too.
- One fence reader (findings-schema fencedBlocks) for the gate and every
  trailing-json reader: a fence closes only on a run at least as long as its
  opener, as in Markdown, so a four-backtick example of a json block is one
  block; CRLF and an indented fence read alike in the gate and the readers.
- Files: a description runs on past a comma and onto the next line until a
  new entry starts (a list item on a new line, or a backticked path), and a
  plain path followed by a colon starts its description. Measured over all
  939 Files: fields in the .md files under the marketplace and the test repo
  (560 of them in the 48 plan.md files outside worktrees): 0 read
  differently, tests-only changed in 0.
- The test lock compares --cmd by its words, so other blanks between the
  same words are the step's command.
- release-check decides "two files" on real paths: one review.md reached
  through a symlinked --repo is one file.
- Brief notes: red proof's stopped control run has its own test (green at
  once, red with the branch deleted); the two tests that assumed jest is not
  installed run with a PATH that finds only git (red on a10018f with a jest
  on PATH, green now); the Files: reader's doc states the measured counts
  (520 fields in 48 plan files at rounds 7-8, not "182 plans"; 560 now).

Not in this round (operator 2026-10-11): a144e9ce, 1dc2e785, dc3a467e,
2fa88853, 26873fa5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-11 12:49:01 +02:00
commit a1e0e50451
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
11 changed files with 416 additions and 104 deletions

View file

@ -28,7 +28,7 @@
import { readFileSync, existsSync } from 'node:fs';
import { validateReviewContent, VERDICT_VALUES } from '../validators/review-validator.mjs';
import { extractFindingsBlock } from './findings-schema.mjs';
import { fencedBlocks, isJsonBlock } from './findings-schema.mjs';
import { isMain } from '../util/is-main.mjs';
export const EXIT = Object.freeze({ PASS: 0, BLOCK: 1, UNDECIDED: 2 });
@ -67,17 +67,23 @@ export function decideVerdict(text) {
if ((fmText.match(/^verdict\s*:/gm) ?? []).length > 1) {
return undecided('frontmatter carries more than one verdict line', verdict);
}
const lastFence = lastFenceLanguage(body);
if (lastFence === null) {
// The last fenced block, its info string and its body from one reading
// (findings-schema `fencedBlocks`): a gate that took the language from one
// reader and the body from another passed a BLOCKER in a last block opened
// as ```json {x} (release review of 6.3.0, round 9).
const last = fencedBlocks(body).at(-1);
if (last === undefined) {
return undecided(`verdict ${verdict} has no trailing JSON block to back it`, verdict);
}
if (lastFence !== 'json') {
return undecided(`the last fenced block is "${lastFence || '(no language)'}", not the trailing \`json\` block`, verdict);
if (!last.closed) {
return undecided(`the last fenced block ("${last.info || '(no language)'}") is never closed, so it runs to the end of the file`, verdict);
}
if (!isJsonBlock(last)) {
return undecided(`the last fenced block is "${last.info || '(no language)'}", not the trailing \`json\` block`, verdict);
}
const block = extractFindingsBlock(body);
let payload;
try {
payload = JSON.parse(block);
payload = JSON.parse(last.body);
} catch (e) {
return undecided(`trailing JSON block does not parse: ${e.message}`, verdict);
}
@ -118,19 +124,6 @@ function atLeastVersion(v, major, minor) {
return Boolean(m) && (Number(m[1]) > major || (Number(m[1]) === major && Number(m[2]) >= minor));
}
// A fence as lib/review/findings-schema.mjs reads one: on a line of its own,
// indented or not, and a closing fence of three backticks or more (round 8).
const FENCE_RE = /^[ \t]*```[ \t]*([A-Za-z0-9_+-]*)[^\n]*\n[\s\S]*?^[ \t]*`{3,}[ \t]*$/gm;
/** Language tag of the LAST fenced block in `body` (lower-cased), '' for none, null when no fence. */
function lastFenceLanguage(body) {
let last = null;
let m;
FENCE_RE.lastIndex = 0;
while ((m = FENCE_RE.exec(body)) !== null) last = m[1].toLowerCase();
return last;
}
/** The JSON block shape the gate needs; returns an error string or null. */
function checkPayloadShape(p) {
if (p === null || typeof p !== 'object' || Array.isArray(p)) return 'not an object';