fix(review): round 10 of the release review of 6.3.0, for 6.3.1
Round 10 (0f400d3..a1e0e50, WARN: 0 BLOCKER, 11 MAJOR): six findings, by operator choice. Each began with a test that failed ona1e0e50on its assertion, every case on its own; suite 2236 (2234/0/2). - Files: a new line inside a description opens a new entry when it opens with a plain file with a slash, or with one alone on the line or followed by a colon or a dash ("src/cli.mjs", "package.json", "Makefile - a target"); "child_process.spawnSync, path.join", "Node.js 22 only" and "index.js and the CLI" stay description (ecd9da6f). - Files: a numbered list item (`1.` or `1)`) is an entry, its marker dropped like `-` (b4215783). - Files: a backticked capitalised name is a file only alone in its entry before any description; `Parser` inside one is not (39a57fee). - Files: a backticked span that is no path (`x()`:) starts no description with its colon (882cc82a); a plain file with a colon starts it wherever it stands ("the entry cli.mjs: wires parse", a6bd9c8c). - 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 froma1e0e50, tests-only changed in 0. - The trailing-json fence reader is built on plan-schema's fenceTracker: a ~~~ fence is a fence, so a ```json line inside one no longer swallows the review's own block or hides its BLOCKER ids (0d94c5fe). This also closes a gate hole in 6.3.0 anda1e0e50: a last block written as ~~~json holding a BLOCKER, after a clean ```json block, passed with exit 0; now 2. - Kept on purpose: a fence line indented four spaces or more still counts in that reader (fenceTracker's new anyIndent). Markdown's three-space rule made a BLOCKER block inside a list item, four spaces in, invisible, and the gate passed it with exit 0 (seen while fixing, before commit); a ```json line in a code block indented four spaces fails the gate closed. Both are pinned in the 0d94c5fe test. - The fieldPaths doc names rounds 1-10 (it said 1-7). Not in this round (operator 2026-10-11): F8 754723c1 (an info string " json" fails closed, as in 6.3.0), F9 88de9e6b, F10 164c5bd0, F11 e70b28b7 (dc3a467e). F1 878e3960: this title carries a scope; the three earlier commits are left as they are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a1e0e50451
commit
10ac9c5231
3 changed files with 169 additions and 53 deletions
|
|
@ -196,12 +196,15 @@ export function ceremonyFromBrief(briefText) {
|
|||
* Tracks fenced code blocks the CommonMark way: a fence closes only on the same
|
||||
* character (` or ~), at least as long as the opener, with nothing after it.
|
||||
* `feed(line)` returns true when the line is inside a fence or is a fence line.
|
||||
* A fence line is indented up to three spaces; with `anyIndent` any indent
|
||||
* counts (the trailing-json readers, findings-schema `fencedBlocks`).
|
||||
*/
|
||||
export function fenceTracker() {
|
||||
export function fenceTracker({ anyIndent = false } = {}) {
|
||||
const fenceLine = anyIndent ? /^[ \t]*(`{3,}|~{3,})(.*)$/ : /^\s{0,3}(`{3,}|~{3,})(.*)$/;
|
||||
let open = null; // { ch, len }
|
||||
return {
|
||||
feed(line) {
|
||||
const m = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
||||
const m = line.match(fenceLine);
|
||||
if (!open) {
|
||||
if (m && !(m[1][0] === '`' && m[2].includes('`'))) {
|
||||
open = { ch: m[1][0], len: m[1].length };
|
||||
|
|
@ -291,29 +294,36 @@ export function onFailureAction(text) {
|
|||
* path. Measured on the plan.md files outside worktrees under the marketplace
|
||||
* and the test repo: 520 Files: fields in 48 plan files at rounds 7-8 (round 7
|
||||
* wrote "182 plans" here; round 8's count of the files is 48), and 560 fields
|
||||
* in 48 plan files at round 9 (2026-10-11), 548 of them with a backticked
|
||||
* path. In 3 of those a plain path-like word stands beside it, each inside a
|
||||
* description, and none of them is read as a file:
|
||||
* in 48 plan files at round 9 (2026-10-11; the same at round 10), 548 of
|
||||
* them with a backticked path. In 3 of those a plain path-like word stands
|
||||
* beside it, each inside a description, and none of them is read as a file:
|
||||
*
|
||||
* - A field with a backticked path names its backticked spans that pass the
|
||||
* path test (a `.`, `/` or `\`; no URL, option or call like `parse()`),
|
||||
* wherever they sit (`a.mjs`/`b.mjs`, inside a note), a backticked build
|
||||
* file (`Makefile`, `Dockerfile`), and a backticked capitalised name with no
|
||||
* extension that stands alone in its entry. Beside them a plain word counts
|
||||
* only when it is clearly a file or a directory, and only before the entry's
|
||||
* description starts (a dash, or a colon after a path, backticked or plain:
|
||||
* "src/cli.mjs: wires parse"): a path with a slash that ends in a file or a
|
||||
* extension that stands alone in its entry before any description. Beside
|
||||
* them a plain word counts only when it is clearly a file or a directory,
|
||||
* and only before the entry's description starts (a dash, or a colon after
|
||||
* a path, backticked or plain, wherever it stands: "src/cli.mjs: wires
|
||||
* parse", "the entry cli.mjs: wires parse"; a colon after a span that is no
|
||||
* path, `x()`:, starts none): a path with a slash that ends in a file or a
|
||||
* `/` anywhere there ("the CLI entry src/cli.mjs"), and a name with an
|
||||
* extension, a dotfile or a build file only where an entry or a list starts:
|
||||
* first in its entry, or after "and", "or", "+" or "&" (", Dockerfile", "and
|
||||
* Makefile", ", package.json"). 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. Round 7: such files were dropped, and a step that builds
|
||||
* something read as tests-only. Rounds 8-9: "— mocks fs.readFileSync", "runs
|
||||
* on Node.js 22.x", "no change to package.json", "— mocks fs,
|
||||
* child_process.spawnSync" or "src/cli.mjs: wires parse and README.md" read
|
||||
* as files, and a step that holds only its test did not. "covers
|
||||
* pass/fail", "JSON, YAML" or "v6.3.0 format" is no path.
|
||||
* the next line until a new entry starts: a list item on a new line (`-`,
|
||||
* `1.` or `1)`), a backticked path, or a new line that opens with a plain
|
||||
* file with a slash, or with one alone on the line or followed by a colon or
|
||||
* a dash ("src/cli.mjs: the fix", "package.json", "Makefile — a target").
|
||||
* Round 7: such files were dropped, and a step that builds something read as
|
||||
* tests-only. Rounds 8-9: "— mocks fs.readFileSync", "runs on Node.js 22.x",
|
||||
* "no change to package.json", "— mocks fs, child_process.spawnSync" or
|
||||
* "src/cli.mjs: wires parse and README.md" read as files, and a step that
|
||||
* holds only its test did not. Round 10: a plain file on the next line, a
|
||||
* numbered item, a `Parser` inside a description, `x()`: and a mid-entry
|
||||
* "cli.mjs:" were each misread. "covers pass/fail", "JSON, YAML", "v6.3.0
|
||||
* format" or a next line "index.js and the CLI" is no path.
|
||||
* - A field with no backticked path names every path-like word: a name with an
|
||||
* extension, a dotfile, a path with a slash, a directory (`docs/`), a glob
|
||||
* (`src/*.mjs`) or a build file; a parenthesised note is not read. Another
|
||||
|
|
@ -321,7 +331,7 @@ export function onFailureAction(text) {
|
|||
* capitalised; "format" or "v6.3.0" in a description is no path.
|
||||
*
|
||||
* A backslash is read as a slash, a leading `./` is dropped. Release review of
|
||||
* 6.3.0, rounds 1-7.
|
||||
* 6.3.0, rounds 1-10.
|
||||
*/
|
||||
const SEG = '[\\w@.*?()-]';
|
||||
const PATH_WORD = new RegExp(`^(?:${SEG}+\\/)+${SEG}*$|^[\\w@*?-][\\w@.*?-]*\\.[A-Za-z*][A-Za-z0-9*]*$|^\\.[\\w-][\\w.-]*$`);
|
||||
|
|
@ -337,7 +347,7 @@ const BUILD_FILE = /^(?!Profile$)(?:[A-Z][a-z]+|GNUmake)file$|^(?:make|rake|just
|
|||
// Where an entry's description starts, and the words that start a list item.
|
||||
const DESCRIPTION_DASH = /^(?:\u2014|\u2013|-|--)$/;
|
||||
const LIST_JOINER = /^(?:and|or|\+|&)$/i;
|
||||
const LIST_ITEM = /^\s*(?:[-*+]|\d+\.)\s/;
|
||||
const LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
|
||||
const lastSegment = (p) => p.replace(/\/$/, '').split('/').pop();
|
||||
const norm = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '');
|
||||
// A backticked span that is a path, not a URL, an option or a call (`parse()`, `p.parse()`).
|
||||
|
|
@ -347,6 +357,14 @@ const plainWord = (w) => w.replace(/^[(\[]+/, '').replace(/[.,;:)\]]+$/, '').rep
|
|||
const isPathWord = (w) => PATH_WORD.test(w) && !NOT_A_PATH.test(w);
|
||||
// Clearly a file or a directory: what a plain word beside backticked paths must be.
|
||||
const isFileWord = (w) => BUILD_FILE.test(w) || (isPathWord(w) && (FILE_OR_DIR.test(w) || BUILD_FILE.test(lastSegment(w))));
|
||||
// A line inside a description that opens a new entry: its first word is a
|
||||
// plain file with a slash, or a plain file alone on the line or followed by a
|
||||
// colon or a dash ("src/cli.mjs", "package.json", "Makefile — a target"), not
|
||||
// "child_process.spawnSync, path.join" or "Node.js 22 only" (round 10).
|
||||
const opensEntry = (tokens) => {
|
||||
const w = plainWord(tokens[0]);
|
||||
return isFileWord(w) && (w.includes('/') || tokens.length === 1 || tokens[0].endsWith(':') || DESCRIPTION_DASH.test(tokens[1]));
|
||||
};
|
||||
|
||||
export function fieldPaths(text) {
|
||||
const spans = [];
|
||||
|
|
@ -357,41 +375,44 @@ export function fieldPaths(text) {
|
|||
const tickedField = isTicked.some(Boolean);
|
||||
const out = [];
|
||||
// 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 (round 9).
|
||||
// entry starts: a list item on a new line (`-`, `1.`, `1)`), a backticked
|
||||
// path, or a line that opens with a plain file (rounds 9-10).
|
||||
let described = false;
|
||||
for (const line of masked.split('\n')) {
|
||||
if (LIST_ITEM.test(line)) described = false;
|
||||
for (const raw of line.split(',')) {
|
||||
const item = LIST_ITEM.exec(line);
|
||||
const body = item ? line.slice(item[0].length) : line;
|
||||
if (item || (described && body.trim() !== '' && opensEntry(body.trim().split(/\s+/)))) described = false;
|
||||
for (const raw of body.split(',')) {
|
||||
const entry = raw.trim().replace(/^[-*]\s+/, '');
|
||||
if (entry === '') continue;
|
||||
const tokens = entry.split(/\s+/);
|
||||
const first = /^\u0000(\d+)\u0000/.exec(entry);
|
||||
if (first && isTicked[Number(first[1])]) described = false;
|
||||
// A backticked capitalised name alone in its entry is a file (round 6).
|
||||
if (first && first[0] === entry && BARE_NAME.test(spans[Number(first[1])])) {
|
||||
// A backticked capitalised name alone in its entry, before any
|
||||
// description, is a file (rounds 6, 10).
|
||||
if (first && first[0] === entry && !described && BARE_NAME.test(spans[Number(first[1])])) {
|
||||
out.push(spans[Number(first[1])]);
|
||||
described = false;
|
||||
continue;
|
||||
}
|
||||
if (tickedField) {
|
||||
// In the order they stand: the backticked paths, and before the
|
||||
// description a plain word that is clearly a file where it may stand
|
||||
// (rounds 7-8). A path followed by a colon starts its description,
|
||||
// backticked or plain (round 9).
|
||||
// backticked or plain, wherever it stands; a span that is no path
|
||||
// does not (rounds 9-10).
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const t = tokens[i];
|
||||
if (t.includes('\u0000')) {
|
||||
out.push(...[...t.matchAll(/\u0000(\d+)\u0000/g)].map((m) => Number(m[1])).filter((n) => isTicked[n]).map((n) => spans[n]));
|
||||
if (t.endsWith(':')) described = true;
|
||||
const ticked = [...t.matchAll(/\u0000(\d+)\u0000/g)].map((m) => Number(m[1])).filter((n) => isTicked[n]);
|
||||
out.push(...ticked.map((n) => spans[n]));
|
||||
if (ticked.length > 0 && t.endsWith(':')) described = true;
|
||||
continue;
|
||||
}
|
||||
if (DESCRIPTION_DASH.test(t)) described = true;
|
||||
const w = plainWord(t);
|
||||
if (described || !isFileWord(w)) continue;
|
||||
if (w.includes('/') || i === 0 || LIST_JOINER.test(tokens[i - 1])) {
|
||||
out.push(w);
|
||||
if (t.endsWith(':')) described = true;
|
||||
}
|
||||
if (w.includes('/') || i === 0 || LIST_JOINER.test(tokens[i - 1])) out.push(w);
|
||||
if (t.endsWith(':')) described = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { readFileSync, existsSync, statSync } from 'node:fs';
|
|||
import { resolve, sep } from 'node:path';
|
||||
import { issue, fail } from '../util/result.mjs';
|
||||
import { RULE_KEYS, SEVERITY_VALUES } from './rule-catalogue.mjs';
|
||||
import { fenceTracker } from '../parsers/plan-schema.mjs';
|
||||
import { isMain } from '../util/is-main.mjs';
|
||||
|
||||
// The fields main + the coordinator depend on. Descriptive fields are not here
|
||||
|
|
@ -37,21 +38,26 @@ export const FINDING_REQUIRED_FIELDS = Object.freeze([
|
|||
]);
|
||||
|
||||
// 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.
|
||||
// fence (plan-schema `fenceTracker`, the one fence rule): an opening line of
|
||||
// three or more backticks or tildes, closed only by a line of the same
|
||||
// character 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 backtick
|
||||
// opener's info string holds no backtick. Inside a ~~~ block a ```json line
|
||||
// opens nothing (round 10: it swallowed the real block, and the review
|
||||
// validator lost every BLOCKER id). A block never closed runs to the end of the
|
||||
// text (`closed: false`). Indented or not, a fence counts: a fence inside a
|
||||
// list item can sit four spaces in, and a gate that skipped it would pass a
|
||||
// BLOCKER written there; the cost is that a ```json line in a code block
|
||||
// indented four spaces fails the gate closed (exit 2), never open. 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-10).
|
||||
// `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]*$/;
|
||||
const FENCE_INFO = /^[ \t]*(?:`{3,}|~{3,})(.*)$/;
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
|
|
@ -60,20 +66,19 @@ const FENCE_CLOSE = /^[ \t]*(`{3,})[ \t]*$/;
|
|||
export function fencedBlocks(text) {
|
||||
const blocks = [];
|
||||
if (typeof text !== 'string') return blocks;
|
||||
const fence = fenceTracker({ anyIndent: true });
|
||||
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;
|
||||
}
|
||||
const wasInside = fence.inside;
|
||||
fence.feed(line);
|
||||
if (!wasInside && fence.inside) {
|
||||
open = { info: FENCE_INFO.exec(line)[1].replace(/[ \t]+$/, ''), start: end + 1 };
|
||||
} else if (wasInside && !fence.inside) {
|
||||
blocks.push({ info: open.info, body: text.slice(open.start, at), closed: true });
|
||||
open = null;
|
||||
}
|
||||
if (nl === -1) break;
|
||||
at = nl + 1;
|
||||
|
|
|
|||
|
|
@ -1193,3 +1193,93 @@ test('review 630 r9 / brief note 8: the Files: reader states one count of plan f
|
|||
assert.match(text, /560 fields in 48 plan files at round 9/);
|
||||
assert.doesNotMatch(text.replace(/round 7 wrote "182 plans" here/, ''), /182/, 'the round-7 count is named only as the one replaced');
|
||||
});
|
||||
|
||||
// ==== round 10 of the release review of 6.3.0 (0f400d3..a1e0e50, 2026-10-11), fixed in 6.3.1 ====
|
||||
|
||||
test('review 630 r10 / ecd9da6f plan-schema.mjs:363: a new line that opens with a plain file starts a new entry', () => {
|
||||
const step = (files) => `- **Files:** ${files}\n- **Changes:** x\n`;
|
||||
for (const [files, paths] of [
|
||||
['`tests/a.test.mjs`: the test\n src/cli.mjs', ['tests/a.test.mjs', 'src/cli.mjs']],
|
||||
['`tests/a.test.mjs` — the test\n src/cli.mjs: the fix', ['tests/a.test.mjs', 'src/cli.mjs']],
|
||||
['`tests/a.test.mjs` — the test\n src/a.mjs, src/b.mjs', ['tests/a.test.mjs', 'src/a.mjs', 'src/b.mjs']],
|
||||
['`tests/a.test.mjs` — the test\n package.json', ['tests/a.test.mjs', 'package.json']],
|
||||
['`tests/a.test.mjs` — the test\n Makefile — a target', ['tests/a.test.mjs', 'Makefile']],
|
||||
]) {
|
||||
assert.deepEqual(stepTestsOnly(step(files)), { testsOnly: false, files: paths }, files);
|
||||
}
|
||||
// The control: a description that runs onto the next line names no file
|
||||
// there (round 9), unless the line opens with one in an entry's own shape.
|
||||
for (const files of ['`tests/a.test.mjs` — mocks fs and\n child_process.spawnSync, path.join',
|
||||
'`tests/a.test.mjs` — runs on\n Node.js 22 only', '`tests/a.test.mjs` — covers\n index.js and the CLI']) {
|
||||
assert.deepEqual(stepTestsOnly(step(files)), { testsOnly: true, files: ['tests/a.test.mjs'] }, files);
|
||||
}
|
||||
});
|
||||
|
||||
test('review 630 r10 / b4215783 plan-schema.mjs:365: a numbered list item is an entry, `1.` or `1)`', () => {
|
||||
const step = (files) => `- **Files:** ${files}\n- **Changes:** x\n`;
|
||||
for (const [files, paths] of [
|
||||
['`tests/a.test.mjs`\n 1. package.json\n 2. src/b.mjs', ['tests/a.test.mjs', 'package.json', 'src/b.mjs']],
|
||||
['`tests/a.test.mjs`\n 1) package.json', ['tests/a.test.mjs', 'package.json']],
|
||||
['`tests/a.test.mjs` — the test, mocks fs\n 2) Makefile', ['tests/a.test.mjs', 'Makefile']],
|
||||
]) {
|
||||
assert.deepEqual(stepTestsOnly(step(files)), { testsOnly: false, files: paths }, files);
|
||||
}
|
||||
});
|
||||
|
||||
test('review 630 r10 / 39a57fee plan-schema.mjs:371: a backticked capitalised name inside a description is no file', () => {
|
||||
const step = (files) => `- **Files:** ${files}\n- **Changes:** x\n`;
|
||||
for (const files of ['`tests/a.test.mjs` — covers `parse`, `Parser`, `ParseError`',
|
||||
'`tests/a.test.mjs` — mocks fs, `Date`, and child_process.spawnSync']) {
|
||||
assert.deepEqual(stepTestsOnly(step(files)), { testsOnly: true, files: ['tests/a.test.mjs'] }, files);
|
||||
}
|
||||
// The control: alone in its own entry, before any description, it is a file (round 6).
|
||||
assert.deepEqual(stepTestsOnly(step('`tests/a.test.mjs`, `Parser`')), { testsOnly: false, files: ['tests/a.test.mjs', 'Parser'] });
|
||||
});
|
||||
|
||||
test('review 630 r10 / 882cc82a plan-schema.mjs:385: a backticked span that is no path starts no description with a colon', () => {
|
||||
const step = (files) => `- **Files:** ${files}\n- **Changes:** x\n`;
|
||||
for (const [files, paths] of [
|
||||
['`tests/a.test.mjs` adds `x()`: y, package.json', ['tests/a.test.mjs', 'package.json']],
|
||||
['`tests/a.test.mjs` adds `parse()`: y and Makefile', ['tests/a.test.mjs', 'Makefile']],
|
||||
]) {
|
||||
assert.deepEqual(stepTestsOnly(step(files)), { testsOnly: false, files: paths }, files);
|
||||
}
|
||||
// The control: a backticked path with a colon still starts its description.
|
||||
assert.deepEqual(stepTestsOnly(step('`tests/a.test.mjs`: y and package.json')), { testsOnly: true, files: ['tests/a.test.mjs'] });
|
||||
});
|
||||
|
||||
test('review 630 r10 / a6bd9c8c plan-schema.mjs:391: a plain file with a colon starts its description wherever it stands', () => {
|
||||
const step = (files) => `- **Files:** ${files}\n- **Changes:** x\n`;
|
||||
for (const files of ['`tests/a.test.mjs`, the entry cli.mjs: wires parse and README.md',
|
||||
'`tests/a.test.mjs`, exercises cli.mjs: argv and package.json',
|
||||
'`tests/a.test.mjs`, then index.js: reads argv and .env']) {
|
||||
assert.deepEqual(stepTestsOnly(step(files)), { testsOnly: true, files: ['tests/a.test.mjs'] }, files);
|
||||
}
|
||||
});
|
||||
|
||||
test('review 630 r10 / 0d94c5fe findings-schema.mjs:53: a ~~~ fence is a fence; a fence four spaces in still counts, so the gate fails closed', async () => {
|
||||
const { extractFindingsBlock, fencedBlocks } = await import('../../lib/review/findings-schema.mjs');
|
||||
const { decideVerdict } = await import('../../lib/review/verdict-gate.mjs');
|
||||
const { validateReviewContent } = await import('../../lib/validators/review-validator.mjs');
|
||||
const asBlock = (text) => text.replace('verdict: WARN\nfindings: []', `verdict: BLOCK\nfindings:\n - ${'a'.repeat(40)}`);
|
||||
// A ~~~ example that holds a ```json line, then the review's own block: the
|
||||
// ```json line opens nothing, so the block, the gate and the BLOCKER ids are read.
|
||||
const tilde = `~~~\n${B3}json\n~~~\n\n`;
|
||||
assert.equal(extractFindingsBlock(`${tilde}${B3}json\n{"findings": []}\n${B3}\n`), '{"findings": []}\n');
|
||||
const v = decideVerdict(r9Review(`${tilde}${r9Clean}`));
|
||||
assert.equal(v.exit, 0, v.reason);
|
||||
const r = validateReviewContent(asBlock(r9Review(`${tilde}${B3}json\n${R9_BLOCK}\n${B3}\n`)));
|
||||
assert.ok([...r.errors, ...r.warnings].some((i) => i.code === 'REVIEW_MISSING_REMEDIATION_STEP'),
|
||||
`a BLOCKER with no ### Step R<n> is found: ${JSON.stringify([...r.errors, ...r.warnings].map((i) => i.code))}`);
|
||||
// A ~~~ block is one block, closed only by ~~~; a ~~~json last block is read.
|
||||
assert.deepEqual(fencedBlocks(`~~~md\n${B3}json\n{}\n~~~\n`).map((b) => [b.info, b.body, b.closed]), [['md', `${B3}json\n{}\n`, true]]);
|
||||
const tildeJson = decideVerdict(r9Review(`${r9Clean}\n~~~json\n${R9_BLOCK}\n~~~\n`));
|
||||
assert.notEqual(tildeJson.exit, 0, `a ~~~json block after the clean one is the last block: ${tildeJson.reason}`);
|
||||
// Indented or not, a fence counts. A BLOCKER block inside a list item, four
|
||||
// spaces in, after the clean block is the last block and is never passed...
|
||||
const listItem = `${r9Clean}\n1. The payload:\n\n${`${B3}json\n${R9_BLOCK}\n${B3}`.replace(/^/gm, ' ')}\n`;
|
||||
assert.notEqual(decideVerdict(r9Review(listItem)).exit, 0, decideVerdict(r9Review(listItem)).reason);
|
||||
// ...and a ```json line in a code block four spaces in fails the gate closed, never open.
|
||||
const coded = decideVerdict(r9Review(`x\n\n ${B3}json\n\nx\n\n${r9Clean}`));
|
||||
assert.equal(coded.exit, 2, coded.reason);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue