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 ona10018fon 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 froma10018f. 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 ona10018fwith 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:
parent
a10018f9d3
commit
a1e0e50451
11 changed files with 416 additions and 104 deletions
|
|
@ -36,33 +36,67 @@ export const FINDING_REQUIRED_FIELDS = Object.freeze([
|
|||
'line',
|
||||
]);
|
||||
|
||||
// Last fenced ```json … ``` block in a reviewer's output. The contract pins the
|
||||
// JSON block as the LAST fence so prose above it never confuses the parser.
|
||||
// Both fences stand on a line of their own, as in Markdown and in the verdict
|
||||
// gate's reader: a JSON string cannot hold a line break, so three backticks
|
||||
// inside one ("a fenced ```bash block") never end the block. Ending it at the
|
||||
// first triple backtick anywhere made fill-finding-ids exit 2 (release review
|
||||
// of 6.3.0, found while writing round 7's review.md; present in 6.2.0). As in
|
||||
// Markdown, the closing fence may be longer than three backticks (round 8);
|
||||
// three backticks after text on the same line are no fence, here or in the gate.
|
||||
// One reader for every trailing json block: the review validator and the
|
||||
// synthesis digest import it.
|
||||
const JSON_FENCE_GLOBAL = /^[ \t]*```json[ \t]*\r?\n([\s\S]*?)^[ \t]*`{3,}[ \t]*\r?$/gim;
|
||||
// The fenced blocks of a Markdown text, in order, read as Markdown reads a
|
||||
// backtick fence: an opening line of three or more backticks whose info string
|
||||
// holds no backtick, closed only by a line of backticks at least as long as the
|
||||
// opening run (round 9: any three closed any opener, so a four-backtick example
|
||||
// of a json block split there). A block never closed runs to the end of the
|
||||
// text (`closed: false`). Indented or not, a fence counts, also inside a list
|
||||
// item; a `\r` before a line break is no part of the line, so a CRLF file reads
|
||||
// as its LF twin. A JSON string cannot hold a line break, so three backticks
|
||||
// inside one ("a fenced ```bash block") never end a block, and three backticks
|
||||
// after text on the same line are no fence (release review of 6.3.0, rounds
|
||||
// 7-9). `info` is the info string as written, trailing blanks dropped; `body`
|
||||
// is the text between the fence lines, as written.
|
||||
// One reader for the verdict gate and every trailing-json reader: the review
|
||||
// validator, the synthesis digest, fill-finding-ids, recurrence, fidelity-diff.
|
||||
const FENCE_OPEN = /^[ \t]*(`{3,})([^`]*)$/;
|
||||
const FENCE_CLOSE = /^[ \t]*(`{3,})[ \t]*$/;
|
||||
|
||||
/**
|
||||
* Extract the inner body of the LAST fenced `json` block in `text`.
|
||||
* @param {string} text
|
||||
* @returns {Array<{info: string, body: string, closed: boolean}>}
|
||||
*/
|
||||
export function fencedBlocks(text) {
|
||||
const blocks = [];
|
||||
if (typeof text !== 'string') return blocks;
|
||||
let open = null;
|
||||
for (let at = 0; at <= text.length;) {
|
||||
const nl = text.indexOf('\n', at);
|
||||
const end = nl === -1 ? text.length : nl;
|
||||
const line = text.slice(at, end).replace(/\r$/, '');
|
||||
if (open === null) {
|
||||
const m = FENCE_OPEN.exec(line);
|
||||
if (m) open = { run: m[1].length, info: m[2].replace(/[ \t]+$/, ''), start: end + 1 };
|
||||
} else {
|
||||
const m = FENCE_CLOSE.exec(line);
|
||||
if (m && m[1].length >= open.run) {
|
||||
blocks.push({ info: open.info, body: text.slice(open.start, at), closed: true });
|
||||
open = null;
|
||||
}
|
||||
}
|
||||
if (nl === -1) break;
|
||||
at = nl + 1;
|
||||
}
|
||||
if (open !== null) blocks.push({ info: open.info, body: text.slice(open.start), closed: false });
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/** A `json` block: its info string is exactly `json` (any case). */
|
||||
export function isJsonBlock(block) {
|
||||
return block.info.toLowerCase() === 'json';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the inner body of the LAST closed `json` block in `text`. The
|
||||
* contract pins the JSON block as the LAST fence so prose above it never
|
||||
* confuses the parser.
|
||||
* @param {string} text
|
||||
* @returns {string|null} the JSON source, or null if no json fence is present.
|
||||
*/
|
||||
export function extractFindingsBlock(text) {
|
||||
if (typeof text !== 'string') return null;
|
||||
JSON_FENCE_GLOBAL.lastIndex = 0;
|
||||
let last = null;
|
||||
let m;
|
||||
while ((m = JSON_FENCE_GLOBAL.exec(text)) !== null) {
|
||||
last = m[1];
|
||||
}
|
||||
return last;
|
||||
const json = fencedBlocks(text).filter((b) => b.closed && isJsonBlock(b));
|
||||
return json.length > 0 ? json[json.length - 1].body : null;
|
||||
}
|
||||
|
||||
function validateFinding(finding, index, errors) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue