// docs-guard-exempt: 'docs/DEVELOPMENT.md' is a synthetic files-list fixture entry, not read as content. // allow-test-rule: source-text-is-the-product // The workflow and agent .md files ARE the product: their text is loaded and // executed/interpreted at runtime by the agent host. Testing that specific // strings exist within these files tests the deployed contract, not an // implementation detail. No runtime API exists to enumerate the label accept- // list or filter-set definitions — the text IS the specification. // // Bug 1 (compute_file_scope) — The inline Node.js script embedded in the // workflow .md is the parser. The test implements the identical parse logic as // a pure JS function (mirroring lines 172-184 of code-review.md exactly) and // asserts on its structured output. A separate docs-parity assertion checks // that the workflow .md contains the hyphen-aware boundary regex and the // em-dash/parenthetical stripping — both of which are the deployed contract. // // Bug 2 (present_results) — Tested both behaviourally (pure JS helper that // mimics the grep|cut pipeline) and via docs-parity on the workflow .md text. // // Bugs 3 and reviewer contract — docs-parity only on agents/*.md: the filter- // set definition and label-equivalence contract exist only as text in those // files; there is no runtime enumeration API. 'use strict'; const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const fc = require('fast-check'); const { runHook, runNode, OUTCOME } = require('./helpers/process-seam.cjs'); const { toLegacyResult, gitOrThrow } = require('./helpers/git-fixture.cjs'); const { PROBE_TIMEOUT_MS, GIT_TIMEOUT_MS, HOOK_FANOUT_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const { createTempDir, createTempGitProject, cleanup, readFileNormalized } = require('./helpers.cjs'); const { foldShellContinuations, findShellFencedMatches, } = require('./helpers/shell-doc-scan.cjs'); const os = require('node:os'); /** * A single invocation of the external `fallow` binary's `audit` * subcommand against the REAL current repo tree (CI-only) -- a heavier * third-party tool operation distinct from any existing shared constant. */ const FALLOW_AUDIT_TIMEOUT_MS = 120000; const ROOT = path.resolve(__dirname, '..'); // HOISTED. `const` is in the temporal dead zone until its declaration executes, and a // `{ skip: !HAS_BASH }` option object is evaluated EAGERLY when its describe body runs -- // so a bash-gated test added ABOVE the old mid-file declaration threw a ReferenceError // that aborted the whole describe and CANCELLED its siblings. It caught three separate // additions in this round alone, and the cancellation reads as a passing run in the // summary line. Declared with the other file-level constants so placement stops mattering. // AND NOT A PROBE — but the honest reason is narrower than the first draft of this comment claimed, // and the correction is worth keeping. Round 5 flagged that every test exercising the shipped bash // fences is `{ skip: !HAS_BASH }`, so block 1's severity-reporting path has no Windows-lane coverage. // The gap is real and the count is 37 — 22 was the number of `{ skip: !HAS_BASH }` CALL SITES, and a // skip on a `describe` cancels its subtests. // // This comment first justified the skip by citing `local/no-unguarded-nonportable-exec` as REQUIRING // this exact guard. That was checked and is wrong on both halves: the rule only fires on a file that // also chmods an exec bit with an octal literal (this file has none, so it never fires here), and // `eslint-rules/lib/platform-guard.cjs` accepts four guard shapes plus `os.platform()`, not one. A // constraint that exists is not a constraint that applies. // // What holds — and this is now MEASURED, not assumed. The measurement the previous version of this // comment deferred has been made, on native Windows (not WSL) with Git Bash 5.2.37 / MINGW64 first on // PATH, node v25.2.1 — the same shell family the repo's `windows-latest` lane runs: // // HAS_BASH left alone: 179 tests, 127 pass, 0 fail, 52 skipped // HAS_BASH forced true: 179 tests, 140 pass, 24 fail, 15 skipped // // So 37 of the skips are this guard's (52 - 15; the other 15 skip for unrelated reasons), and // unskipping them does NOT reveal a clean win: 13 pass and 24 fail. The failures cluster on exactly // the divergence the eslint rule's subject line names — `bash -c` quoting (one surfaces as // `unexpected EOF while looking for matching '"'`), empty captured output, and two outright // `spawn_failed`. Flipping this constant to a runtime probe today would red the Windows lane with 24 // failures, so the guard STAYS; what changes is that it now documents a measured gap instead of an // assumed one. Closing it means porting the fences themselves, which is a change of its own, not a // line in a review round. // // NOTE for anyone re-running this: on a WSL host `bash` on the Windows PATH resolves to // C:\Windows\system32\bash.exe, the WSL bridge — measuring through that runs real Linux bash and // reports a false clean. Put `C:\Program Files\Git\bin` first. const HAS_BASH = process.platform !== 'win32'; const WORKFLOW_PATH = path.join(ROOT, 'msd-core', 'workflows', 'code-review.md'); const PRE_PASS_STEP_PATH = path.join(ROOT, 'msd-core', 'workflows', 'code-review', 'steps', 'structural-pre-pass.md'); const FIXER_PATH = path.join(ROOT, 'agents', 'msd-code-fixer.md'); const REVIEWER_PATH = path.join(ROOT, 'agents', 'msd-code-reviewer.md'); const EXECUTE_PHASE_PATH = path.join(ROOT, 'msd-core', 'workflows', 'execute-phase.md'); const DISPOSITION_STEP_PATH = path.join(ROOT, 'msd-core', 'workflows', 'execute-phase', 'steps', 'code-review-disposition.md'); // --------------------------------------------------------------------------- // #4259: the T6 docs-parity site scan, hoisted out of the assertion so it can // be driven directly by the negative controls below. // // The scan is `$`-anchored with `[^\n]*` on both sides of `--grep=`, so it only // ever matched when `git log` and `--grep=` sat on the SAME physical line. A // shell line-continuation made a semantically identical derivation invisible: // zero hits, and T6 passed. Both generations of the assertion were defeated by // it — the current anti-revert ban let a wrapped site through outright, and at // v1.12.0 a wrapped site was silently exempted from the pattern-conformance // checks written to catch the macOS `\b`-no-op class, so it could have carried // exactly the malformed pattern T6 exists to reject. That is not a hypothetical // shape: wrapping is the natural way to write a `git log` carrying a long ERE, // and a real candidate implementation for #3926 did it, passed T6, and was // caught only by later manual review. // // Folding continuations BEFORE matching is the repair, rather than widening the // regex in place: the assertion's message and its `PHASE_SCOPE_NUM` filter both // assume one site is one string, and a `[\s\S]*?` would happily run the scan // across unrelated statements. The fold corrects the input, so everything built // on the scan is fixed at once. // const GREP_SITE_RE = /^\s*[A-Z_]+=\$\(git log[^\n]*--grep=[^\n]*$/gm; const findGrepSites = (src) => findShellFencedMatches(src, GREP_SITE_RE); // --------------------------------------------------------------------------- // Pure-function implementation of the compute_file_scope Node script body. // This mirrors the logic in code-review.md lines 172-184 exactly. // If those lines change, this function must be updated in tandem (and the // docs-parity assertions below will catch a mismatch at the regex level). // // #2666: the acceptance predicate accepts root-level paths (no `/`) and known // extensionless build files (Dockerfile/Makefile/etc.), not only nested paths // with a trailing extension. Prose bullets are rejected by the known-filename / // has-extension distinction (plus the post-processing existence check backstop // in the shipped workflow). const KNOWN_EXTENSIONLESS_BUILD_FILES = new Set([ 'dockerfile', 'containerfile', 'makefile', 'justfile', 'procfile', ]); function isAcceptablePath(raw) { // A trailing `.`+alphanumerics extension qualifies (root-level OR nested): // package.json, renovate.json, .gitlab-ci.yml, AGENTS.md, app/foo.tsx if (/\.[A-Za-z0-9]+$/.test(raw)) return true; // Known extensionless build filename (basename, case-insensitive): Dockerfile, Makefile, … const base = raw.split('/').pop(); if (KNOWN_EXTENSIONLESS_BUILD_FILES.has(base.toLowerCase())) return true; return false; } function parseKeyFiles(yaml) { const files = []; let inSection = null; for (const line of yaml.split('\n')) { if (/^\s+created:/.test(line)) { inSection = 'created'; continue; } if (/^\s+modified:/.test(line)) { inSection = 'modified'; continue; } // Hyphen-aware boundary: reset inSection for ANY key: line (including key-decisions:, etc.) if (/^\s*[\w-]+:/.test(line) && !/^\s*-/.test(line)) { inSection = null; continue; } if (inSection && /^\s+-\s+(.+)/.test(line)) { let raw = line.match(/^\s+-\s+(.+)/)[1].trim(); raw = raw.replace(/^['"]|['"]$/g, ''); // Order matters: parens BEFORE em-dash because em-dashes can appear inside parens raw = raw.replace(/\s+\([^)]*\)\s*$/, ''); raw = raw.split(/\s+—\s/)[0].trim(); if (isAcceptablePath(raw)) { files.push(raw); } } } return files; } // --------------------------------------------------------------------------- // Pure-function implementation of the present_results severity-label parser. // Mirrors the grep -E "^\s*(critical|blocker):" | head -1 | cut -d: -f2 | xargs // pipeline from code-review.md. // --------------------------------------------------------------------------- function parseFrontmatterCritical(frontmatter) { const lines = frontmatter.split('\n'); const match = lines.find((l) => /^\s*(critical|blocker):/.test(l)); if (!match) return { critical: 0 }; const value = match.split(':').slice(1).join(':').trim(); return { critical: parseInt(value, 10) || 0 }; } // --------------------------------------------------------------------------- // BUG 1 — SUMMARY parser: compute_file_scope must not bleed prose from // hyphenated sections (key-decisions:, patterns-established:, etc.) into the // file list, and must strip em-dash descriptions and parentheticals. // --------------------------------------------------------------------------- describe('Bug 1 — compute_file_scope SUMMARY parser', () => { test('#4461: the shipped compute_file_scope bash fence parses verbatim', () => { const src = readFileNormalized(WORKFLOW_PATH); const stepStart = src.indexOf(''); assert.notStrictEqual(stepStart, -1, 'compute_file_scope step must exist'); const marker = src.indexOf('EXTRACTED=$(', stepStart); assert.notStrictEqual(marker, -1, 'Tier-2 SUMMARY extractor must exist'); const fenceStart = src.lastIndexOf('```bash\n', marker); const fenceEnd = src.indexOf('\n```', marker); assert.ok(fenceStart !== -1 && fenceEnd !== -1, 'Tier-2 SUMMARY bash fence must be complete'); const script = src.slice(fenceStart + '```bash\n'.length, fenceEnd); const result = runHook('-n', ['-c', script], { interpreter: 'bash', timeoutMs: PROBE_TIMEOUT_MS, }); assert.equal(result.exitCode, 0, `bash rejected the shipped fence:\n${result.stderr}`); }); test('#4461: the shipped extractor helper treats adversarial SUMMARY text and argv as inert data', () => { const src = readFileNormalized(WORKFLOW_PATH); const helperStart = src.indexOf(' extract_summary_files() {'); // Anchor on the real loop gate, not the whitespace-only separator above it: // editors are entitled to trim trailing spaces without changing behavior. const helperEnd = src.indexOf('\n if [ -n "$SUMMARIES" ]; then', helperStart); assert.ok(helperStart !== -1 && helperEnd !== -1, 'extract_summary_files helper must be extractable'); const helper = src.slice(helperStart, helperEnd); const dir = createTempDir('msd-4461-adversarial-'); try { const sentinel = path.join(dir, 'MUST-NOT-EXIST'); // This directly executes the shipped helper's argv boundary. It does // not claim the workflow's outer SUMMARY-list iteration preserves // whitespace; that pre-existing shell-word-splitting behavior remains // tracked separately by #4109. // Double quotes are not legal in Windows filenames. Keep the argv // adversarial with shell syntax, whitespace, and a quote that is valid // on every supported filesystem; the SUMMARY payload below exercises // a literal double quote independently. const summary = path.join(dir, "SUMMARY $(not-a-command) 'quoted'.md"); // Git Bash launches the native Windows Node binary in CI. Forward-slash // absolute paths survive that argv boundary on both platforms, while a // raw drive path's backslashes are MSYS quoting syntax rather than data. const shellSentinel = sentinel.replace(/\\/g, '/'); const shellSummary = summary.replace(/\\/g, '/'); const dollarPath = `src/$(touch ${shellSentinel}).js`; const backtickPath = `src/\`touch ${shellSentinel}\`.js`; const quotePath = 'src/"quoted".js'; fs.writeFileSync(summary, [ '---', 'key-files:', ' created:', ` - ${dollarPath}`, ' modified:', ` - ${backtickPath}`, ` - ${quotePath}`, '---', '', ].join('\n')); const script = ['set -eu', helper, 'extract_summary_files "$SUMMARY_PATH"'].join('\n'); const result = toLegacyResult(runHook('-c', [script, 'bash'], { interpreter: 'bash', env: { ...process.env, SUMMARY_PATH: shellSummary }, timeoutMs: PROBE_TIMEOUT_MS, })); assert.equal(result.status, 0, result.stderr); assert.deepStrictEqual(result.stdout.trim().split('\n'), [dollarPath, backtickPath, quotePath]); assert.ok(!fs.existsSync(sentinel), 'SUMMARY payload must never execute command substitutions'); } finally { cleanup(dir); } }); test('extracts only key-files.created and key-files.modified entries', () => { const yaml = [ 'key-files:', ' created:', ' - app/foo.tsx', ' modified:', ' - lib/bar.ts', 'key-decisions:', ' - We chose RSC for performance reasons', 'patterns-established:', ' - Always validate at the boundary', 'requirements-completed:', ' - REQ-01 done', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual(files.sort(), ['app/foo.tsx', 'lib/bar.ts'].sort()); }); test('strips em-dash narrative from bullet: "app/foo.tsx — RSC catalogue with filters"', () => { const yaml = [ 'key-files:', ' created:', ' - app/foo.tsx — RSC catalogue with topic/mode/date filters', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual(files, ['app/foo.tsx']); }); test('strips parenthetical from bullet: "tests/bar.test.ts (122 lines — 17 assertions)"', () => { const yaml = [ 'key-files:', ' created:', ' - tests/bar.test.ts (122 lines — 17 assertions)', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual(files, ['tests/bar.test.ts']); }); test('hyphenated sections in any order produce identical results', () => { const yamlA = [ 'key-decisions:', ' - Some decision', 'key-files:', ' created:', ' - src/index.ts', 'patterns-established:', ' - Some pattern', ].join('\n'); const yamlB = [ 'patterns-established:', ' - Some pattern', 'key-files:', ' created:', ' - src/index.ts', 'key-decisions:', ' - Some decision', ].join('\n'); assert.deepStrictEqual(parseKeyFiles(yamlA), parseKeyFiles(yamlB)); assert.deepStrictEqual(parseKeyFiles(yamlA), ['src/index.ts']); }); test('prose-only bullets from key-decisions are never included in file list', () => { const yaml = [ 'key-decisions:', ' - We chose RSC for performance reasons', ' - Deferred auth to Phase 3', 'key-files:', ' created:', ' - app/page.tsx', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual(files, ['app/page.tsx']); }); // #2666 — the Tier-2 extractor must NOT drop repository-root files (no `/`) // or known extensionless build files. Pre-fix the buggy predicate // `/\//.test(raw) && /\.[A-Za-z0-9]+$/.test(raw)` dropped every root-level // path and every extensionless build file anywhere in the tree. test('#2666 RED: root-level files with extensions are accepted (package.json, renovate.json, .gitlab-ci.yml, AGENTS.md)', () => { const yaml = [ 'key-files:', ' modified:', ' - package.json', ' - renovate.json', ' - .gitlab-ci.yml', ' - AGENTS.md', ' - CLAUDE.md', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual( files.sort(), ['.gitlab-ci.yml', 'AGENTS.md', 'CLAUDE.md', 'package.json', 'renovate.json'], 'root-level files with extensions must not be dropped for lacking a directory separator', ); }); test('#2666: nested extensionless build files are accepted (docker/Dockerfile, web/Makefile)', () => { const yaml = [ 'key-files:', ' modified:', ' - docker/Dockerfile', ' - web/Makefile', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual(files.sort(), ['docker/Dockerfile', 'web/Makefile']); }); test('#2666: root-level extensionless build files are accepted (Dockerfile, Makefile, Justfile, Containerfile, Procfile)', () => { const yaml = [ 'key-files:', ' created:', ' - Dockerfile', ' - Makefile', ' - Justfile', ' - Containerfile', ' - Procfile', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual( files.sort(), ['Containerfile', 'Dockerfile', 'Justfile', 'Makefile', 'Procfile'], ); }); test('#2666 acceptance #1: the reporter 10-file Docker+CI phase yields all 10 paths', () => { const yaml = [ 'key-files:', ' created:', ' - Dockerfile', ' - .gitlab-ci.yml', ' - renovate.json', ' - AGENTS.md', ' - CLAUDE.md', ' - docs/DEVELOPMENT.md', ' - scripts/version-consistency-gate.mjs', ' - web/package.json', ' - web/version_management.md', ' - web/update-version.cjs', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual( files.sort(), [ '.gitlab-ci.yml', 'AGENTS.md', 'CLAUDE.md', 'Dockerfile', 'docs/DEVELOPMENT.md', 'renovate.json', 'scripts/version-consistency-gate.mjs', 'web/package.json', 'web/update-version.cjs', 'web/version_management.md', ], 'the full reporter phase must scope all 10 files, including Dockerfile + root files', ); }); test('#2666 negative-space: a path-like prose bullet with no extension and unknown basename is rejected', () => { // `topic/mode/date filters` has a `/` but no extension and an unknown basename — // the pre-fix predicate dropped it (good), the relaxed predicate must STILL drop it. const yaml = [ 'key-decisions:', ' - topic/mode/date filters', 'key-files:', ' created:', ' - app/page.tsx', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual(files, ['app/page.tsx']); }); test('#2666 negative-space: em-dash/parenthetical stripping still works on an accepted root file', () => { const yaml = [ 'key-files:', ' modified:', ' - Dockerfile — multi-stage build', ].join('\n'); const files = parseKeyFiles(yaml); assert.deepStrictEqual(files, ['Dockerfile']); }); // Docs-parity: the workflow .md must contain the hyphen-aware boundary regex // so what we tested above is actually what is deployed. test('code-review.md contains hyphen-aware boundary regex [\\w-]+', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf8'); // Locate the Node script block in the compute_file_scope step const scriptStart = src.indexOf('const files = [];'); assert.ok(scriptStart !== -1, 'compute_file_scope script must contain "const files = [];"'); const scriptEnd = src.indexOf('if (files.length)', scriptStart); const scriptSection = src.slice(scriptStart, scriptEnd); // Must use [\\w-]+ (hyphen-aware) not \\w+ only const hasHyphenAwareRegex = scriptSection.includes('[\\\\w-]') || scriptSection.includes('[\\w-]'); assert.ok( hasHyphenAwareRegex, 'compute_file_scope boundary regex must be hyphen-aware ([\\w-]+), found section:\n' + scriptSection ); }); // Docs-parity: the workflow .md must contain the em-dash and parenthetical stripping. test('code-review.md contains em-dash split and parenthetical strip in script body', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf8'); const scriptStart = src.indexOf('const files = [];'); const scriptEnd = src.indexOf('if (files.length)', scriptStart); const scriptSection = src.slice(scriptStart, scriptEnd); assert.ok( scriptSection.includes('replace(/\\s+\\([^)]*\\)\\s*$/, \'\')'), 'Script must strip parentheticals with replace(/\\s+\\([^)]*\\)\\s*$/, \'\')' ); assert.ok( scriptSection.includes('split(/\\s+—\\s'), 'Script must split on em-dash to strip narrative' ); }); // #2666 docs-parity: the shipped workflow must NOT still carry the buggy // AND-joined predicate that required BOTH a `/` and a trailing extension — // that predicate dropped every root-level file and every extensionless build // file. Catches a revert of the #2666 fix. test('#2666 docs-parity: compute_file_scope does not contain the buggy slash-and-extension predicate', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf8'); const scriptStart = src.indexOf('const files = [];'); const scriptEnd = src.indexOf('if (files.length)', scriptStart); const scriptSection = src.slice(scriptStart, scriptEnd); assert.ok( !scriptSection.includes('/\\//.test(raw) && /\\.[A-Za-z0-9]+$/.test(raw)'), 'compute_file_scope must not use the buggy AND-joined /\\//.test(raw) && /\\.[A-Za-z0-9]+$/.test(raw) ' + 'predicate (#2666) — it drops every root-level and extensionless build file. Found section:\n' + scriptSection ); }); // #2666 docs-parity: the shipped workflow must reference the known // extensionless build filenames so Dockerfile/Makefile/etc. are accepted. test('#2666 docs-parity: compute_file_scope accepts known extensionless build files (Dockerfile)', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf8'); const scriptStart = src.indexOf('const files = [];'); const scriptEnd = src.indexOf('if (files.length)', scriptStart); const scriptSection = src.slice(scriptStart, scriptEnd); assert.ok( /dockerfile/i.test(scriptSection), 'compute_file_scope must reference known extensionless build filenames (e.g. Dockerfile) ' + 'so they are not dropped (#2666). Found section:\n' + scriptSection ); }); // #2666 docs-parity: the Tier-3 git-diff fallback must intersect with the // SUMMARY scope and warn on dropped files (not only fire on zero Tier-2 hits). test('#2666 docs-parity: Tier-3 intersects/warns against git diff --name-only', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf8'); // The shipped workflow must compute git diff --name-only AND emit a warning // when the diff contains files the SUMMARY extractor did not surface. assert.ok( src.includes('git diff --name-only'), 'code-review.md must run `git diff --name-only` to cross-check the SUMMARY scope (#2666)' ); assert.ok( /warn|missing|not surfaced|did not|not in/i.test(src), 'code-review.md must warn when git diff contains files the SUMMARY extractor dropped (#2666)' ); }); // #2666 docs-parity: the membership test must be EXACT whole-line matching // (grep -Fxq), not an unanchored `case` substring match — otherwise a short // basename in the diff (root `Dockerfile`) substring-matches a longer scoped // path (`docker/Dockerfile`) and is silently skipped, reintroducing the bug. test('#2666 docs-parity: Tier-3 cross-check uses exact whole-line matching (grep -Fxq), not substring case', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf8'); assert.ok( src.includes('grep -Fxq'), 'code-review.md Tier-3 cross-check must use grep -Fxq (exact whole-line match) for membership ' + 'testing, not an unanchored `case` substring match that would skip a root `Dockerfile` ' + 'whose name appears as a suffix of an already-scoped `docker/Dockerfile` (#2666)' ); // The unanchored substring `case "$IN_SCOPE" in` membership test must NOT be // present — it would false-match a basename suffix. Plain substring check (no // regex, so no CRLF-fragility): the grep -Fxq positive guard above proves the // correct mechanism; this negative guard catches a revert to the `case` form. assert.ok( !src.includes('case "$IN_SCOPE"'), 'code-review.md Tier-3 must not use the unanchored `case "$IN_SCOPE"` substring membership ' + 'test (#2666) — use grep -Fxq for exact whole-line matching' ); }); }); // --------------------------------------------------------------------------- // BUG 2 — severity-label parser: present_results must accept both `critical:` // and `blocker:` as Critical-tier frontmatter keys. // --------------------------------------------------------------------------- describe('Bug 2 — present_results severity-label parser', () => { test('frontmatter with blocker: 8 is parsed as critical: 8', () => { const frontmatter = [ 'phase: 03-courses', 'reviewed: 2025-01-01T00:00:00Z', 'findings:', ' blocker: 8', ' warning: 2', ' info: 0', ' total: 10', 'status: issues_found', ].join('\n'); const result = parseFrontmatterCritical(frontmatter); assert.strictEqual(result.critical, 8); }); test('frontmatter with critical: 5 is parsed as critical: 5', () => { const frontmatter = [ 'phase: 03-courses', 'reviewed: 2025-01-01T00:00:00Z', 'findings:', ' critical: 5', ' warning: 1', ' info: 0', ' total: 6', 'status: issues_found', ].join('\n'); const result = parseFrontmatterCritical(frontmatter); assert.strictEqual(result.critical, 5); }); test('frontmatter with neither critical nor blocker returns 0', () => { const frontmatter = [ 'phase: 03-courses', 'findings:', ' warning: 3', ' info: 1', ' total: 4', 'status: issues_found', ].join('\n'); const result = parseFrontmatterCritical(frontmatter); assert.strictEqual(result.critical, 0); }); // Docs-parity: the workflow .md must contain the updated grep pattern. test('code-review.md present_results grep accepts both critical and blocker labels', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf8'); assert.ok( src.includes('grep -E "^[[:space:]]*(critical|blocker):"'), 'code-review.md present_results must grep for both critical: and blocker: labels' ); }); // Docs-parity: the workflow .md must contain the updated grep for BL- headings. test('code-review.md present_results grep includes BL- headings alongside CR- and WR-', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf8'); assert.ok( src.includes('### BL-') && src.includes('### CR-') && src.includes('### WR-'), 'code-review.md present_results must grep for BL- alongside CR- and WR- headings' ); }); }); // --------------------------------------------------------------------------- // BUG 3 — fixer agent ID alphabet and filter sets must include BL-* alongside CR-*. // --------------------------------------------------------------------------- describe('Bug 3 — msd-code-fixer BL-* inclusion in filter sets', () => { test('finding_parser documents BL-\\d+ as Critical-tier-equivalent', () => { const src = fs.readFileSync(FIXER_PATH, 'utf8'); const parserStart = src.indexOf(''); const parserEnd = src.indexOf(''); assert.ok(parserStart !== -1, 'msd-code-fixer.md must have a block'); const parserSection = src.slice(parserStart, parserEnd); assert.ok( parserSection.includes('BL-'), 'finding_parser block must document BL-* as a Critical-tier-equivalent ID prefix' ); }); test('parse_findings step documents severity as "Critical (CR-* or BL-*)"', () => { const src = fs.readFileSync(FIXER_PATH, 'utf8'); const stepStart = src.indexOf(''); const stepEnd = src.indexOf('', stepStart); assert.ok(stepStart !== -1, 'msd-code-fixer.md must have a parse_findings step'); const stepSection = src.slice(stepStart, stepEnd); assert.ok( stepSection.includes('CR-* or BL-*') || stepSection.includes('CR-* and BL-*'), 'parse_findings step must describe Critical severity as "CR-* or BL-*"' ); }); test('critical_warning filter set includes BL-* alongside CR-* and WR-*', () => { const src = fs.readFileSync(FIXER_PATH, 'utf8'); const stepStart = src.indexOf(''); const stepEnd = src.indexOf('', stepStart); const stepSection = src.slice(stepStart, stepEnd); const critWarningIdx = stepSection.indexOf('critical_warning'); assert.ok(critWarningIdx !== -1, 'parse_findings must define critical_warning filter'); const lineStart = stepSection.lastIndexOf('\n', critWarningIdx); const lineEnd = stepSection.indexOf('\n', critWarningIdx); const filterLine = stepSection.slice(lineStart, lineEnd); assert.ok( filterLine.includes('BL-'), 'critical_warning filter line must include BL-*: ' + filterLine.trim() ); }); test('sort order description mentions both CR-* and BL-* for Critical tier', () => { const src = fs.readFileSync(FIXER_PATH, 'utf8'); const stepStart = src.indexOf(''); const stepEnd = src.indexOf('', stepStart); const stepSection = src.slice(stepStart, stepEnd); assert.ok( stepSection.includes('BL-'), 'parse_findings sort-order description must mention BL-* as Critical-tier alongside CR-*' ); }); }); // --------------------------------------------------------------------------- // REVIEWER CONTRACT — msd-code-reviewer.md must acknowledge BL-/blocker: as // an accepted alternative to CR-/critical: (tier-equivalent). // --------------------------------------------------------------------------- describe('Reviewer contract — msd-code-reviewer.md label-equivalence', () => { test('write_review step documents blocker: as accepted alternative to critical:', () => { const src = fs.readFileSync(REVIEWER_PATH, 'utf8'); const stepStart = src.indexOf(''); const stepEnd = src.indexOf('', stepStart); assert.ok(stepStart !== -1, 'msd-code-reviewer.md must have a write_review step'); const stepSection = src.slice(stepStart, stepEnd); assert.ok( stepSection.includes('blocker'), 'write_review step must acknowledge blocker: as a tier-equivalent alternative to critical:' ); }); test('write_review step acknowledges BL- finding ID prefix as Critical-tier-equivalent', () => { const src = fs.readFileSync(REVIEWER_PATH, 'utf8'); const stepStart = src.indexOf(''); const stepEnd = src.indexOf('', stepStart); const stepSection = src.slice(stepStart, stepEnd); assert.ok( stepSection.includes('BL-'), 'write_review step must acknowledge BL- as a Critical-tier-equivalent finding ID prefix' ); }); }); // --------------------------------------------------------------------------- // BUG 4 (#2352) — compute_file_scope must tilde-expand `~/...`-prefixed // SUMMARY.md key-files entries BEFORE the "Filter deleted files" existence // check. Bash only tilde-expands a literal `~` written in source text, never // one arriving as the value of an already-expanded variable — so a real file // recorded as `~/.claude/msd-core/workflows/verify-phase.md` was silently // misclassified as deleted and dropped from REVIEW_FILES, and a phase whose // every recorded file used a `~/...` path hit the empty-scope skip // ("No source files changed ... Skipping review.") as a false negative. // // Tested both ways: a docs-parity assertion (cross-platform, pure fs read) // that the normalization block exists in the deployed workflow text, and a // behavioral test that extracts the actual "Expand tilde paths" + // "Filter deleted files" bash blocks from code-review.md and executes them // via a real bash subprocess against planted files under a fresh HOME. // --------------------------------------------------------------------------- describe('Bug 4 (#2352) — compute_file_scope tilde-path expansion', () => { // Docs-parity: the workflow .md must contain the tilde-normalization block // as step 1 of "Post-processing (all tiers)", ahead of the deleted-file // filter, so what we behaviorally test below is what is actually deployed. test('code-review.md contains a tilde-expansion block ahead of the deleted-file filter', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf8'); const postProcessingIdx = src.indexOf('**Post-processing (all tiers):**'); assert.ok(postProcessingIdx !== -1, 'code-review.md must have a "Post-processing (all tiers)" section'); const expandIdx = src.indexOf('EXPANDED_FILES=()', postProcessingIdx); assert.ok(expandIdx !== -1, 'Post-processing must contain an EXPANDED_FILES=() tilde-expansion loop'); const caseIdx = src.indexOf('case "$file" in', postProcessingIdx); assert.ok(caseIdx !== -1 && caseIdx < expandIdx + 400, 'tilde-expansion loop must use a case "$file" in match'); assert.ok( src.slice(caseIdx, caseIdx + 200).includes('"~/"*)') && src.slice(caseIdx, caseIdx + 200).includes('${HOME}${file#\\~}'), 'tilde-expansion loop must rewrite a leading ~/ to ${HOME}/... via ${file#\\~}' ); const deletedFilterIdx = src.indexOf('DELETED_COUNT=0', postProcessingIdx); assert.ok(deletedFilterIdx !== -1, 'Post-processing must still contain the deleted-file filter'); assert.ok( expandIdx < deletedFilterIdx, 'tilde-expansion loop must run BEFORE the deleted-file filter, not after' ); }); // Extract the tilde-expansion fence and the (non-adjacent — the exclusions // filter sits between them) deleted-file-filter fence from the // "Post-processing (all tiers)" section of code-review.md — the exact // snippets the runtime executes, located by content anchor rather than // position so an intervening step doesn't silently swap in the wrong // block — and glue them behind a synthetic REVIEW_FILES=("$@") seed for // direct execution. The exclusions filter itself is intentionally skipped // here: it only matches relative planning-artifact paths and is orthogonal // to tilde expansion (see code-review.md step 2, "Apply exclusions"). function extractPostProcessingScript() { // readFileNormalized() strips \r\n -> \n before either fence below is // sliced out and later spawned via spawnSync('bash', ...) in // runPostProcessing() — an un-normalized read on a Windows checkout would // break bash mid-script (DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE, #2650). const src = readFileNormalized(WORKFLOW_PATH); const postProcessingIdx = src.indexOf('**Post-processing (all tiers):**'); assert.ok(postProcessingIdx !== -1, 'code-review.md must have a "Post-processing (all tiers)" section'); function fenceContaining(marker) { const markerIdx = src.indexOf(marker, postProcessingIdx); assert.ok(markerIdx !== -1, `expected to find "${marker}" in the Post-processing section`); const fenceStart = src.lastIndexOf('```bash', markerIdx); assert.ok(fenceStart !== -1 && fenceStart > postProcessingIdx, `no \`\`\`bash fence before "${marker}"`); const bodyStart = src.indexOf('\n', fenceStart) + 1; const fenceEnd = src.indexOf('\n```', bodyStart); assert.ok(fenceEnd !== -1, `unterminated \`\`\`bash fence containing "${marker}"`); return src.slice(bodyStart, fenceEnd); } const tildeBlock = fenceContaining('EXPANDED_FILES=()'); const deletedBlock = fenceContaining('DELETED_COUNT=0'); return [ 'REVIEW_FILES=("$@")', tildeBlock, deletedBlock, 'printf "%s\\n" "${REVIEW_FILES[@]}"', 'echo "REVIEW_FILES_COUNT=${#REVIEW_FILES[@]}"', 'echo "DELETED_COUNT=$DELETED_COUNT"', ].join('\n'); } function runPostProcessing(homeDir, files) { const script = extractPostProcessingScript(); // "bash" as $0 so the real REVIEW_FILES entries land in "$@" from $1. return toLegacyResult( runHook('-c', [script, 'bash', ...files], { interpreter: 'bash', env: { ...process.env, HOME: homeDir }, timeoutMs: PROBE_TIMEOUT_MS, }) ); } let tmpHome; test('setup: plant a fresh HOME with a real file', { skip: process.platform === 'win32' }, () => { tmpHome = createTempDir('msd-2352-home-'); fs.mkdirSync(path.join(tmpHome, '.claude', 'msd-core', 'workflows'), { recursive: true }); fs.writeFileSync( path.join(tmpHome, '.claude', 'msd-core', 'workflows', 'verify-phase.md'), '# real file\n', 'utf8' ); }); test( 'AC1: a ~/-prefixed path to a real file survives and is not counted deleted', { skip: process.platform === 'win32' }, () => { const result = runPostProcessing(tmpHome, ['~/.claude/msd-core/workflows/verify-phase.md']); assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`); assert.match( result.stdout, new RegExp(path.join(tmpHome, '.claude', 'msd-core', 'workflows', 'verify-phase.md').replace(/[/\\.]/g, '\\$&')), `expected expanded absolute path in surviving REVIEW_FILES; got: ${JSON.stringify(result.stdout)}` ); assert.match(result.stdout, /DELETED_COUNT=0/, `expected DELETED_COUNT=0; got: ${JSON.stringify(result.stdout)}`); assert.match( result.stdout, /REVIEW_FILES_COUNT=1/, `expected the tilde path to survive into REVIEW_FILES; got: ${JSON.stringify(result.stdout)}` ); } ); test( 'AC2: a ~/-prefixed path to a non-existent file is still correctly excluded as deleted', { skip: process.platform === 'win32' }, () => { const result = runPostProcessing(tmpHome, ['~/.claude/msd-core/workflows/does-not-exist.md']); assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`); assert.match(result.stdout, /DELETED_COUNT=1/, `expected DELETED_COUNT=1; got: ${JSON.stringify(result.stdout)}`); assert.match( result.stdout, /REVIEW_FILES_COUNT=0/, `expected the missing tilde path to be dropped; got: ${JSON.stringify(result.stdout)}` ); } ); test( 'AC3: a phase where every recorded file is a real ~/-prefixed path does not empty the scope', { skip: process.platform === 'win32' }, () => { const result = runPostProcessing(tmpHome, ['~/.claude/msd-core/workflows/verify-phase.md']); assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`); const countMatch = result.stdout.match(/REVIEW_FILES_COUNT=(\d+)/); assert.ok(countMatch, `expected a REVIEW_FILES_COUNT line; got: ${JSON.stringify(result.stdout)}`); assert.ok( Number(countMatch[1]) > 0, 'an all-tilde real-file scope must not reduce to zero (would trigger the empty-scope skip)' ); } ); test( 'AC4: mixed tilde + missing ordinary relative path resolve independently', { skip: process.platform === 'win32' }, () => { const result = runPostProcessing(tmpHome, [ '~/.claude/msd-core/workflows/verify-phase.md', 'this/relative/path/does-not-exist.md', ]); assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`); assert.match(result.stdout, /DELETED_COUNT=1/, `expected exactly 1 deleted; got: ${JSON.stringify(result.stdout)}`); assert.match( result.stdout, /REVIEW_FILES_COUNT=1/, `expected only the tilde path to survive; got: ${JSON.stringify(result.stdout)}` ); assert.doesNotMatch( result.stdout, /this\/relative\/path\/does-not-exist\.md/, 'the missing ordinary relative path must not survive into REVIEW_FILES' ); } ); test('teardown: remove the temp HOME', { skip: process.platform === 'win32' }, () => { cleanup(tmpHome); }); }); // --------------------------------------------------------------------------- // Shared diff-base extraction/execution helpers (Bug 5 #3191, Bug 6 #3503). // // The workflow computes "the phase's base commit" in three independent bash // invocations (each is its own shell): the Tier-3 file-scope fallback // (compute_file_scope), the agent-context DIFF_BASE (spawn_reviewer), and the // fallow pre-pass's --changed-since base (structural-pre-pass.md). // // Behavioral style follows Bug 4: extract the SHIPPED bash from the workflow // .md files by content anchor and execute it via a real bash subprocess // against a git fixture — so the assertion binds the deployed text, not a // JS reimplementation. Running the real `git log` (not a regex shim) is what // makes platform-level regex holes (the #3191 macOS `\b` no-op) visible. // --------------------------------------------------------------------------- // The ```bash fence containing `marker`, located after `fromIdx`. function fenceContaining(src, marker, fromIdx = 0) { const markerIdx = src.indexOf(marker, fromIdx); assert.ok(markerIdx !== -1, `expected to find "${marker}" in workflow source`); const fenceStart = src.lastIndexOf('```bash', markerIdx); assert.ok(fenceStart !== -1, `no \`\`\`bash fence before "${marker}"`); const bodyStart = src.indexOf('\n', fenceStart) + 1; const fenceEnd = src.indexOf('\n```', bodyStart); assert.ok(fenceEnd !== -1, `unterminated \`\`\`bash fence containing "${marker}"`); return src.slice(bodyStart, fenceEnd); } // The Tier-3 derivation prefix: fence start up to the REVIEW_FILES branch. function extractTier3Derivation() { const src = readFileNormalized(WORKFLOW_PATH); const fence = fenceContaining(src, '# Compute diff base from phase commits'); const cut = fence.indexOf('if [ ${#REVIEW_FILES[@]} -eq 0 ]'); assert.ok(cut !== -1, 'Tier-3 fence must contain the REVIEW_FILES empty-scope branch'); return fence.slice(0, cut); } // spawn_reviewer no longer derives its own DIFF_BASE (#4209 B3 fix: a second, // divergent recomputation there made the external reviewer lane and the // internal reviewer diff against different base SHAs on any re-review). It // now reuses the value compute_file_scope's Tier-3 derivation already // computed, so this is the SAME snippet as extractTier3Derivation() — kept // as a distinct name so T2/T5 below still read as testing spawn_reviewer's // contract, not just Tier 3's. function extractSpawnReviewerDerivation() { return extractTier3Derivation(); } // The fallow phase-scope derivation, from the step fragment. The fragment // carries markdown-escaped quotes (\") in this fence — an authoring // artifact that survived #2994 fragmentization verbatim; the runtime agent // normalizes them when transcribing, so the test does the same before // executing. Sliced from FALLOW_SCOPE_ARGS=() (skipping the msd-tools // runtime resolver line above it, which exits 1 on machines without an // installed msd-tools and is orthogonal to the base-derivation under test) // to just before the msd_run invocation (which needs the real binary). function extractFallowDerivation() { const src = readFileNormalized(PRE_PASS_STEP_PATH); const fence = fenceContaining(src, 'FALLOW_PHASE_START=$(git log'); const scopeStart = fence.indexOf('FALLOW_SCOPE_ARGS=()'); assert.ok(scopeStart !== -1, 'fallow fence must define FALLOW_SCOPE_ARGS=()'); const cut = fence.indexOf('msd_run run-with-timeout'); assert.ok(cut !== -1, 'fallow fence must contain the msd_run run-with-timeout call'); assert.ok(scopeStart < cut, 'FALLOW_SCOPE_ARGS must precede the msd_run invocation'); return fence.slice(scopeStart, cut).replace(/\\"/g, '"'); } // Execute a derivation snippet with PADDED_PHASE (and the fallow scope gate) // set, echoing the values it computes between sentinels so multi-line // PHASE_COMMITS parse cleanly. function runDerivation(repo, snippet, phase) { const script = [ `PADDED_PHASE=${phase}`, // #3995: the derivations anchor on the phase's own directory, not a // commit-subject grep — the fixture commits each phase's directory at // its first scope commit. `PHASE_DIR=${repo}/.planning/phases/${phase}-ctx`, 'FALLOW_SCOPE=phase', snippet, 'echo "===PHASE_START==="', 'printf \'%s\\n\' "$PHASE_START"', 'echo "===DIFF_BASE==="', 'printf \'%s\\n\' "$DIFF_BASE"', 'echo "===FALLOW_BASE==="', 'printf \'%s\\n\' "$FALLOW_BASE"', 'echo "===END==="', ].join('\n'); // Bash FAN-OUT: the extracted snippet runs `git log` plus an `echo | tail` // pipe — the wrong class for `PROBE_TIMEOUT_MS` (a single short CLI // probe). Same class as the observed CI failures in // tests/quick-branching.test.cjs (PR #3787 run 32668773524) and // tests/worktree-safety.test.cjs (`next` run 32608945654). See // HOOK_FANOUT_TIMEOUT_MS in ./helpers/timeouts.cjs for the class // rationale. return toLegacyResult( runHook('-c', [script, 'bash'], { interpreter: 'bash', cwd: repo, timeoutMs: HOOK_FANOUT_TIMEOUT_MS, }) ); } function parseSentinel(stdout, name) { const m = stdout.match(new RegExp(`===${name}===\\n([\\s\\S]*?)\\n===`)); if (!m) return null; return m[1].split('\n').map((l) => l.trim()).filter((l) => l.length > 0); } // --------------------------------------------------------------------------- // Bug 5 (#3191) — EVERY diff-base derivation must use the same anchored, // portable derivation. // // The workflow computes "the phase's base commit" in three independent bash // invocations (each is its own shell): the Tier-3 file-scope fallback // (compute_file_scope), the agent-context DIFF_BASE (spawn_reviewer), and the // fallow pre-pass's --changed-since base (structural-pre-pass.md). #2989 // anchored only the Tier-3 copy — and did so with `\b`, which is not a POSIX // ERE token, so on macOS (regex(3)) that grep matches NOTHING and Tier 3 // always fails closed. The other two sites kept the original unanchored // `--grep="${PADDED_PHASE}"`, whose oldest substring match is routinely a // version-string/date commit from months before the phase existed. // (#3503 later replaced the anchor itself — a subject-line conventional- // commit scope match instead of the "[Pp]hase N" prose phrase, which MSD's // own commits never contain; see Bug 6. The lockstep + portability + // fail-closed contract THIS block verifies is unchanged.) // // Behavioral style follows Bug 4: extract the SHIPPED bash from the workflow // .md files by content anchor and execute it via a real bash subprocess // against a git fixture — so the assertion binds the deployed text, not a // JS reimplementation. Running the real `git log` (not a regex shim) is what // keeps platform-level regex holes (the #3191 macOS `\b` no-op) visible. // --------------------------------------------------------------------------- // Shared: the fixture phase directory every derivation anchors on (#3995). const PHASE06_PLAN_REL = path.join('.planning', 'phases', '06-ctx', '06-PLAN.md'); // Shared history builder (was local to the #3503 describe; the #3995 rows // reuse it). Each entry is [relPath, subject, body?]; parent dirs are created. function buildHistory(prefix, commits) { const repo = createTempGitProject(prefix); const hashes = {}; for (const [file, message, body] of commits) { fs.mkdirSync(path.dirname(path.join(repo, file)), { recursive: true }); fs.writeFileSync(path.join(repo, file), `${message}\n`); gitOrThrow(['add', file], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); gitOrThrow(['commit', '-m', message, ...(body ? ['-m', body] : [])], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); hashes[file] = gitOrThrow(['rev-parse', 'HEAD'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }).trim(); } return { repo, hashes }; } describe('Bug 5 (#3191) — same anchored, portable phase-scope grep at all three diff-base sites', () => { const SKIP_WIN32 = { skip: process.platform === 'win32' }; // Fixture: five commits whose messages exercise every false-match class // from the issue — version string + date, another phase's plan whose scope // number is a digit-superset, a prose "Phase N" mention in another phase's // subject — plus the phase's real first scope commit and an unrelated HEAD. function buildFixture(prefix, phaseCommitMessage, opts = {}) { // opts.skipPhaseDir: the fail-closed row (T5) commits NO phase directory, // so the directory anchor must resolve nothing. const commitPhaseDir = opts.skipPhaseDir !== true; const repo = createTempGitProject(prefix); const phaseDir = path.join(repo, '.planning', 'phases', '06-ctx'); fs.mkdirSync(phaseDir, { recursive: true }); const commits = [ ['c1.txt', 'chore: bump to v2.06.0 on 2026-01-05'], ['c2.txt', 'docs(60-01): unrelated phase-plan work'], [commitPhaseDir ? PHASE06_PLAN_REL : 'c3.txt', phaseCommitMessage], ['c4.txt', 'chore: Phase 60 cleanup'], ['c5.txt', 'docs: touch README'], ]; const hashes = {}; for (const [file, message] of commits) { fs.mkdirSync(path.dirname(path.join(repo, file)), { recursive: true }); fs.writeFileSync(path.join(repo, file), `${message}\n`); gitOrThrow(['add', file], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); gitOrThrow(['commit', '-m', message], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); hashes[file] = gitOrThrow(['rev-parse', 'HEAD'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }).trim(); } return { repo, hashes }; } test( 'T1 + T4: Tier-3 derivation matches ONLY the phase\'s real scope commit — never a digit-substring or superset hit', SKIP_WIN32, () => { const { repo, hashes } = buildFixture('msd-3191-tier3-', 'docs(06): capture phase context'); try { const result = runDerivation(repo, extractTier3Derivation(), '06'); assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`); const phaseStart = parseSentinel(result.stdout, 'PHASE_START'); const diffBase = parseSentinel(result.stdout, 'DIFF_BASE'); // AC: the phase's real commits are a small minority of digit-containing // commits; the derivation must resolve to an ancestor near the phase's // actual first commit (c3^) — never the older v2.06.0/docs(06-01) hits. assert.deepStrictEqual( phaseStart, [hashes[PHASE06_PLAN_REL]], `Tier-3 anchor must resolve to the phase dir's first commit; got: ${JSON.stringify(phaseStart)}` ); assert.deepStrictEqual( diffBase, [`${hashes[PHASE06_PLAN_REL]}^`], 'Tier-3 DIFF_BASE must be the phase first-commit parent' ); } finally { cleanup(repo); } } ); test( 'T2: spawn_reviewer DIFF_BASE derivation uses the same anchored grep (not the bare digit)', SKIP_WIN32, () => { const { repo, hashes } = buildFixture('msd-3191-spawn-', 'docs(06): capture phase context'); try { const result = runDerivation(repo, extractSpawnReviewerDerivation(), '06'); assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`); const phaseStart = parseSentinel(result.stdout, 'PHASE_START'); const diffBase = parseSentinel(result.stdout, 'DIFF_BASE'); // Pre-fix this matches c1 and c2 as well and tail -1 picks c1 — the // oldest unrelated match — feeding a bogus diff_base to the reviewer // agent exactly when files: is empty (the fail-closed scenario). assert.deepStrictEqual( phaseStart, [hashes[PHASE06_PLAN_REL]], `spawn_reviewer anchor must resolve to the phase dir's first commit; got: ${JSON.stringify(phaseStart)}` ); assert.deepStrictEqual( diffBase, [`${hashes[PHASE06_PLAN_REL]}^`], 'spawn_reviewer DIFF_BASE must be the phase first-commit parent' ); } finally { cleanup(repo); } } ); test( 'T3: fallow phase scope derives --changed-since from the anchored grep, never an old substring match', SKIP_WIN32, () => { const { repo, hashes } = buildFixture('msd-3191-fallow-', 'docs(06): capture phase context'); try { const result = runDerivation(repo, extractFallowDerivation(), '06'); assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`); const fallowBase = parseSentinel(result.stdout, 'FALLOW_BASE'); // Pre-fix the unanchored grep's oldest match is the v2.06.0 commit, so // FALLOW_SCOPE_ARGS resolves to --changed-since // and widens the structural pre-pass far beyond the phase. assert.deepStrictEqual( fallowBase, [`${hashes[PHASE06_PLAN_REL]}^`], `FALLOW_BASE must be the phase first-commit parent, got: ${JSON.stringify(fallowBase)}` ); } finally { cleanup(repo); } } ); test( 'root commit: fallow phase scope uses a resolvable root SHA', SKIP_WIN32, () => { const repo = createTempDir('msd-4183-fallow-root-'); try { gitOrThrow(['init', '-b', 'main'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); gitOrThrow(['config', 'user.email', 'test@test.com'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); gitOrThrow(['config', 'user.name', 'Test'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); gitOrThrow(['config', 'commit.gpgsign', 'false'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); const phaseFile = path.join(repo, '.planning', 'phases', '06-ctx', 'PLAN.md'); fs.mkdirSync(path.dirname(phaseFile), { recursive: true }); fs.writeFileSync(phaseFile, '# phase context\n'); gitOrThrow(['add', '.planning'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); gitOrThrow(['commit', '-m', 'docs(06): initial phase context'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); const rootSha = gitOrThrow(['rev-parse', 'HEAD'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }).trim(); fs.writeFileSync(path.join(repo, 'index.js'), 'module.exports = 1;\n'); gitOrThrow(['add', 'index.js'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); gitOrThrow(['commit', '-m', 'feat: add source'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }); const result = runDerivation(repo, extractFallowDerivation(), '06'); assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`); const fallowBase = parseSentinel(result.stdout, 'FALLOW_BASE'); assert.deepStrictEqual( fallowBase, [rootSha], `root-parent FALLOW_BASE regression: expected ${rootSha}, got ${JSON.stringify(fallowBase)}`, ); assert.equal( gitOrThrow(['rev-parse', '--verify', `${fallowBase[0]}^{commit}`], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS }).trim(), rootSha, 'FALLOW_BASE must resolve to the root commit', ); if (process.env.CI) { const { requireFallowBinary } = require('../msd-core/bin/lib/fallow-runner.cjs'); const { execTool } = require('../msd-core/bin/lib/shell-command-projection.cjs'); const audit = execTool( requireFallowBinary({ cwd: ROOT, envPath: '' }), ['audit', '--changed-since', fallowBase[0], '--format', 'json'], { cwd: repo, timeout: FALLOW_AUDIT_TIMEOUT_MS }, ); assert.ok([0, 1].includes(audit.exitCode), `fallow root audit exit=${audit.exitCode}; stderr=${audit.stderr}`); console.log(`fallow-root-audit normal-exit=${audit.exitCode}`); } } finally { cleanup(repo); } }, ); test( 'T5: with no genuine phase scope commit, every derivation yields NO base (fail-closed preserved)', SKIP_WIN32, () => { const { repo } = buildFixture('msd-3191-closed-', 'feat: scanner core', { skipPhaseDir: true }); // no committed phase dir anywhere try { for (const [label, snippet] of [ ['tier3', extractTier3Derivation()], ['spawn_reviewer', extractSpawnReviewerDerivation()], ['fallow', extractFallowDerivation()], ]) { const result = runDerivation(repo, snippet, '06'); assert.equal(result.status, 0, `${label} exited ${result.status}; stderr=${result.stderr}`); const phaseStart = parseSentinel(result.stdout, 'PHASE_START'); const diffBase = parseSentinel(result.stdout, 'DIFF_BASE'); const fallowBase = parseSentinel(result.stdout, 'FALLOW_BASE'); assert.deepStrictEqual(phaseStart, [], `${label}: no phase dir committed — anchor must stay empty`); assert.deepStrictEqual(diffBase, [], `${label}: DIFF_BASE must stay empty (no bogus base)`); assert.deepStrictEqual(fallowBase, [], `${label}: FALLOW_BASE must stay unset`); } } finally { cleanup(repo); } } ); // T6 docs-parity anti-revert (#3191/#3995): every diff-base derivation in // both files must use the SAME phase-directory anchor — and no message-grep // derivation may return (a subject carries no milestone bound; that class // failed five times: #2989/#3191/#3503/#3995). // #4259: the T6 scan drives itself off the live workflow files, which are // clean — so its matching branch is exercised only by whatever those files // happen to contain, and the hole it had was invisible for exactly that // reason. These fixtures drive findGrepSites directly, in both directions. test('#4259 T6 site scan sees a backslash-continued derivation, and still ignores what it should', () => { const sameLine = [ '```bash', 'PHASE_START=$(git log --extended-regexp --grep="^(feat|fix)\\(phase-${PHASE_SCOPE_NUM}" --format="%H")', '```', ].join('\n'); // Semantically identical to the row above. The only difference is two // continued physical lines, and that used to be enough to vanish. const continued = [ '```bash', 'PHASE_START=$(git log \\', ' --extended-regexp \\', ' --grep="^(feat|fix)\\(phase-${PHASE_SCOPE_NUM}" --format="%H")', '```', ].join('\n'); assert.equal(findGrepSites(sameLine).length, 1, 'the same-line form must stay caught'); assert.equal(findGrepSites(continued).length, 1, 'the continued form must now be caught (#4259)'); // The filter T6 actually asserts on has to see the marker too. Before the // fold this failed twice over: the scan returned nothing, AND // PHASE_SCOPE_NUM sat on a different physical line from the one the scan // would have captured, so even a matching scan would have filtered it out. for (const src of [sameLine, continued]) { assert.equal( findGrepSites(src).filter((l) => l.includes('PHASE_SCOPE_NUM')).length, 1, 'the captured site must carry the marker T6 filters on', ); } // Negative control that DOES exercise the fold: a continued, non-phase // grep site remains a site, but must not become a phase-scope finding. const benign = [ '```bash', 'RELEASE_NOTES=$(git log \\', ' --grep="^chore" --format="%s")', '```', ].join('\n'); assert.equal( findGrepSites(benign).filter((l) => l.includes('PHASE_SCOPE_NUM') || /phase-\)?\(/.test(l)).length, 0, 'a non-phase-scope --grep must stay clean', ); // A wrapped git-log assignment that merely sits near a --grep string must // not be glued into one logical line with it. This exercises the fold and // still contains every keyword the site regex looks for. const unrelated = [ '```bash', 'SOME_VAR=$(git log \\', ' --format="%H")', 'echo "--grep=$SOME_VAR"', '```', ].join('\n'); assert.deepStrictEqual(findGrepSites(unrelated), [], 'a wrapped unrelated assignment must not glue into a hit'); // A continuation must not reach across a blank line — the reason this // folds [ \t]* rather than \s* after the newline. const acrossBlank = [ '```bash', 'SOME_VAR=$(git log \\', '', 'FOO=--grep=x', '```', ].join('\n'); assert.deepStrictEqual(findGrepSites(acrossBlank), [], 'the fold must stop at a blank line'); // Two trailing backslashes represent one literal backslash followed by a // real newline. Folding this would invent a site the shell does not have. const evenBackslashes = [ '```bash', 'PHASE_START=$(git log \\\\', ' --grep="phase-${PHASE_SCOPE_NUM}")', '```', ].join('\n'); assert.deepStrictEqual( findGrepSites(evenBackslashes), [], 'an even trailing-backslash run is not a shell continuation', ); // Three trailing backslashes retain one literal pair while the final // backslash continues the command. This pins the non-trivial odd boundary. const oddBackslashes = [ '```bash', `PHASE_START=$(git log ${'\\'.repeat(3)}`, ' --grep="phase-${PHASE_SCOPE_NUM}")', '```', ].join('\n'); assert.deepStrictEqual( findGrepSites(oddBackslashes), [`PHASE_START=$(git log ${'\\'.repeat(2)} --grep="phase-\${PHASE_SCOPE_NUM}")`], 'an odd trailing-backslash run keeps its literal pairs and continues the line', ); }); test('#4259 continuation folding preserves every odd/even backslash-run boundary', () => { fc.assert(fc.property( fc.integer({ min: 0, max: 31 }), fc.array(fc.constantFrom(' ', '\t'), { maxLength: 8 }).map((chars) => chars.join('')), (runLength, indentation) => { const slashes = '\\'.repeat(runLength); const source = `cmd ${slashes}\n${indentation}tail`; const expected = runLength % 2 === 1 ? `cmd ${'\\'.repeat(runLength - 1)} tail` : source; assert.strictEqual(foldShellContinuations(source), expected); }, ), { numRuns: 200 }); }); test('#4259 T6 site scan reports nothing on the live workflow files', () => { // The adoption check: only shell code fences are scanned, so markdown hard // breaks and examples in other languages cannot be folded into fake shell // sites. Distinct from T6 itself, this asserts the live scan is quiet. for (const src of [ readFileNormalized(WORKFLOW_PATH), readFileNormalized(PRE_PASS_STEP_PATH).replace(/\\"/g, '"'), ]) { assert.deepStrictEqual(findGrepSites(src), []); } }); test('T6 docs-parity: all diff-base derivations use the identical phase-directory anchor; no --grep site remains', () => { const sources = [ readFileNormalized(WORKFLOW_PATH), readFileNormalized(PRE_PASS_STEP_PATH).replace(/\\"/g, '"'), ]; for (const src of sources) { assert.ok( src.includes('PHASE_START=$(git log --format="%H" --diff-filter=A -- "${PHASE_DIR}"'), 'each file must derive the base from the phase directory\'s first commit (#3995)' ); } const grepSites = []; for (const src of sources) { // #4259: findGrepSites folds backslash continuations first, so a wrapped // assignment presents as one logical line and cannot slip the scan. grepSites.push(...findGrepSites(src)); } assert.deepStrictEqual( grepSites.filter((l) => l.includes('PHASE_SCOPE_NUM') || /phase-\)?\(/.test(l)), [], 'no phase-scope message-grep derivation may remain — subjects carry no milestone bound (#3995)' ); }); }); describe('Bug 6 (#3503/#3995) — diff base keys on the phase directory, not commit subjects', () => { const SKIP_WIN32 = { skip: process.platform === 'win32' }; const REPRO_HISTORY = [ ['c1.txt', 'chore: bump to v2.06.0 on 2026-01-05'], ['c2.txt', 'feat(60-01): probe wiring', 'The EF path still uses it, fenced to Phase 06 per D-09.'], ['c3.txt', 'docs: commit message format', 'Phase headers use the form:\n\n### Phase 06 (Cluster B): Title\n\nin ROADMAP detail sections.'], [PHASE06_PLAN_REL, 'docs(06): capture phase context'], ['c5.txt', 'feat(06-01): implement scanner core'], ['c6.txt', 'docs(phase-6): update tracking after wave 1'], ['c7.txt', 'docs: touch README'], ]; test( 'T1: prose forward-references and doc-format examples never capture the base — it resolves to the phase dir first commit at all three sites', SKIP_WIN32, () => { const { repo, hashes } = buildHistory('msd-3503-scope-', REPRO_HISTORY); try { const sites = [ ['tier3', extractTier3Derivation()], ['spawn_reviewer', extractSpawnReviewerDerivation()], ['fallow', extractFallowDerivation()], ]; for (const [label, snippet] of sites) { const result = runDerivation(repo, snippet, '06'); assert.equal(result.status, 0, `${label} exited ${result.status}; stderr=${result.stderr}`); const phaseStart = parseSentinel(result.stdout, 'PHASE_START'); const diffBase = parseSentinel(result.stdout, 'DIFF_BASE'); const fallowBase = parseSentinel(result.stdout, 'FALLOW_BASE'); const dirFirst = hashes[PHASE06_PLAN_REL]; if (label !== 'fallow') { assert.deepStrictEqual( phaseStart, [dirFirst], `${label}: anchor must resolve to the phase dir's first commit; got: ${JSON.stringify(phaseStart)}` ); } const expected = [`${dirFirst}^`]; if (label === 'fallow') { assert.deepStrictEqual(fallowBase, expected, `${label}: base must be the phase dir first commit's parent`); } else { assert.deepStrictEqual(diffBase, expected, `${label}: base must be the phase dir first commit's parent`); } } } finally { cleanup(repo); } } ); test( 'T2: subject spellings are irrelevant to the directory anchor — unpadded and padded histories resolve identically', SKIP_WIN32, () => { const { repo, hashes } = buildHistory('msd-3503-unpadded-', [ ['c1.txt', 'feat(60-01): probe wiring', 'Deferred to Phase 06 per D-09.'], [PHASE06_PLAN_REL, 'docs(phase-6): capture phase context'], ['c3.txt', 'feat(6-01): implement scanner core'], ['c4.txt', 'test(6): persist human verification items as UAT'], ['c5.txt', 'docs: touch README'], ]); try { for (const [label, snippet] of [ ['tier3', extractTier3Derivation()], ['spawn_reviewer', extractSpawnReviewerDerivation()], ['fallow', extractFallowDerivation()], ]) { const result = runDerivation(repo, snippet, '06'); assert.equal(result.status, 0, `${label} exited ${result.status}; stderr=${result.stderr}`); const diffBase = parseSentinel(result.stdout, 'DIFF_BASE'); const fallowBase = parseSentinel(result.stdout, 'FALLOW_BASE'); const expected = [`${hashes[PHASE06_PLAN_REL]}^`]; if (label === 'fallow') { assert.deepStrictEqual(fallowBase, expected, `${label}: base must be the phase dir first commit's parent`); } else { assert.deepStrictEqual(diffBase, expected, `${label}: base must be the phase dir first commit's parent`); } } } finally { cleanup(repo); } } ); test( 'T3: no committed phase dir fails closed (no silent arbitrary base)', SKIP_WIN32, () => { const { repo } = buildHistory('msd-3503-closed-', [ ['c1.txt', 'chore: bump to v2.06.0'], ['c2.txt', 'feat(60-01): probe wiring', 'Deferred to Phase 06 per D-09.'], ['c3.txt', 'docs(06): capture phase context'], ['c4.txt', 'docs: touch README'], ]); try { for (const [label, snippet] of [ ['tier3', extractTier3Derivation()], ['spawn_reviewer', extractSpawnReviewerDerivation()], ['fallow', extractFallowDerivation()], ]) { const result = runDerivation(repo, snippet, '06'); assert.equal(result.status, 0, `${label} exited ${result.status}; stderr=${result.stderr}`); const diffBase = parseSentinel(result.stdout, 'DIFF_BASE'); const fallowBase = parseSentinel(result.stdout, 'FALLOW_BASE'); assert.deepStrictEqual(diffBase, [], `${label}: DIFF_BASE must stay empty without a committed phase dir`); assert.deepStrictEqual(fallowBase, [], `${label}: FALLOW_BASE must stay unset`); } } finally { cleanup(repo); } } ); // #3995: the milestone-blind repro. A PREVIOUS milestone's phase-02 commit // exists in history with a perfectly anchored subject; the current // milestone's phase 02 has its own directory. The old derivation's // unbounded grep + tail -1 selected the archived milestone's commit and // took a 7-file phase to a 3388-file scope; the directory anchor cannot. test( "T4 (#3995): a previous milestone's same-numbered phase commit never captures the base", SKIP_WIN32, () => { const oldMilestonePhase = path.join('.planning', 'milestones', 'v1.1-phases', '02-old', '02-PLAN.md'); const currentPhase = path.join('.planning', 'phases', '02-ctx', '02-PLAN.md'); const { repo, hashes } = buildHistory('msd-3995-milestone-', [ [oldMilestonePhase, 'feat(02-01): research-project command, workflow, and template'], ['mid.txt', 'chore: close milestone v1.1'], [currentPhase, 'feat(02-01): current milestone phase 02 plan 01'], ['c4.txt', 'docs: touch README'], ]); try { for (const [label, snippet] of [ ['tier3', extractTier3Derivation()], ['spawn_reviewer', extractSpawnReviewerDerivation()], ['fallow', extractFallowDerivation()], ]) { const result = runDerivation(repo, snippet, '02'); assert.equal(result.status, 0, `${label} exited ${result.status}; stderr=${result.stderr}`); const diffBase = parseSentinel(result.stdout, 'DIFF_BASE'); const fallowBase = parseSentinel(result.stdout, 'FALLOW_BASE'); const expected = [`${hashes[currentPhase]}^`]; if (label === 'fallow') { assert.deepStrictEqual(fallowBase, expected, `${label}: base must be the CURRENT phase dir's first commit, never the archived milestone's (#3995)`); } else { assert.deepStrictEqual(diffBase, expected, `${label}: base must be the CURRENT phase dir's first commit, never the archived milestone's (#3995)`); } } } finally { cleanup(repo); } } ); }); // --------------------------------------------------------------------------- // #4209 Phase 1 Plan 3 (Task 2) — external reviewer evidence consolidation. // msd-code-reviewer.md must treat as untrusted // input: independently re-verify every claim against the actual current // source before it can appear in REVIEW.md, fold a verified claim into the // SAME Narrative Findings section (no second schema), and never let text // embedded inside an evidence file act as an instruction. code-review.md's // EXTERNAL_EVIDENCE_BLOCK must keep restating the four fixed prohibitions. // --------------------------------------------------------------------------- describe('CONS-01..03 — external reviewer evidence consolidation (#4209)', () => { function loadStep(src, stepName) { const stepStart = src.indexOf(``); assert.ok(stepStart !== -1, `agent must have a ${stepName} step`); const stepEnd = src.indexOf('', stepStart); return src.slice(stepStart, stepEnd); } test('load_context parses and marks it untrusted', () => { const src = fs.readFileSync(REVIEWER_PATH, 'utf8'); const stepSection = loadStep(src, 'load_context'); assert.ok(stepSection.includes('external_reviewer_evidence'), 'load_context must parse the external_reviewer_evidence block'); assert.ok(/untrusted/i.test(stepSection), 'load_context must explicitly mark external reviewer evidence as untrusted data'); }); test('load_context requires independent re-verification against actual source before accepting a claim', () => { const src = fs.readFileSync(REVIEWER_PATH, 'utf8'); const stepSection = loadStep(src, 'load_context'); assert.ok(/re-open|reopen/i.test(stepSection) && /re-read/i.test(stepSection), 'load_context must require re-opening and re-reading the actual cited source before accepting an external claim'); assert.ok(/REJECTED|reject/i.test(stepSection), 'load_context must state that an unverifiable external claim is rejected, not included'); }); test('load_context resists prompt injection embedded inside evidence text', () => { const src = fs.readFileSync(REVIEWER_PATH, 'utf8'); const stepSection = loadStep(src, 'load_context'); assert.ok(/prompt-injection|prompt injection/i.test(stepSection), 'load_context must name prompt injection as a threat from evidence content'); assert.ok(/never a command|not a command/i.test(stepSection), 'load_context must state evidence text is data, never a command'); }); test('a verified external claim folds into Narrative Findings with no separate schema (CONS-03)', () => { const src = fs.readFileSync(REVIEWER_PATH, 'utf8'); const stepSection = loadStep(src, 'load_context'); assert.ok(/Narrative Findings/.test(stepSection), 'load_context must route a verified external claim into the existing Narrative Findings section'); const writeReviewSection = loadStep(src, 'write_review'); assert.ok(/external:/.test(writeReviewSection), 'write_review must document the (external: {slug}) provenance tag for a verified external finding'); assert.ok(!/## External/i.test(writeReviewSection), 'write_review must not introduce a separate External Findings section — one REVIEW.md schema only'); }); test('critical_rules restates the untrusted-evidence contract', () => { const src = fs.readFileSync(REVIEWER_PATH, 'utf8'); const rulesStart = src.indexOf(''); const rulesEnd = src.indexOf(''); assert.ok(rulesStart !== -1 && rulesEnd !== -1, 'msd-code-reviewer.md must have a critical_rules section'); const rulesSection = src.slice(rulesStart, rulesEnd); assert.ok(/external_reviewer_evidence|external reviewer/i.test(rulesSection), 'critical_rules must restate the external-evidence-is-untrusted contract'); }); test('code-review.md restates the four fixed source-review prohibitions when handing off evidence', () => { const workflowSrc = fs.readFileSync(WORKFLOW_PATH, 'utf8'); const blockStart = workflowSrc.indexOf('EXTERNAL_EVIDENCE_BLOCK=$(printf'); assert.ok(blockStart !== -1, 'code-review.md must build an EXTERNAL_EVIDENCE_BLOCK'); const blockEnd = workflowSrc.indexOf('\n', workflowSrc.indexOf(')', blockStart)); const blockText = workflowSrc.slice(blockStart, blockEnd); for (const prohibition of ['no source mutation', 'no test execution', 'no background processes', 'no active polling']) { assert.ok(blockText.includes(prohibition), `EXTERNAL_EVIDENCE_BLOCK must restate "${prohibition}" (SAFE-03..06)`); } }); }); // #3829 — execute-phase `code_review_gate`: surface the severity counts it // already parses, and record a per-finding disposition. // // Before this change the gate extracted `status:` from REVIEW.md's frontmatter, // discarded the `critical`/`warning`/`info`/`total` values sitting in the same // `sed` range, and printed a message that was byte-identical for a review with // one `info` finding and a review with a Critical. Nothing recorded what // happened to any finding, so a phase reached `phase.complete` with Criticals // standing and no trace they had been seen. // // Tested the way this file tests every other embedded parser: a pure-JS mirror // of the shipped `node -e` script (validated against the shipped block during // implementation), plus docs-parity assertions on the workflow .md text, which // is itself the deployed contract. // --------------------------------------------------------------------------- // Mirror of the gate's frontmatter scalar reads. The shipped block strips CR, then takes ONLY // the first `---` ... `---` block — it does NOT use a sed range, because a sed range re-opens on // a body `---` and runs to EOF, which leaks body lines into an OPTIONAL key's read. This mirror // must model that extraction, not a whole-document scan: a mirror that scans the document passes // on a fixture the shipped code fails, which is the drift this comment exists to prevent. function parseGateCounts(reviewText) { const lines = String(reviewText).replace(/\r/g, '').split('\n'); // Only a CLOSED frontmatter block counts: an unterminated one must yield nothing rather than // hand the whole review body to the reads below. let fm = []; if (lines[0] === '---') { const buf = []; let closed = false; for (let i = 1; i < lines.length; i++) { if (lines[i] === '---') { closed = true; break; } buf.push(lines[i]); } if (closed) fm = buf; } // The shipped reads are `grep -m1 | cut -d: -f2- | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//'`, // and BOTH stages matter. This mirror previously modelled `cut -d: -f2 | tr -d ' '`, which REPAIRED a // malformed scalar into a number twice over -- `tr -d` deleting INTERNAL spaces (`1 0` -> `10`) and // `-f2` keeping only the second colon-field (`1: junk` -> `1`). Both were retired from the shipped // reads when an adversarial pass showed a repaired number deciding whether a shortfall was reported. // `-f2-` keeps the whole scalar and only the ends are trimmed. The class below is POSIX // [[:space:]] AS THE C LOCALE DEFINES IT -- space, tab, newline, VT, FF, CR -- spelled out // literally rather than as JS `\s`, which also matches the unicode spaces C does not. That is an // equivalence, not an approximation, ONLY because the shipped reads pin LC_ALL=C: glibc's C.UTF-8 // classifies U+2003 as [[:space:]] and as [[:blank:]], so an unpinned shipped read trimmed a // character this mirror keeps, and the divergence was invisible here because both sides still // landed on the same arm for every fixture that existed. The anchors below are spelled with the // same literal class, for the same reason -- `\s` there would match a U+2003 indent the pinned // shipped grep does not. const TRIM = /^[ \t\n\v\f\r]+|[ \t\n\v\f\r]+$/g; const first = (re) => { for (const line of fm) { const m = line.match(re); if (m) return line.slice(line.indexOf(':') + 1).replace(TRIM, ''); } return ''; }; // The four counts belong to the `findings:` MAPPING, so the mirror scopes to it exactly as the // shipped awk does: select the findings: block, stop at the next column-0 key. Without this a // top-level key later named `total:` / `info:` / `critical:` is picked up ahead of the nested // one. `status:` stays anchored at column 0 because it IS top-level. const findingsBlock = []; { let inBlock = false; for (const line of fm) { // The BLOCK BOUNDARY takes the literal class too, and it is not decoration: the shipped // selector is an `awk` whose `/^findings:[[:space:]]*$/` and `/^[^[:space:]]/` resolve // through the ambient locale exactly as grep's and sed's did. Pass 6 found the awk had been // left unpinned while grep and sed were fixed -- `findings:` opened the block under // C.UTF-8 and did not under C, so the SAME review reported a breakdown on one machine and // the countless message on another. `\s`/`\S` here would model neither pinned side. if (/^findings:[ \t\n\v\f\r]*$/.test(line)) { inBlock = true; continue; } if (inBlock && /^[^ \t\n\v\f\r]/.test(line)) break; if (inBlock) findingsBlock.push(line); } } // SAME pipeline as `first` above, and the duplication is the point: BOTH helpers model the shipped // reads, and the counts go through THIS one. A previous edit updated `first` and the comment above // it while leaving this body on the retired `split(':')[1].replace(/ /g,'')` — the shipped block had // moved to `-f2-` + end-trim and the mirror had not, which the parity fixtures could not see because // both parsers landed on the countless arm for every fixture that then existed. The // `a self-consistent repaired breakdown` fixture below is what separates them. const firstIn = (lines, re) => { for (const line of lines) { const m = line.match(re); if (m) return line.slice(line.indexOf(':') + 1).replace(TRIM, ''); } return ''; }; return { status: first(/^status:(.*)$/), critical: firstIn(findingsBlock, /^[ \t\n\v\f\r]*(?:critical|blocker):(.*)$/), warning: firstIn(findingsBlock, /^[ \t\n\v\f\r]*warning:(.*)$/), info: firstIn(findingsBlock, /^[ \t\n\v\f\r]*info:(.*)$/), total: firstIn(findingsBlock, /^[ \t\n\v\f\r]*total:(.*)$/), }; } // ── The SHIPPED disposition builder, executed — not modelled ────────────────── // // Rounds 1 and 2 of the pre-filing review both refuted "the mirrors are faithful": a // hand-written model of a shell-embedded script drifts, and when it drifts the behavioural // tests pass while the shipped block is broken. Mutation-testing confirmed it — deleting the // carried-row logic from the shipped file turned nothing red. // // So the disposition tests below run the ACTUAL script. It is pure Node (every input arrives // through process.env), so extracting it and executing it needs no shell and works on Windows. // The only transformation is undoing the two shell escapes the surrounding double-quoted // `node -e "…"` string requires: \` -> ` and \$ -> $. function shippedDispositionScript() { const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8').replace(/\r\n/g, '\n'); const open = src.indexOf('node -e "'); assert.ok(open !== -1, 'the disposition step must still embed a node -e script'); const body = src.slice(open + 'node -e "'.length); const end = body.indexOf('\n" || echo '); assert.ok(end !== -1, 'the node -e script must still be closed by its || echo fallback'); // Undo exactly what the surrounding double-quoted shell string does, in ONE left-to-right // pass: inside "..." a backslash is special only before $ ` " or \\, and everything else is // literal. Doing these as separate passes (or missing \\\\ -> \\) silently hands the test a // DIFFERENT regex from the one that ships — which is how an escaped-pipe case passed here // while failing in the shell. return body.slice(0, end).replace(/\\([\\$`"])/g, '$1'); } // Run the shipped script against a temp phase dir and return the ledger it wrote (or null). function runShippedDisposition({ reviewText, priorText, fixText, iterFixText, padded = '01', reviewTotal }) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3829-')); try { const reviewPath = path.join(dir, padded + '-REVIEW.md'); const dispPath = path.join(dir, padded + '-REVIEW-DISPOSITION.md'); const fixPath = path.join(dir, padded + '-REVIEW-FIX.md'); fs.writeFileSync(reviewPath, reviewText); if (priorText !== undefined) fs.writeFileSync(dispPath, priorText); if (fixText !== undefined) fs.writeFileSync(fixPath, fixText); // The --auto loop's per-iteration backups (-REVIEW-FIX.iterN.md). Keyed by iteration // number so a test can drive the ordering the script relies on (newest wins). for (const [n, text] of Object.entries(iterFixText || {})) { fs.writeFileSync(path.join(dir, padded + '-REVIEW-FIX.iter' + n + '.md'), text); } // Bounded by construction via the process seam — an unbounded spawn is an indefinite hang. const res = runNode(['-e', shippedDispositionScript()], { timeoutMs: PROBE_TIMEOUT_MS, env: { ...process.env, REVIEW_FILE: reviewPath, DISPOSITION_FILE: dispPath, FIX_REPORT_FILE: fixPath, PADDED: padded, // The frontmatter total block 2 derives and hands to the script, so the two parsers can // be reconciled. Passed through here so a test can drive the shortfall path. REVIEW_TOTAL: reviewTotal === undefined ? '' : String(reviewTotal), }, }); assert.strictEqual(res.outcome, OUTCOME.EXITED, 'the shipped script must run to completion'); assert.strictEqual(res.exitCode, 0, 'the shipped script must exit 0: ' + res.stderr); const ledger = fs.existsSync(dispPath) ? fs.readFileSync(dispPath, 'utf8') : null; return { ledger, stdout: res.stdout, wroteNothing: /disposition unchanged/.test(res.stdout) }; } finally { cleanup(dir); } } // Parse the rows out of a rendered ledger, so assertions read against real output. function ledgerRows(ledger) { if (ledger === null) return null; const rows = []; for (const line of ledger.split('\n')) { const m = line.match(/^\|\s*((?:CR|BL|WR|IN)-\d+)\s*\|\s*([a-z]+)\s*\|\s*([a-z]+)\s*\|\s*(.*?)\s*\|$/); if (m) rows.push({ id: m[1], severity: m[2], disposition: m[3], source: m[4] }); } return rows; } // buildDisposition is NOT a mirror any more — it drives the SHIPPED script. // // Three pre-filing review rounds each refuted "the mirror is faithful", and mutation testing // confirmed the cost: a hand-written model of a shell-embedded script drifts, and when it drifts // the tests pass while the shipped block is broken. The model is gone; this adapter runs the real // thing and returns the same shape the assertions below already expect. function buildDisposition({ reviewText, priorText, fixText, padded = '01', reviewTotal }) { const rows = ledgerRows(runShippedDisposition({ reviewText, priorText, fixText, padded, reviewTotal }).ledger); if (rows === null) return null; return { rows: rows.map((r) => ({ ...r, carried: / \(not in the current review\)$/.test(r.source) || undefined, source: r.source.replace(/ \(not in the current review\)$/, ''), })), open: rows.filter((r) => r.disposition === 'open').length, total: rows.length, }; } const REVIEW_WITH_FINDINGS = [ '---', 'phase: 01', 'findings:', ' critical: 1', ' warning: 2', ' info: 1', ' total: 4', 'status: issues_found', '---', '', '## Critical Issues', '', '### CR-01: SQL injection in auth', '', '## Warnings', '', '### WR-01: missing null check', '', '### WR-02: unused import', '', '## Info', '', '### IN-01: stale TODO', ].join('\n'); describe('#3829 — code_review_gate severity surfacing', () => { test('the gate reads the counts that sit beside the status it already parsed', () => { const parsed = parseGateCounts(REVIEW_WITH_FINDINGS); assert.deepStrictEqual(parsed, { status: 'issues_found', critical: '1', warning: '2', info: '1', total: '4', }); }); test('blocker: is accepted as the Critical tier-equivalent of critical:', () => { const parsed = parseGateCounts(REVIEW_WITH_FINDINGS.replace(' critical: 1', ' blocker: 1')); assert.strictEqual(parsed.critical, '1'); }); test('a status:/total: line in the review BODY never displaces the frontmatter value', () => { // The `sed -n '/^---$/,/^---$/p'` range re-opens on a body `---` and runs to // EOF, so first-match (`grep -m1`) is what makes this correct — not the range. const poisoned = REVIEW_WITH_FINDINGS + '\n\n---\n\nstatus: clean\ntotal: 999\n'; const parsed = parseGateCounts(poisoned); assert.strictEqual(parsed.status, 'issues_found'); assert.strictEqual(parsed.total, '4'); }); test('a REVIEW.md with no findings: block yields no counts, so the gate can fall back', () => { const legacy = ['---', 'phase: 02', 'status: issues_found', '---', '', '# Phase 02'].join('\n'); const parsed = parseGateCounts(legacy); assert.strictEqual(parsed.status, 'issues_found'); assert.strictEqual(parsed.total, ''); assert.strictEqual(parsed.critical, ''); }); test('docs-parity: the gate states the counts rather than the countless message alone', () => { const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8'); assert.ok( src.includes('Code review: ${REVIEW_TOTAL} findings — ${REVIEW_CRITICAL} critical, ${REVIEW_WARNING} warning, ${REVIEW_INFO} info.'), 'code_review_gate must display the per-severity breakdown it parsed' ); }); test('docs-parity: every frontmatter scalar read carries a first-match guard', () => { const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8'); assert.ok( src.includes('grep -m1 "^status:"'), 'the status: read must keep its single-match guard (DEFECT.FRONTMATTER-SCALAR-BROAD-GREP)' ); }); }); describe('#3829 — code_review_gate per-finding disposition record', () => { test('every finding is recorded, defaulting to open', () => { const d = buildDisposition({ reviewText: REVIEW_WITH_FINDINGS, padded: '01' }); assert.strictEqual(d.total, 4); assert.strictEqual(d.open, 4); assert.deepStrictEqual(d.rows.map((r) => r.id), ['CR-01', 'WR-01', 'WR-02', 'IN-01']); assert.deepStrictEqual(d.rows.map((r) => r.severity), ['critical', 'warning', 'warning', 'info']); }); test('BL- findings are recorded at the Critical tier alongside CR-', () => { const d = buildDisposition({ reviewText: REVIEW_WITH_FINDINGS.replace('### CR-01:', '### BL-01:'), padded: '01', }); assert.strictEqual(d.rows[0].id, 'BL-01'); assert.strictEqual(d.rows[0].severity, 'critical'); }); test('--fix outcomes are reconciled from REVIEW-FIX.md with provenance', () => { const fixText = [ '# Phase 01: Code Review Fix Report', '', '## Fixed Issues', '', '### WR-01: missing null check', '', '## Skipped Issues', '', '### WR-02: unused import', ].join('\n'); const d = buildDisposition({ reviewText: REVIEW_WITH_FINDINGS, fixText, padded: '01' }); const byId = Object.fromEntries(d.rows.map((r) => [r.id, r])); assert.strictEqual(byId['WR-01'].disposition, 'fixed'); assert.strictEqual(byId['WR-01'].source, '01-REVIEW-FIX.md'); assert.strictEqual(byId['WR-02'].disposition, 'skipped'); assert.strictEqual(byId['CR-01'].disposition, 'open'); assert.strictEqual(d.open, 2); }); test('a finding heading outside the Fixed/Skipped sections is not a disposition', () => { const fixText = [ '## Fixed Issues', '', '### WR-01: missing null check', '', '## Verification', '', '### IN-01: mentioned while describing how the fix was verified', ].join('\n'); const d = buildDisposition({ reviewText: REVIEW_WITH_FINDINGS, fixText, padded: '01' }); const byId = Object.fromEntries(d.rows.map((r) => [r.id, r])); assert.strictEqual(byId['WR-01'].disposition, 'fixed'); assert.strictEqual(byId['IN-01'].disposition, 'open'); }); test('a recorded decision survives a re-run — open never overwrites deferred', () => { const priorText = [ '| Finding | Severity | Disposition | Source |', '|---------|----------|-------------|--------|', '| CR-01 | critical | deferred | ships next phase |', '| WR-01 | warning | open | - |', ].join('\n'); const d = buildDisposition({ reviewText: REVIEW_WITH_FINDINGS, priorText, padded: '01' }); const byId = Object.fromEntries(d.rows.map((r) => [r.id, r])); assert.strictEqual(byId['CR-01'].disposition, 'deferred'); // The source cell carries the human's stated reason and is preserved, not replaced. assert.strictEqual(byId['CR-01'].source, 'ships next phase'); assert.strictEqual(d.open, 3); }); test('an applied --fix outcome wins over an earlier deferral', () => { const priorText = '| CR-01 | critical | deferred | later |'; const fixText = ['## Fixed Issues', '', '### CR-01: SQL injection in auth'].join('\n'); const d = buildDisposition({ reviewText: REVIEW_WITH_FINDINGS, priorText, fixText, padded: '01' }); assert.strictEqual(d.rows[0].disposition, 'fixed'); }); test('a review with no finding headings produces no record at all', () => { const legacy = ['---', 'phase: 02', 'status: issues_found', '---', '', 'prose only'].join('\n'); assert.strictEqual(buildDisposition({ reviewText: legacy, padded: '02' }), null); }); test('docs-parity: the gate writes a REVIEW-DISPOSITION sibling, not into REVIEW.md', () => { // Eighth src.includes() converted. It pinned the literal `${PHASE_DIR}` interpolation, so it // went red when path construction moved to a VALIDATED local -- while the property it names, // "a REVIEW-DISPOSITION sibling", was untouched. Assert the property: the ledger lands beside // the review, under the derived name, and REVIEW.md itself is not written. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3861-sib-')); try { const reviewPath = path.join(dir, '02-REVIEW.md'); const reviewText = ['---', 'status: issues_found', '---', '', '### CR-01: a finding'].join('\n'); fs.writeFileSync(reviewPath, reviewText); const res = runNode(['-e', shippedDispositionScript()], { timeoutMs: PROBE_TIMEOUT_MS, env: { ...process.env, REVIEW_FILE: reviewPath, DISPOSITION_FILE: path.join(dir, '02-REVIEW-DISPOSITION.md'), FIX_REPORT_FILE: path.join(dir, '02-REVIEW-FIX.md'), PADDED: '02', }, }); assert.strictEqual(res.exitCode, 0, res.stderr); assert.ok(fs.existsSync(path.join(dir, '02-REVIEW-DISPOSITION.md')), 'code_review_gate must write the disposition record to a REVIEW-DISPOSITION sibling'); assert.strictEqual(fs.readFileSync(reviewPath, 'utf8'), reviewText, 'and must not write into REVIEW.md — msd-code-reviewer is its single writer'); } finally { cleanup(dir); } // Sixth src.includes() converted. It pinned the exact SOURCE LINE of the enumeration loop, so // it went red when M3 reformatted that loop to track the enclosing section -- while the // property it names, "enumerate every finding ID", was strictly widened rather than broken. // A pin on a code shape reports every refactor as a regression and no regression as one. const review = ['---', 'phase: 01', 'status: issues_found', '---', '', '### CR-01: a', '### WR-01: b', '### IN-01: c', '### CR-01: a duplicate id, which must not produce a second row'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: review }).ledger); assert.deepStrictEqual( rows.map((r) => r.id), ['CR-01', 'WR-01', 'IN-01'], 'code_review_gate must enumerate every finding ID from REVIEW.md, in order, once each' ); }); test('docs-parity: code_review_gate actually reaches the extracted step', () => { // The step is only reachable because the parent says to read and execute it. Without this, // every other docs-parity assertion here could pass against a file nothing loads. const parent = fs.readFileSync(EXECUTE_PHASE_PATH, 'utf8'); assert.ok( /Read and execute\s+`msd-core\/workflows\/execute-phase\/steps\/code-review-disposition\.md`/.test(parent), 'code_review_gate must read and execute the disposition step' ); assert.ok( parent.indexOf('code-review-disposition.md') < parent.indexOf('**TDD review escalation'), 'and must do so before the TDD escalation can stop the phase' ); }); test('docs-parity: the disposition write is non-blocking', () => { const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8'); assert.ok( src.includes('Code review disposition record skipped (non-blocking).'), 'the disposition write must never block execution flow' ); }); }); // --------------------------------------------------------------------------- // #3829 review round — the seven defects a cross-AI adversarial pass on the fix // diff refuted before filing. Each is pinned here so the round's work survives. // --------------------------------------------------------------------------- describe('#3829 review round — frontmatter scoping, section anchoring, ledger durability', () => { test('an OPTIONAL count absent from frontmatter is not supplied by a leaked body value', () => { // A sed range re-opens on a body `---` and runs to EOF. First-match protects a key the // frontmatter DOES carry; it cannot protect one it does not. Extraction must stop at the // closing delimiter, or a body `total:` becomes the reported finding count. const leaky = [ '---', 'phase: 01', 'status: issues_found', '---', '', '# Report', '', '---', '', 'total: 7', 'critical: 9', ].join('\n'); const parsed = parseGateCounts(leaky); assert.strictEqual(parsed.status, 'issues_found'); assert.strictEqual(parsed.total, '', 'a body total: must not become the finding count'); assert.strictEqual(parsed.critical, '', 'a body critical: must not become the critical count'); }); test('a CRLF-authored review parses to bare values, with no carriage return riding along', () => { const crlf = [ '---', 'phase: 01', 'findings:', ' critical: 1', ' warning: 0', ' info: 2', ' total: 3', 'status: issues_found', '---', '', '### CR-01: a', ].join('\r\n'); const parsed = parseGateCounts(crlf); assert.deepStrictEqual(parsed, { status: 'issues_found', critical: '1', warning: '0', info: '2', total: '3', }); for (const v of Object.values(parsed)) assert.ok(!/\r/.test(v), 'no value may carry a CR'); }); test('docs-parity: the gate stops at the closing delimiter and strips CR before parsing', () => { const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8'); assert.ok( src.includes(`awk 'NR==1{if($0!="---") exit; next} /^---$/{closed=1; exit}`), 'the gate must extract only the FIRST frontmatter block, not a re-opening sed range' ); assert.ok( src.includes(`tr -d '\\r' < "$REVIEW_FILE"`), 'the gate must strip CR so a CRLF review cannot inject one into the message' ); }); test('a section heading that merely STARTS with "Fixed Issues" does not classify findings', () => { const fixText = [ '# Fix report', '', '## Fixed Issues Verification', '', '### IN-01: named while verifying', ].join('\n'); const d = buildDisposition({ reviewText: REVIEW_WITH_FINDINGS, fixText, padded: '01' }); const byId = Object.fromEntries(d.rows.map((r) => [r.id, r])); assert.strictEqual(byId['IN-01'].disposition, 'open'); }); test('a deferral reason written into the Source cell survives a re-run verbatim', () => { const priorText = '| CR-01 | critical | deferred | ships next phase, see ADR-99 |'; const d = buildDisposition({ reviewText: REVIEW_WITH_FINDINGS, priorText, padded: '01' }); const cr = d.rows.find((r) => r.id === 'CR-01'); assert.strictEqual(cr.disposition, 'deferred'); assert.strictEqual(cr.source, 'ships next phase, see ADR-99'); }); test('a decided finding the current review no longer reports is carried, not dropped', () => { // --auto re-reviews and rewrites REVIEW.md; a fixed or deferred finding can vanish from it. // Dropping the row would erase the record that it was seen — the exact failure #3829 is about. const priorText = [ '| CR-01 | critical | deferred | ships next phase |', '| IN-01 | info | open | - |', ].join('\n'); const shrunk = ['---', 'status: issues_found', '---', '', '### WR-01: still here'].join('\n'); const d = buildDisposition({ reviewText: shrunk, priorText, padded: '01' }); const byId = Object.fromEntries(d.rows.map((r) => [r.id, r])); assert.ok(byId['CR-01'], 'a deferred finding absent from the review must still be recorded'); assert.strictEqual(byId['CR-01'].disposition, 'deferred'); assert.strictEqual(byId['CR-01'].carried, true); assert.strictEqual(byId['CR-01'].source, 'ships next phase'); // M1: the untriaged row is carried TOO, and marked. Dropping it erased the record that the // finding was ever seen -- which is #3829's complaint verbatim, reproduced by the artifact // built to prevent it. 'Nothing was decided about it' is precisely the state that must leave // a trace. The marker keeps it honest: the row does not claim the finding is live. assert.ok(byId['IN-01'], 'an untriaged row for a vanished finding is carried, not deleted'); assert.strictEqual(byId['IN-01'].disposition, 'open'); assert.strictEqual(byId['IN-01'].carried, true); }); test('the carried marker is rendered, never stored — it cannot accumulate across runs', () => { // The marker lives in the Source cell, which is re-parsed on the next run. Storing it would // re-append it every time: the cell grows without bound AND the file changes on every run, // which silently defeats the unchanged-run check and restores the empty-docs-commit churn. const shrunk = ['---', 'status: issues_found', '---', '', '### WR-01: b'].join('\n'); let priorText = '| CR-01 | critical | deferred | ships next phase |'; let cr; for (let i = 0; i < 3; i++) { const d = buildDisposition({ reviewText: shrunk, priorText, padded: '01' }); cr = d.rows.find((r) => r.id === 'CR-01'); // Re-render the row the way the shipped builder does, and feed it back in. priorText = '| ' + cr.id + ' | ' + cr.severity + ' | ' + cr.disposition + ' | ' + cr.source + (cr.carried ? ' (not in the current review)' : '') + ' |'; } assert.strictEqual(cr.source, 'ships next phase', 'the stored source must stay canonical'); assert.strictEqual( (priorText.match(/\(not in the current review\)/g) || []).length, 1, 'the marker must appear exactly once no matter how many times the gate re-runs' ); }); test('docs-parity: the ledger is rewritten only on a real change, timestamp excluded', () => { const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8'); assert.ok( src.includes("const stripTs = (t) => t.replace(/^recorded:.*\\$/m, 'recorded:');"), 'the gate must compare ignoring the timestamp so an unchanged run writes nothing' ); assert.ok( src.includes('Code review disposition unchanged: '), 'an unchanged run must say so rather than producing an empty docs commit' ); }); test('docs-parity: section headings are anchored whole and the prior source cell is captured', () => { const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8'); assert.ok( src.includes('/^##\\s+Fixed Issues\\s*\\$/') && src.includes('/^##\\s+Skipped Issues\\s*\\$/'), 'fix-report section headings must be matched whole, never by prefix' ); // Eighth src.includes() converted -- it pinned the exact source-cell CAPTURE, and that // capture was the round-3 defect (a bare | failed the whole match). The property it named // is asserted behaviourally below: 'an escaped pipe in a deferral reason survives whole' // and 'a bare pipe in a deferral reason is kept as prose'. // Seventh src.includes() converted -- it pinned the exact comparison EXPRESSION, so it went // red when the comparison gained whitespace normalization while the property it names was // unchanged. Assert the property instead: a fix report naming a different finding under a // reused id does not decide the row. const review = ['---', 'status: issues_found', '---', '', '### CR-01: a new finding'].join('\n'); const stale = ['## Fixed Issues', '', '### CR-01: what this id used to mean'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: review, fixText: stale }).ledger); assert.strictEqual(rows[0].disposition, 'open', 'a fix report must name the SAME finding before its outcome is applied'); }); }); describe('#3861 round 2 — the disposition ledger is reachable in a shipped path (B1c/B1d)', () => { const FIX_WORKFLOW = path.join(ROOT, 'msd-core', 'workflows', 'code-review-fix.md'); test('code-review-fix.md reads and executes the disposition step after committing the fix report', () => { // Without this the reconciliation logic -- the bulk of the step and nearly all of its test // surface -- is reachable only on a RE-EXECUTION of the phase, and REQ-REVIEW-10 is unmet in // every shipped path. execute-phase.md's gate invokes review with neither --fix nor --auto, // so REVIEW-FIX.md cannot exist there and every row it writes is `open` by construction. const src = fs.readFileSync(FIX_WORKFLOW, 'utf8'); assert.match(src, //, 'the fix workflow must carry a step that records the disposition'); assert.match(src, /msd-core\/workflows\/execute-phase\/steps\/code-review-disposition\.md/, 'and it must invoke the SAME step, not a second copy of the logic'); // Ordering is load-bearing: the fix report must be on disk before the ledger claims anything // about it. Assert the step positions rather than merely their presence. const commitAt = src.indexOf(''); const recordAt = src.indexOf(''); const presentAt = src.indexOf(''); assert.ok(commitAt > -1 && recordAt > -1 && presentAt > -1, 'all three steps must exist'); assert.ok(commitAt < recordAt, 'the ledger is reconciled AFTER the fix report is written'); assert.ok(recordAt < presentAt, 'and before results are presented'); }); test('the iteration backups are removed AFTER the ledger has read them, not inside the loop', () => { // #3861 round 5. The .iterN.md backups are the only surviving record of what an earlier --auto // iteration fixed -- this workflow keeps ONE final version of each artifact, and the re-review // drops a finding once it is fixed, so neither final artifact carries it. Deleting them at the // end of the loop meant record_disposition reached a CONVERGED run with every early fix already // erased and recorded those findings as open. const src = fs.readFileSync(FIX_WORKFLOW, 'utf8'); const recordAt = src.indexOf(''); const cleanupAt = src.indexOf(''); assert.ok(cleanupAt > -1, 'the backups must be removed by a named step, not inline in the loop'); assert.ok(recordAt < cleanupAt, 'the ledger reads the backups BEFORE they are removed'); // And the removal must not have been left behind in the loop as well. const loopAt = src.indexOf(''); const loopBody = src.slice(loopAt, src.indexOf('')); assert.ok(!/rm -f "\$\{REVIEW_PATH%\.md\}\.iter"/.test(loopBody), 'the loop must no longer delete the backups it just wrote'); }); test('the reconciliation is reachable: gate writes all-open, the fix path resolves it', () => { // The two call sites driven in sequence, which is the shipped order. This is the review's own // input -> wrong output case, inverted: a phase whose findings are all fixed by a subsequent // --fix run used to end at `open: N / total: N`, asserting that every triaged finding was // forgotten -- worse than recording nothing, because it looks authoritative and is inverted. const review = ['---', 'phase: 01', 'status: issues_found', 'findings:', ' critical: 1', ' warning: 1', ' info: 0', ' total: 2', '---', '', '## Critical Issues', '', '### CR-01: the critical one', '', '## Warnings', '', '### WR-01: the warning one'].join('\n'); const fixReport = ['---', 'status: complete', '---', '', '## Fixed Issues', '', '### CR-01: the critical one', '', '## Skipped Issues', '', '### WR-01: the warning one'].join('\n'); // Call site 1 -- execute-phase.md's code_review_gate. No fix report exists yet. const gate = runShippedDisposition({ reviewText: review, reviewTotal: 2 }); const gateRows = ledgerRows(gate.ledger); assert.deepStrictEqual(gateRows.map((r) => r.disposition), ['open', 'open'], 'at the gate there is no fix report, so every row is open -- correctly'); // Call site 2 -- code-review-fix.md's record_disposition, with the report on disk and the // gate's ledger carried in as prior state. const after = runShippedDisposition({ reviewText: review, priorText: gate.ledger, fixText: fixReport, reviewTotal: 2, }); const rows = ledgerRows(after.ledger); assert.deepStrictEqual( rows.map((r) => [r.id, r.disposition]), [['CR-01', 'fixed'], ['WR-01', 'skipped']], 'the fix outcomes reach the ledger' ); assert.match(after.ledger, /^open: 0$/m, 'and the headline agrees'); assert.match(after.ledger, /^total: 2$/m); }); }); describe('#3861 round 2 — severity comes from the section, not just the id prefix (M3)', () => { const sevOf = (reviewText, id) => { const rows = ledgerRows(runShippedDisposition({ reviewText }).ledger); const row = rows.find((r) => r.id === id); assert.ok(row, id + ' must have a row'); return row.severity; }; test("a Critical mis-numbered as WR- is recorded critical when it sits under '## Critical Issues'", () => { // The section heading is the reviewer's OWN statement of severity, and the walker already // visits it. Deriving from the prefix alone puts the ledger's Severity column -- the whole // basis for triaging it -- in disagreement with the review it summarizes and with the // frontmatter count line block 1 prints. const review = ['---', 'phase: 01', 'status: issues_found', '---', '', '## Critical Issues', '', '### CR-01: properly numbered', '', '### WR-04: a critical the reviewer mis-numbered', '', '## Info', '', '### IN-01: an info item'].join('\n'); assert.strictEqual(sevOf(review, 'WR-04'), 'critical', 'the section outranks the prefix'); assert.strictEqual(sevOf(review, 'CR-01'), 'critical'); assert.strictEqual(sevOf(review, 'IN-01'), 'info'); }); test('an IN- finding under ## Warnings is recorded warning', () => { // The other direction, so the rule is not one-way: the section governs whichever way the // prefix disagrees with it. const review = ['---', 'phase: 01', 'status: issues_found', '---', '', '## Warnings', '', '### IN-02: mis-numbered the other way'].join('\n'); assert.strictEqual(sevOf(review, 'IN-02'), 'warning'); }); test('the id prefix still governs when no recognized section encloses the finding', () => { // Fallback control. A review that does not use the documented headings -- and every carried // row from an earlier review -- must keep the prefix mapping, BL- included. const review = ['---', 'phase: 01', 'status: issues_found', '---', '', '### CR-01: a', '### BL-02: b', '### WR-03: c', '### IN-04: d'].join('\n'); assert.strictEqual(sevOf(review, 'CR-01'), 'critical'); assert.strictEqual(sevOf(review, 'BL-02'), 'critical', 'BL- stays Critical-tier-equivalent'); assert.strictEqual(sevOf(review, 'WR-03'), 'warning'); assert.strictEqual(sevOf(review, 'IN-04'), 'info'); }); test('a lookalike section heading does not re-tier the findings under it', () => { // Matched WHOLE, exactly as the fix-report sections are. A prefix match would let // '## Critical Issues Verification' promote everything beneath it. const review = ['---', 'phase: 01', 'status: issues_found', '---', '', '## Critical Issues Verification', '', '### IN-05: not actually critical'].join('\n'); assert.strictEqual(sevOf(review, 'IN-05'), 'info', 'an unrecognized section falls back to the prefix'); }); test('a section heading inside a fenced example does not govern', () => { // The fence walker already skips fenced content; this pins that the new section tracking // honours it rather than reading an illustration as document structure. const review = ['---', 'phase: 01', 'status: issues_found', '---', '', '```markdown', '## Critical Issues', '```', '', '### IN-06: outside the fence'].join('\n'); assert.strictEqual(sevOf(review, 'IN-06'), 'info'); }); }); describe('#3861 round 11 — a carried row keeps the severity the ledger recorded', () => { // The ledger always WROTE a severity (table cell + frontmatter key) and nothing read it back: // the row regex skipped the cell as [^|]*, the frontmatter walk collected only titles, and a // carried row was rebuilt from the id prefix because sectionSev holds only the CURRENT review's // findings. So the one case round 2's M3 fix exists for -- a Critical the reviewer mis-numbered // WR-04 -- was recorded critical on run 1 and silently re-recorded warning on run 2, once the // review stopped reporting it. Every carried-row test used CR-01/IN-01, whose prefix already // matched, so the fallback returned the right answer by coincidence and no mutation could tell // the two paths apart. Reproduced by the round-11 review by executing the shipped script twice. const REVIEW_1 = ['---', 'phase: 01', 'status: issues_found', '---', '', '## Critical Issues', '', '### CR-01: properly numbered', '', '### WR-04: a critical the reviewer mis-numbered', ''].join('\n'); // Run 2: the review no longer reports WR-04 at all. const REVIEW_2_DROPPED = ['---', 'phase: 01', 'status: issues_found', '---', '', '## Critical Issues', '', '### CR-01: properly numbered', ''].join('\n'); const rowOf = (ledger, id) => { const row = ledgerRows(ledger).find((r) => r.id === id); assert.ok(row, id + ' must have a row'); return row; }; // Run 1's ledger, with WR-04 deferred BY HAND, the way the ledger's own legend instructs. Edited // in place so the frontmatter (and its title) is present -- a bare row would carry no title and // sameFinding() would pass through its back-compat arm, never consulting identity at all. const deferredLedger = () => { const first = runShippedDisposition({ reviewText: REVIEW_1 }).ledger; assert.strictEqual(rowOf(first, 'WR-04').severity, 'critical', 'run 1 records the section severity'); const edited = first.replace(/^\| WR-04 \| critical \| open \| - \|$/m, '| WR-04 | critical | deferred | waiting on team A |'); assert.notStrictEqual(edited, first, 'the hand edit must have landed'); return edited; }; test("a deferred WR-* filed under '## Critical Issues' stays critical once the review stops reporting it", () => { const after = runShippedDisposition({ reviewText: REVIEW_2_DROPPED, priorText: deferredLedger() }); const row = rowOf(after.ledger, 'WR-04'); assert.strictEqual(row.severity, 'critical', 'the recorded severity survives the carry -- the prefix would say warning'); assert.strictEqual(row.disposition, 'deferred', 'and so does the decision'); assert.match(row.source, /^waiting on team A/, 'and the reason'); assert.match(row.source, /\(not in the current review\)$/, 'and the row is marked carried'); assert.match(after.ledger, /^ {4}severity: critical$/m, 'the frontmatter agrees with the table'); }); test('the recorded severity also outranks the prefix while the review still reports the finding under no recognized section', () => { // A re-review that dropped the documented headings. The current review gives no section, the // ledger remembers what the last one said, and that is a better source than the id alone. const plain = ['---', 'phase: 01', 'status: issues_found', '---', '', '### CR-01: properly numbered', '### WR-04: a critical the reviewer mis-numbered'].join('\n'); const after = runShippedDisposition({ reviewText: plain, priorText: deferredLedger() }); assert.strictEqual(rowOf(after.ledger, 'WR-04').severity, 'critical'); }); test("the current review's section still wins over the recorded value", () => { // Precedence control: a reviewer who re-tiers a finding on re-review is the newer statement. const retiered = ['---', 'phase: 01', 'status: issues_found', '---', '', '## Critical Issues', '', '### CR-01: properly numbered', '', '## Warnings', '', '### WR-04: a critical the reviewer mis-numbered'].join('\n'); const after = runShippedDisposition({ reviewText: retiered, priorText: deferredLedger() }); assert.strictEqual(rowOf(after.ledger, 'WR-04').severity, 'warning', 'the section is the current statement'); }); test('a REUSED id does not inherit the old finding\'s severity', () => { // Identity control, the same rule the disposition takes: ids are reused across re-reviews, so // a brand-new WR-04 under no section starts from its own prefix, not from the finding it // replaced. Inheriting here would put a Critical tier on an ordinary warning. const reused = ['---', 'phase: 01', 'status: issues_found', '---', '', '### CR-01: properly numbered', '### WR-04: an entirely different finding'].join('\n'); const after = runShippedDisposition({ reviewText: reused, priorText: deferredLedger() }); const row = rowOf(after.ledger, 'WR-04'); assert.strictEqual(row.severity, 'warning', 'a different finding under a reused id is inferred from its prefix'); assert.strictEqual(row.disposition, 'open', 'and, as before, does not inherit the decision either'); }); test('a hand-mangled Severity cell falls back to the frontmatter copy, and a mangled pair to the prefix', () => { // Both sources are enum-validated (ADR-227): a value outside critical|warning|info is not a // severity. The table is read first because it is the surface a human edits; the frontmatter // is the copy a human is not invited to touch. const base = deferredLedger(); const cellMangled = base.replace('| WR-04 | critical | deferred |', '| WR-04 | critcal | deferred |'); assert.notStrictEqual(cellMangled, base); let after = runShippedDisposition({ reviewText: REVIEW_2_DROPPED, priorText: cellMangled }); assert.strictEqual(rowOf(after.ledger, 'WR-04').severity, 'critical', 'the frontmatter still knows'); assert.strictEqual(rowOf(after.ledger, 'WR-04').disposition, 'deferred', 'a bad severity cell does not cost the decision'); const bothMangled = cellMangled.replace(/^( {2}- id: WR-04\n {4}severity: )critical$/m, '$1critcal'); assert.notStrictEqual(bothMangled, cellMangled, 'the frontmatter mangling must have landed'); after = runShippedDisposition({ reviewText: REVIEW_2_DROPPED, priorText: bothMangled }); assert.strictEqual(rowOf(after.ledger, 'WR-04').severity, 'warning', 'nothing recorded is usable, so the prefix is all that is left'); }); test('a pre-severity ledger (bare rows, no frontmatter) still infers from the prefix', () => { // Back-compat control. A ledger written by hand with no severity cell to speak of is not // rejected; it just has nothing to carry, so the prefix governs as it always did. const bare = '| WR-04 | | deferred | by hand |\n'; const after = runShippedDisposition({ reviewText: REVIEW_2_DROPPED, priorText: bare }); const row = rowOf(after.ledger, 'WR-04'); assert.strictEqual(row.severity, 'warning'); assert.strictEqual(row.disposition, 'deferred', 'an empty severity cell does not cost the decision'); }); }); describe('#3861 round 2 — a finding the heading parser cannot match is SURFACED, not dropped (B4)', () => { // Two independent parsers produce two numbers one paragraph apart: the counts come from // REVIEW.md's frontmatter, the rows from `### :` heading matches against a CLOSED // CR|BL|WR|IN alternation. Nothing reconciled them, so a finding the alternation cannot reach // contributed no row, no note and no diagnostic -- and the ledger asserted `open: N of N` over // a set strictly smaller than the console line had just reported. const REVIEW_5 = ['---', 'phase: 01', 'status: issues_found', 'findings:', ' critical: 1', ' warning: 2', ' info: 2', ' total: 5', '---', '', '### CR-01: a conforming finding', '### WR-01: another conforming one', '### WR-02: a third', '### SEC-01: a prefix the alternation does not carry', '#### IN-09: a heading one level too deep'].join('\n'); test('the shortfall is stated in the ledger frontmatter and on the console', () => { const out = runShippedDisposition({ reviewText: REVIEW_5, reviewTotal: 5 }); assert.match(out.ledger, /^unparsed: 2$/m, 'the ledger must record that two findings reached no row'); assert.match(out.ledger, /^total: 3$/m, 'and must still report the rows it does have'); assert.match(out.stdout, /2 finding\(s\) recorded NOWHERE/, 'the console line must say so too -- the ledger is not the only surface a human reads'); assert.match(out.stdout, /the review reports 5, but only 3 matched/, 'and must name both numbers, so the shortfall is checkable rather than asserted'); }); test('a review whose findings all parse gains no unparsed key at all', () => { // Negative control for the key itself. An ordinary ledger must not grow a noise key, or the // unchanged-run check starts rewriting the file on every phase. const clean = ['---', 'phase: 01', 'status: issues_found', 'findings:', ' critical: 1', ' warning: 0', ' info: 0', ' total: 1', '---', '', '### CR-01: the only finding'].join('\n'); const out = runShippedDisposition({ reviewText: clean, reviewTotal: 1 }); assert.doesNotMatch(out.ledger, /^unparsed:/m, 'nothing was dropped, so nothing is reported'); assert.doesNotMatch(out.stdout, /recorded NOWHERE/); }); test('an absent or non-numeric total reconciles nothing rather than inventing a shortfall', () => { // The reconciliation needs a number on BOTH sides. A legacy review with no findings: block // has no total to compare against, and reporting `unparsed: 3` there would be a fabrication. const legacy = ['---', 'phase: 01', 'status: issues_found', '---', '', '### CR-01: a', '### WR-01: b'].join('\n'); const out = runShippedDisposition({ reviewText: legacy, reviewTotal: '' }); assert.doesNotMatch(out.ledger, /^unparsed:/m); assert.match(out.ledger, /^total: 2$/m); }); test('a total SMALLER than the rows is not reported as a negative shortfall', () => { // Boundary in the other direction: the subtraction is clamped, so a review under-reporting // its own total cannot produce `unparsed: -1`. const out = runShippedDisposition({ reviewText: ['---', 'phase: 01', 'status: issues_found', 'findings:', ' total: 1', '---', '', '### CR-01: a', '### WR-01: b'].join('\n'), reviewTotal: 1, }); assert.doesNotMatch(out.ledger, /^unparsed:/m, 'no shortfall when more parsed than declared'); assert.doesNotMatch(out.ledger, /unparsed: -/); }); test('a review NONE of whose findings parse still records the shortfall on a first run', () => { // The corner every case above misses, and the one carrying the LEAST evidence anywhere else: // every finding in a heading shape the alternation cannot reach, on a phase with no prior // ledger and no fix report. `order` is empty, so the early exit that stands down for "nothing // to record" fired BEFORE the shortfall was computed -- no ledger, no console line, no // diagnostic, for a review that declared two Criticals. A PARTIAL shortfall always reported, // which is exactly why the total one read as covered. The shortfall is now derived above that // exit and both exits decline to fire while one is outstanding. const allUnmatched = ['---', 'phase: 01', 'status: issues_found', 'findings:', ' critical: 2', ' warning: 0', ' info: 0', ' total: 2', '---', '', '## Critical Issues', '', '### SEC-01: a prefix the alternation does not carry', '### SEC-02: and a second one'].join('\n'); const out = runShippedDisposition({ reviewText: allUnmatched, reviewTotal: 2 }); assert.notStrictEqual(out.ledger, null, 'a review whose findings NONE parsed must still leave a record — silence here is the exact silent drop this reconciliation exists to close'); assert.match(out.ledger, /^unparsed: 2$/m, 'and must state how many findings reached no row'); assert.match(out.ledger, /^total: 0$/m, 'while reporting honestly that it carries no rows'); assert.match(out.stdout, /2 finding\(s\) recorded NOWHERE/, 'the console line must say so too — the ledger is not the only surface a human reads'); }); test('a fix report present, nothing parsed, no prior ledger — the SECOND exit records it too', () => { // The two exits that discarded the shortfall are reached by DIFFERENT inputs, so one test // cannot pin both. The earlier one stands down as soon as a fix report exists; this input // therefore sails past it and lands on the later `rows.length === 0` return, which had the // identical hole. Without this case, deleting the later guard's conjunct leaves the pair // above green and the drop returns by the other road — a surviving mutant, not a covered one. const allUnmatched = ['---', 'phase: 01', 'status: issues_found', 'findings:', ' critical: 2', ' warning: 0', ' info: 0', ' total: 2', '---', '', '### SEC-01: a prefix the alternation does not carry', '### SEC-02: and a second one'].join('\n'); // The fix report must contribute NO row, or it never reaches the later exit: an id the // alternation CAN match becomes a carried row, rows.length is 1, and the guard under test is // not the one that decides. Mutation-controlled — with a CR-NN id here the later guard's // conjunct could be deleted and this test stayed green. const fixReport = ['## Fixed Issues', '', '### SEC-03: an unmatched id in the fix report too'].join('\n'); const out = runShippedDisposition({ reviewText: allUnmatched, fixText: fixReport, reviewTotal: 2 }); assert.notStrictEqual(out.ledger, null, 'a fix report does not excuse the drop — the shortfall is still two findings recorded nowhere'); assert.match(out.ledger, /^unparsed: 2$/m, 'and the count must survive this path too'); }); test('a genuinely clean review still writes nothing — the relaxed exit is scoped to a shortfall', () => { // Negative control for the fix itself, and the reason it is scoped rather than removed: // relaxing that exit unconditionally would grow a zero-row ledger on every clean phase. With // `total: 0` there is no shortfall to outstand, so both exits still fire exactly as before. const clean = ['---', 'phase: 01', 'status: clean', 'findings:', ' critical: 0', ' warning: 0', ' info: 0', ' total: 0', '---', '', 'No issues.'].join('\n'); const out = runShippedDisposition({ reviewText: clean, reviewTotal: 0 }); assert.strictEqual(out.ledger, null, 'no findings declared and none parsed — nothing to record'); assert.strictEqual(out.stdout.trim(), '', 'and nothing to say'); }); test('a clean review with an unparseable fix report still writes nothing — the later exit must keep firing', () => { // The fourth cell of the matrix, and the only one the three tests above leave open: they pin // the LATER exit's under-fire (it must not swallow a shortfall) and the EARLIER exit's // over-fire (a clean review must stay unrecorded), but nothing pins the later exit's OWN // over-fire. It is reachable because a fix report — even one naming no matchable id — makes // the earlier exit stand down, so control arrives at the later exit with a genuinely clean // review and no shortfall. Neutering only that exit then writes a zero-row ledger reading // "0 of 0 finding(s) open" for a phase that had nothing to report, and all three tests above // stay green through it. const clean = ['---', 'phase: 01', 'status: clean', 'findings:', ' critical: 0', ' warning: 0', ' info: 0', ' total: 0', '---', '', 'No issues.'].join('\n'); const unparseableFix = ['## Fixed Issues', '', '### SEC-03: an unmatched id in the fix report'].join('\n'); const out = runShippedDisposition({ reviewText: clean, fixText: unparseableFix, reviewTotal: 0 }); assert.strictEqual(out.ledger, null, 'a fix report that decides nothing is not a reason to record a ledger for a clean review'); assert.strictEqual(out.stdout.trim(), '', 'and nothing to say about it'); }); }); describe('#3829 review round 2 — a review that reports nothing still reconciles the ledger', () => { const EMPTY_REVIEW = ['---', 'phase: 01', 'status: issues_found', '---', '', 'no findings'].join('\n'); test('a decided row is carried when the review reports no findings at all', () => { // Exiting early on an empty review would freeze a stale ledger showing findings as open that // the review no longer reports — the opposite of what this record exists to do. const priorText = [ '| CR-01 | critical | deferred | ships next phase |', '| WR-01 | warning | open | - |', ].join('\n'); const d = buildDisposition({ reviewText: EMPTY_REVIEW, priorText, padded: '01' }); // M1: BOTH rows survive. The decided one because it was triaged, the untriaged one because // it was not -- and an untriaged finding vanishing without trace is the defect #3829 names. assert.strictEqual(d.total, 2, 'both rows survive; neither is silently deleted'); assert.deepStrictEqual(d.rows.map((r) => r.id), ['CR-01', 'WR-01']); assert.strictEqual(d.rows[0].disposition, 'deferred'); assert.strictEqual(d.rows[0].carried, true); assert.strictEqual(d.rows[1].disposition, 'open'); assert.strictEqual(d.rows[1].carried, true); assert.strictEqual(d.open, 1, 'the untriaged carried row is still open, and the count says so'); }); test('a review with no findings and no prior ledger produces nothing at all', () => { assert.strictEqual(buildDisposition({ reviewText: EMPTY_REVIEW, padded: '01' }), null); }); // Ninth src.includes() retired (round 3): it pinned the guard's exact line, including the // process.exit(0) the script no longer calls, so it went red on a change that left the property // untouched. The property -- an empty review still reconciles an EXISTING ledger -- is asserted // behaviourally by the round-2 describe '#3829 review round 2 — a review that reports nothing // still reconciles the ledger' below, which drives the shipped script against a prior ledger. }); // --------------------------------------------------------------------------- // #3829 — the SHIPPED disposition script, driven. These tests execute the real // embedded script (see shippedDispositionScript), so a regression in the // workflow file turns them red. The mirror-based tests above are kept for the // pure parsing shapes; these are the ones that hold the contract. // --------------------------------------------------------------------------- describe('#3829 — shipped disposition script (executed, not mirrored)', () => { const REVIEW = ['---', 'phase: 01', 'status: issues_found', '---', '', '### CR-01: a', '', '### WR-01: b', '', '### IN-01: c'].join('\n'); test('every finding is recorded, defaulting to open, at the right severity', () => { const rows = ledgerRows(runShippedDisposition({ reviewText: REVIEW }).ledger); assert.deepStrictEqual(rows.map((r) => [r.id, r.severity, r.disposition]), [ ['CR-01', 'critical', 'open'], ['WR-01', 'warning', 'open'], ['IN-01', 'info', 'open'], ]); }); test('--fix outcomes are reconciled, and a lookalike section heading is not one', () => { const fixText = ['## Fixed Issues', '', '### WR-01: b', '', '## Fixed Issues Verification', '', '### IN-01: c'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: REVIEW, fixText }).ledger); const by = Object.fromEntries(rows.map((r) => [r.id, r])); assert.strictEqual(by['WR-01'].disposition, 'fixed'); assert.strictEqual(by['WR-01'].source, '01-REVIEW-FIX.md'); assert.strictEqual(by['IN-01'].disposition, 'open', 'a lookalike heading must classify nothing'); }); test("a human's deferral reason survives, and the carried marker never accumulates", () => { // Render -> re-parse -> render, four times, exactly as consecutive phase runs would. const shrunk = ['---', 'status: issues_found', '---', '', '### WR-01: b'].join('\n'); let prior = ['| Finding | Severity | Disposition | Source |', '|---------|----------|-------------|--------|', '| CR-01 | critical | deferred | ships next phase, see ADR-99 |'].join('\n'); let ledger; for (let i = 0; i < 4; i++) { ledger = runShippedDisposition({ reviewText: shrunk, priorText: prior }).ledger; prior = ledger; } const cr = ledgerRows(ledger).find((r) => r.id === 'CR-01'); assert.strictEqual(cr.disposition, 'deferred'); assert.strictEqual(cr.source, 'ships next phase, see ADR-99 (not in the current review)'); assert.strictEqual( (ledger.match(/\(not in the current review\)/g) || []).length, 1, 'the carried marker must appear exactly once however many times the gate runs' ); }); test('the ledger is a fixed point: a run that changes nothing rewrites nothing', () => { const first = runShippedDisposition({ reviewText: REVIEW }).ledger; const again = runShippedDisposition({ reviewText: REVIEW, priorText: first }); assert.strictEqual(again.wroteNothing, true, 'an unchanged run must report unchanged'); assert.strictEqual(again.ledger, first, 'and must leave the bytes alone'); }); test('a finding the review no longer reports is carried whether or not it was triaged', () => { const prior = ['| CR-01 | critical | deferred | later |', '| IN-01 | info | open | - |'].join('\n'); const shrunk = ['---', 'status: issues_found', '---', '', '### WR-01: b'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: shrunk, priorText: prior }).ledger); const ids = rows.map((r) => r.id); assert.ok(ids.includes('CR-01'), 'a deferred finding must survive leaving the review'); assert.ok(ids.includes('IN-01'), 'and so must an untriaged one -- that is the whole record'); }); test('a renumbered finding leaves its old row behind, marked, rather than vanishing', () => { // The concrete case M1 names: run 1 records CR-01 open, the re-review renumbers it to CR-02, // and run 2's ledger used to contain neither. The old row is now carried and marked, so the // double-count is legible rather than a silent delete -- the stated cost of the fix. const prior = '| CR-01 | critical | open | - |'; const renumbered = ['---', 'status: issues_found', '---', '', '### CR-02: the same finding, renumbered'].join('\n'); const ledger = runShippedDisposition({ reviewText: renumbered, priorText: prior }).ledger; const rows = ledgerRows(ledger); assert.deepStrictEqual(rows.map((r) => r.id), ['CR-02', 'CR-01']); assert.match(ledger, /\| CR-01 \|.*\(not in the current review\) \|/, 'the carried row is MARKED, so it does not claim the finding is live'); }); test('a review reporting nothing still reconciles an existing ledger', () => { const empty = ['---', 'status: issues_found', '---', '', 'no findings'].join('\n'); const prior = ['| CR-01 | critical | deferred | later |', '| WR-01 | warning | open | - |'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: empty, priorText: prior }).ledger); assert.deepStrictEqual(rows.map((r) => r.id), ['CR-01', 'WR-01'], 'reconciled, not truncated'); }); test('a review reporting nothing with no prior ledger writes no ledger at all', () => { const empty = ['---', 'status: issues_found', '---', '', 'no findings'].join('\n'); assert.strictEqual(runShippedDisposition({ reviewText: empty }).ledger, null); }); test('an applied outcome outranks an earlier deferral', () => { const prior = '| CR-01 | critical | deferred | later |'; const fixText = ['## Fixed Issues', '', '### CR-01: a'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: REVIEW, priorText: prior, fixText }).ledger); assert.strictEqual(rows.find((r) => r.id === 'CR-01').disposition, 'fixed'); }); test('a deferral on a finding STILL in the review is not reset to open', () => { // Distinct from the carried case: this finding is present in the current review, so it takes // the ordinary path. 'open' must never overwrite a recorded decision on that path either. const prior = '| CR-01 | critical | deferred | ships next phase |'; const rows = ledgerRows(runShippedDisposition({ reviewText: REVIEW, priorText: prior }).ledger); const cr = rows.find((r) => r.id === 'CR-01'); assert.strictEqual(cr.disposition, 'deferred'); assert.strictEqual(cr.source, 'ships next phase'); assert.ok(!/not in the current review/.test(cr.source), 'it is present, so it is not carried'); }); test('BL- is recorded at the Critical tier, as the documented CR- equivalent', () => { const withBlocker = ['---', 'status: issues_found', '---', '', '### BL-01: a', '', '### WR-01: b'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: withBlocker }).ledger); assert.deepStrictEqual(rows.map((r) => [r.id, r.severity]), [ ['BL-01', 'critical'], ['WR-01', 'warning'], ]); }); test('a finding id whose prefix is a JS object property name produces no row', () => { const hostile = ['---', 'status: issues_found', '---', '', '### constructor-01: x', '', '### __proto__-02: y', '', '### CR-01: real'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: hostile }).ledger); assert.deepStrictEqual(rows.map((r) => r.id), ['CR-01']); }); }); describe('#3829 review round 3 — hostile frontmatter and hand-edited ledgers', () => { test('an UNTERMINATED frontmatter block yields no values, not the whole review body', () => { // Stopping at "the next ---" is not enough: with no closing delimiter the scan would run to // EOF and hand body text to every read, undoing the scoping fix entirely. const unterminated = [ '---', 'phase: 01', 'status: issues_found', '', '# body', '', 'total: 777', 'critical: 66', ].join('\n'); const parsed = parseGateCounts(unterminated); assert.deepStrictEqual(parsed, { status: '', critical: '', warning: '', info: '', total: '' }); }); test('docs-parity: the frontmatter scan emits only when the closing delimiter was seen', () => { const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8'); assert.ok( src.includes('END{if (closed) printf "%s", buf}'), 'an unterminated frontmatter block must yield nothing' ); }); test('a hand-edited row missing its trailing pipe still preserves the decision', () => { // A mangled table is already broken; silently dropping the row would lose a deferral, which // is the exact class of loss this record exists to prevent. const review = ['---', 'status: issues_found', '---', '', '### CR-01: a'].join('\n'); const prior = '| CR-01 | critical | deferred | see issue 42'; const rows = ledgerRows(runShippedDisposition({ reviewText: review, priorText: prior }).ledger); const cr = rows.find((r) => r.id === 'CR-01'); assert.strictEqual(cr.disposition, 'deferred'); assert.strictEqual(cr.source, 'see issue 42'); }); }); describe('#3829 review round 3 — stale fix reports, fenced examples, hostile REVIEW.md', () => { test('a STALE fix report does not mark a new finding of the same id as fixed', () => { // Finding ids are reused across re-reviews. Matching on the id alone would let a fix report // from an earlier review declare a brand-new CR-01 already fixed — the worst possible lie // for a record whose whole job is saying what happened to a finding. const review = ['---', 'status: issues_found', '---', '', '### CR-01: NEW authentication bypass'].join('\n'); const fixText = ['## Fixed Issues', '', '### CR-01: OLD null dereference'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: review, fixText }).ledger); assert.strictEqual(rows[0].disposition, 'open'); }); test('a fix report naming the same finding still applies', () => { const review = ['---', 'status: issues_found', '---', '', '### CR-01: same title'].join('\n'); const fixText = ['## Fixed Issues', '', '### CR-01: same title'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: review, fixText }).ledger); assert.strictEqual(rows[0].disposition, 'fixed'); }); test('a finding heading inside a fenced block is an example, not a finding', () => { const review = ['---', 'status: issues_found', '---', '', '### CR-01: real', '', '```', '### CR-77: an illustration', '```'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: review }).ledger); assert.deepStrictEqual(rows.map((r) => r.id), ['CR-01']); }); test('an id listed under BOTH Fixed and Skipped is not decided by row order', () => { const review = ['---', 'status: issues_found', '---', '', '### WR-01: dup'].join('\n'); const fixText = ['## Fixed Issues', '', '### WR-01: dup', '', '## Skipped Issues', '', '### WR-01: dup'].join('\n'); const rows = ledgerRows(runShippedDisposition({ reviewText: review, fixText }).ledger); assert.strictEqual(rows[0].disposition, 'fixed', 'first occurrence wins, deterministically'); }); test('an escaped pipe in a deferral reason survives whole', () => { const review = ['---', 'status: issues_found', '---', '', '### CR-01: a'].join('\n'); const prior = '| CR-01 | critical | deferred | wait \\| see ADR-9 |'; const rows = ledgerRows(runShippedDisposition({ reviewText: review, priorText: prior }).ledger); assert.strictEqual(rows[0].source, 'wait \\| see ADR-9'); }); // #3861 round 3. The Source cell is the one field this ledger asks a human to hand-edit, and // "waiting on team A | team B to align" is an ordinary thing to type there. Under the previous // capture a bare | failed the WHOLE prior-row match: prior.get() was undefined, the row fell // through to open with an empty Source, and the console line read "1 of 1 finding(s) open" -- // a Critical a human explicitly deferred, with a documented reason, rendered indistinguishable // from one never triaged, and the reason gone. Exactly the "was this ever seen" ambiguity // #3829 exists to remove, reachable by one missing backslash. The cell is the LAST column, so // it is now captured to the end of the line and a bare pipe is prose; the render escapes it // so the table stays a table, and the second run converges. test('a bare pipe in a deferral reason is kept as prose: the decision and the reason survive', () => { const review = ['---', 'status: issues_found', '---', '', '### CR-01: a'].join('\n'); const prior = '| CR-01 | critical | deferred | waiting on team A | team B to align |'; const first = runShippedDisposition({ reviewText: review, priorText: prior }); const rows = ledgerRows(first.ledger); assert.strictEqual(rows[0].disposition, 'deferred', 'a bare pipe must not revert the decision'); assert.strictEqual(rows[0].source, 'waiting on team A \\| team B to align', 'the reason survives, escaped'); assert.match(first.ledger, /^open: 0$/m, 'and the headline count agrees with the row'); // Fixed point: the escaped form re-parses to itself, so the second run rewrites nothing. const second = runShippedDisposition({ reviewText: review, priorText: first.ledger }); assert.strictEqual(ledgerRows(second.ledger)[0].source, 'waiting on team A \\| team B to align'); assert.match(second.stdout, /disposition unchanged/, 'the second run must converge'); }); // Round-3 adversarial pass on the fix above. The first escape used /(^|[^\\])\|/g, which CONSUMES // the character before the pipe: adjacent bare pipes were escaped one per run (A||B -> A\||B -> // A\|\|B, a third run to converge, breaking the advertised second-run fixed point), and an escaped // backslash before a pipe (A\\|B) hid the pipe behind the wrong parity and left it bare in the // rendered table. The generator then emitted at most one bare pipe, so no property reached either; it does now. // Round-3 adversarial pass. The script printed its verdict and then called process.exit(0) on the // 'unchanged' branch only. Node documents process.stdout writes to pipes as ASYNCHRONOUS on // POSIX, and an explicit exit can pre-empt a pending write, so on those lanes the caller can see // exit 0 with no verdict line -- a hardening, not a reproduced defect: the reviewer's empty-stdout // observation turned out to be its own sandbox (a bare console.log child printed nothing there // either), which is stated so nobody re-reads this as evidence the drop was seen. The script now // runs inside main() and leaves by return, so the loop drains stdout before exit. // Shape-pinned deliberately: the property IS the absence of the call, and a behavioural test would // have to race a pipe to fail. Comments are stripped first so a mention is not a match, and the // match covers the dotted, bracketed and whitespace-split spellings; a call built by any other // indirection is outside this pin and is what code review is for. test('the shipped script never calls process.exit -- it returns, so its verdict line is never lost', () => { const code = shippedDispositionScript().replace(/^\s*\/\/.*$/gm, ''); assert.doesNotMatch(code, /process\s*(?:\.\s*exit\b|\[\s*['"]exit['"]\s*\])/, 'leave main() by return; an explicit exit can drop the verdict line on a piped stdout'); }); test('adjacent bare pipes and a backslash-then-pipe are escaped in ONE write, then converge', () => { const review = ['---', 'status: issues_found', '---', '', '### CR-01: a'].join('\n'); const prior = '| CR-01 | critical | deferred | A||B and C\\\\|D |'; const first = runShippedDisposition({ reviewText: review, priorText: prior }); assert.strictEqual(ledgerRows(first.ledger)[0].source, 'A\\|\\|B and C\\\\\\|D', 'every bare pipe is escaped on the first write, whatever precedes it'); const second = runShippedDisposition({ reviewText: review, priorText: first.ledger }); assert.strictEqual(second.ledger, first.ledger, 'and the escaped form is a fixed point'); assert.match(second.stdout, /disposition unchanged/); }); }); // --------------------------------------------------------------------------- // #4665 — `--fix` must be able to act on an existing REVIEW.md even when the // incremental scope for a FRESH review is empty. check_empty_scope used to // exit the entire workflow whenever REVIEW_FILES was empty — before // dispatch-fix — so the documented contract ("after review completes (or if // REVIEW.md already exists), auto-apply findings found") was unreachable for // any phase whose post-review changes are planning artifacts only. // --------------------------------------------------------------------------- describe('#4665 — check_empty_scope --fix recovery onto an existing REVIEW.md', () => { const readWorkflow = () => fs.readFileSync(WORKFLOW_PATH, 'utf8'); test('check_empty_scope recovers --fix onto an existing REVIEW.md instead of exiting (#4665)', () => { const src = readWorkflow(); const stepIdx = src.indexOf(''); const nextStepIdx = src.indexOf(''); assert.ok(stepIdx !== -1, 'check_empty_scope step must exist'); assert.ok(nextStepIdx > stepIdx, 'structural_pre_pass must follow check_empty_scope'); const block = src.slice(stepIdx, nextStepIdx); // The recovery computes the phase's REVIEW.md path with the same // expression spawn_reviewer uses. assert.ok( block.includes('REVIEW_PATH="${PHASE_DIR}/${PADDED_PHASE}-REVIEW.md"'), 'check_empty_scope must compute REVIEW_PATH exactly as spawn_reviewer does' ); // The skip is now guarded: it fires only when --fix is absent OR no // REVIEW.md exists on disk. assert.ok( block.includes('FIX_FLAG') && block.includes('-f "${REVIEW_PATH}"'), 'the empty-scope skip must be guarded on FIX_FLAG and the existing REVIEW.md file check' ); // The fence must be self-contained about its own precondition: an explicit // emptiness check, so a literal-minded execution cannot read the recovery // paragraph as skipping a needed fresh review on a non-empty scope. assert.ok( block.includes("\"${#REVIEW_FILES[@]}\" -ne 0"), 'check_empty_scope must assert REVIEW_FILES emptiness explicitly, not only in prose' ); }); test('the fix-recovery path routes to dispatch-fix past the fresh-review steps (#4665)', () => { const src = readWorkflow(); const stepIdx = src.indexOf(''); const nextStepIdx = src.indexOf(''); const block = src.slice(stepIdx, nextStepIdx); assert.match(block, /dispatch-fix/, 'the recovery must route to dispatch-fix'); assert.match( block, /structural_pre_pass[\s\S]*dispatch_reviewer_lanes[\s\S]*spawn_reviewer[\s\S]*commit_review/, 'the recovery must name the fresh-review steps it skips (no reviewer spawn, nothing to commit)' ); assert.doesNotMatch( block, /spawn the (reviewer|agent)|msd-code-reviewer/, 'the recovery must not contain an instruction to spawn a fresh reviewer' ); }); test('the plain empty-scope skip survives for the non-fix path (#4665)', () => { const src = readWorkflow(); const stepIdx = src.indexOf(''); const nextStepIdx = src.indexOf(''); const block = src.slice(stepIdx, nextStepIdx); assert.ok( block.includes('No source files changed in phase ${PHASE_ARG}. Skipping review.'), 'the plain skip text must remain for invocations without --fix or without an existing REVIEW.md' ); assert.match(block, /Exit workflow/i, 'the non-recovery path must still end the workflow'); }); }); // --------------------------------------------------------------------------- // #3861 round 1 — the two blockers, and the structural properties that catch them // // Both were invisible to every behavioural test above, for the same reason: those // tests execute the node script through the process seam with the environment // handed to it, so they never see the SHELL that is supposed to build that // environment, nor the prose that tells the agent what to read. // --------------------------------------------------------------------------- // Every ```bash fence in a step file, in order. function bashFences(src) { const out = []; const lines = src.replace(/\r\n/g, '\n').split('\n'); let start = -1; for (let i = 0; i < lines.length; i++) { if (start === -1 && /^```bash\s*$/.test(lines[i])) { start = i + 1; continue; } if (start !== -1 && /^```\s*$/.test(lines[i])) { out.push(lines.slice(start, i).join('\n')); start = -1; } } return out; } describe('#3861 round 1 — step-file structural contract', () => { test('the step file never instructs the agent to read and execute ITSELF', () => { // A step file that names its own path as something to "read and execute" is // unbounded self-recursion at runtime, and nothing downstream bounds it. const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8'); const self = 'execute-phase/steps/' + path.basename(DISPOSITION_STEP_PATH); assert.ok( !src.includes(self), 'code-review-disposition.md must not reference its own path — execute-phase.md is what points here' ); }); test('the disposition instruction appears exactly once', () => { // The self-reference above arrived as a duplicated paragraph; the duplicate is // the tell, and no linter scores markdown prose. const src = fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8'); const n = src.split(/\r?\n/).filter((l) => l.startsWith('**Record a per-finding disposition.**')).length; assert.strictEqual(n, 1, 'exactly one disposition instruction, not a pointer plus a body'); }); test('every shell block derives the variables it reads — blocks do not share a shell', () => { // Each fenced block is dispatched as its own Bash call, so a variable derived in // block 1 is EMPTY in block 2 — and empty is silent: the write lands on a bare // `-REVIEW-DISPOSITION.md` and the step still reports success. The step's own // inputs (PHASE_DIR, PHASE_NUMBER) are the only values a block may inherit. const INPUTS = new Set(['PHASE_DIR', 'PHASE_NUMBER']); const DERIVED = ['PADDED', 'REVIEW_FILE', 'DISPOSITION_FILE']; const fences = bashFences(fs.readFileSync(DISPOSITION_STEP_PATH, 'utf8')); assert.ok(fences.length >= 2, 'the step must still carry more than one shell block'); for (const [i, fence] of fences.entries()) { for (const v of DERIVED) { if (INPUTS.has(v)) continue; // The derivation may be INDENTED or sit inside a `case` arm -- PADDED is derived by a // case split so a dotted phase number does not reach `printf %02d`. Anchoring the // detector at column 0 made a real derivation invisible and the guard fired on it. The // property being checked is unchanged: the block must ASSIGN what it reads. // A SELF-REFERENTIAL assignment is a PASS-THROUGH, not a derivation. Block 2 prefixes its // `node -e` with `REVIEW_FILE="${REVIEW_FILE}" DISPOSITION_FILE="${DISPOSITION_FILE}" ...` // to put them in the child's environment, and the detector counted that as deriving them. // So deleting block 2's REAL derivation left this guard GREEN -- on the exact defect it // was written for. Found by negative-controlling the guard rather than by reading it. // (Pre-existing: the original column-0 anchor matched that same line, which is at column 0.) const fenceNoPassthrough = fence.replace( new RegExp('(?:^|[\\s;])' + v + '="\\$\\{' + v + '\\}"', 'gm'), ' '); // Non-empty RHS required. `REVIEW_FILE=` is an assignment TOKEN and not a derivation, and // the reviewer evaded the earlier predicate with exactly that. This is a cheap fast-fail, // NOT the guard's authority -- see the executed guard below, which is. const assigned = new RegExp('(?:^|[\\s;])' + v + '=(?![\\s;#]|$)', 'm'); const reads = new RegExp('\\$\\{?' + v + '\\b').test(fence.replace(new RegExp('(?:^|[\\s;])' + v + '=', 'gm'), '')); if (!reads) continue; assert.ok( assigned.test(fenceNoPassthrough), 'block ' + (i + 1) + ' reads ' + v + ' without deriving it — it is empty in a fresh shell' ); } } }); }); describe('#3861 round 16 — the embedded script fits a Windows command line', () => { // Block 2 runs the record-builder as `node -e "