diff --git a/eslint-rules/lib/readfilesync-trace.cjs b/eslint-rules/lib/readfilesync-trace.cjs new file mode 100644 index 000000000..d10764c2d --- /dev/null +++ b/eslint-rules/lib/readfilesync-trace.cjs @@ -0,0 +1,172 @@ +'use strict'; + +/** + * readfilesync-trace.cjs + * + * Shared data-flow tracing for "is this AST node derived from a readFileSync + * call" and "is this RegExpLiteral applied to a readFileSync-derived + * receiver" — extracted from `no-crlf-fragile-split.cjs` (ADR-1703 Phase 4) + * so `no-unbounded-quantifier.cjs` (ADR-3212 §5/§7, epic #3212 Phase 4, + * #3415) does not carry a second, divergence-prone copy of the same ~80-line + * scope walk. Both rules import this module; a parity test + * (tests/readfilesync-trace-parity.test.cjs) asserts they classify a shared + * fixture set identically. + * + * Byte-identical logic to the pre-extraction copy in `no-crlf-fragile-split.cjs` + * — this is a pure relocation, not a rewrite (ADR-3212 §6, extend-never-mutate). + */ + +/** + * Returns true if the node is a call to `readFileSync` or `fs.readFileSync`. + * @param {import('eslint').Rule.Node} node + * @returns {boolean} + */ +function isReadFileSyncCall(node) { + if (!node || node.type !== 'CallExpression') return false; + const callee = node.callee; + if (callee.type === 'Identifier' && callee.name === 'readFileSync') return true; + if ( + callee.type === 'MemberExpression' && + !callee.computed && + callee.property.type === 'Identifier' && + callee.property.name === 'readFileSync' + ) { + return true; + } + return false; +} + +/** + * Given an Identifier node, walk the scope chain to find its binding, then + * check if the initializer is derived from readFileSync. + * @param {import('eslint').Rule.Node} identNode + * @param {import('eslint').SourceCode} sourceCode + * @returns {boolean} + */ +function resolveIdentifierToReadFileSync(identNode, sourceCode) { + if (typeof sourceCode.getScope !== 'function') return false; + + let scope; + try { + scope = sourceCode.getScope(identNode); + } catch (_) { + // If scope resolution fails (e.g. due to unsupported node type or parser + // version mismatch), conservatively return false (not flagged) — an + // unresolvable scope produces a false negative rather than a spurious error. + return false; + } + if (!scope) return false; + + let s = scope; + while (s) { + const variable = s.variables.find((v) => v.name === identNode.name); + if (variable) { + const defs = variable.defs; + if (!defs || defs.length === 0) return false; + const decl = defs[0].node; // VariableDeclarator + if (!decl || !decl.init) return false; + return isReadFileSyncDerived(decl.init, sourceCode); + } + s = s.upper; + } + return false; +} + +/** + * Returns true if `node` is (transitively) derived from a readFileSync call. + * + * Handles: + * - Direct: readFileSync(...) -- the node itself IS the readFileSync call + * - Chain: readFileSync(...).toString() etc. + * - Identifier resolved via scope to a variable initialized from readFileSync + * + * @param {import('eslint').Rule.Node} node + * @param {import('eslint').SourceCode} sourceCode + * @returns {boolean} + */ +function isReadFileSyncDerived(node, sourceCode) { + if (!node) return false; + + if (isReadFileSyncCall(node)) return true; + + if (node.type === 'MemberExpression') { + return isReadFileSyncDerived(node.object, sourceCode); + } + + if (node.type === 'CallExpression') { + if (isReadFileSyncCall(node)) return true; + if (node.callee.type === 'MemberExpression') { + return isReadFileSyncDerived(node.callee.object, sourceCode); + } + } + + if (node.type === 'Identifier') { + return resolveIdentifierToReadFileSync(node, sourceCode); + } + + return false; +} + +/** + * Returns true if a pattern-bearing node (RegExpLiteral, or the callee-object + * position for `/regex/.test(str)`) is used on a readFileSync-derived + * receiver via .match/.test/.exec/.replace/.replaceAll/.split/.matchAll. + * + * Two AST shapes: + * Shape A: str.match(/regex/) — regex is an ARG to the call. + * regex.parent = CallExpression (arg), callee.object = str + * Shape B: /regex/.test(str) — regex is the callee object. + * regex.parent = MemberExpression (the .test callee) + * regex.parent.parent = CallExpression, first arg = str + * + * @param {import('eslint').Rule.Node} regexNode — the RegExpLiteral (or `new RegExp(...)` CallExpression) + * @param {import('eslint').SourceCode} sourceCode + * @returns {boolean} + */ +function isPatternUsedOnFileContent(regexNode, sourceCode) { + const FILE_METHODS = new Set(['match', 'test', 'exec', 'replace', 'replaceAll', 'split', 'matchAll']); + const parent = regexNode.parent; + if (!parent) return false; + + // Shape A: str.match(regex) — regex is an argument; parent is CallExpression + if (parent.type === 'CallExpression') { + const callee = parent.callee; + if ( + callee && + callee.type === 'MemberExpression' && + !callee.computed && + callee.property.type === 'Identifier' && + FILE_METHODS.has(callee.property.name) + ) { + if (parent.arguments.includes(regexNode)) { + return isReadFileSyncDerived(callee.object, sourceCode); + } + } + return false; + } + + // Shape B: /regex/.test(str) — regex is the callee object. + if (parent.type === 'MemberExpression' && !parent.computed) { + if ( + parent.object === regexNode && + parent.property.type === 'Identifier' && + FILE_METHODS.has(parent.property.name) + ) { + const callExpr = parent.parent; + if (callExpr && callExpr.type === 'CallExpression' && callExpr.callee === parent) { + const args = callExpr.arguments; + if (args && args.length > 0) { + return isReadFileSyncDerived(args[0], sourceCode); + } + } + } + } + + return false; +} + +module.exports = { + isReadFileSyncCall, + isReadFileSyncDerived, + isPatternUsedOnFileContent, +}; diff --git a/eslint-rules/no-crlf-fragile-split.cjs b/eslint-rules/no-crlf-fragile-split.cjs index fdd4443c7..0c6c8f234 100644 --- a/eslint-rules/no-crlf-fragile-split.cjs +++ b/eslint-rules/no-crlf-fragile-split.cjs @@ -39,6 +39,7 @@ */ const { isWindowsExcludedNode } = require('./lib/platform-guard.cjs'); +const { isReadFileSyncDerived, isPatternUsedOnFileContent } = require('./lib/readfilesync-trace.cjs'); /** @type {import('eslint').Rule.RuleModule} */ const rule = { @@ -79,103 +80,6 @@ const rule = { return null; } - /** - * Returns true if the node is a call to `readFileSync` or `fs.readFileSync`. - * @param {import('eslint').Rule.Node} node - * @returns {boolean} - */ - function isReadFileSyncCall(node) { - if (!node || node.type !== 'CallExpression') return false; - const callee = node.callee; - // readFileSync(...) - if (callee.type === 'Identifier' && callee.name === 'readFileSync') return true; - // fs.readFileSync(...) - if ( - callee.type === 'MemberExpression' && - !callee.computed && - callee.property.type === 'Identifier' && - callee.property.name === 'readFileSync' - ) { - return true; - } - return false; - } - - /** - * Returns true if `node` is (transitively) derived from a readFileSync call. - * - * Handles: - * - Direct: readFileSync(...) -- the node itself IS the readFileSync call - * - Chain: readFileSync(...).toString() etc. - * - Identifier resolved via scope to a variable initialized from readFileSync - * - * @param {import('eslint').Rule.Node} node - * @returns {boolean} - */ - function isReadFileSyncDerived(node) { - if (!node) return false; - - // Direct readFileSync call - if (isReadFileSyncCall(node)) return true; - - // MemberExpression: x.something — check the object - if (node.type === 'MemberExpression') { - return isReadFileSyncDerived(node.object); - } - - // CallExpression: x.something() — check object of the callee - if (node.type === 'CallExpression') { - if (isReadFileSyncCall(node)) return true; - if (node.callee.type === 'MemberExpression') { - return isReadFileSyncDerived(node.callee.object); - } - } - - // Identifier: resolve to its variable initializer via scope - if (node.type === 'Identifier') { - return resolveIdentifierToReadFileSync(node); - } - - return false; - } - - /** - * Given an Identifier node, walk the scope chain to find its binding, - * then check if the initializer is derived from readFileSync. - * @param {import('eslint').Rule.Node} identNode - * @returns {boolean} - */ - function resolveIdentifierToReadFileSync(identNode) { - if (typeof sourceCode.getScope !== 'function') return false; - - let scope; - try { - scope = sourceCode.getScope(identNode); - } catch (_) { - // If scope resolution fails (e.g. due to unsupported node type or - // parser version mismatch), conservatively return false (not flagged). - // This is an intentional boundary: an unresolvable scope produces a - // false negative rather than a spurious error. - return false; - } - if (!scope) return false; - - let s = scope; - while (s) { - const variable = s.variables.find(v => v.name === identNode.name); - if (variable) { - const defs = variable.defs; - if (!defs || defs.length === 0) return false; - const decl = defs[0].node; // VariableDeclarator - if (!decl || !decl.init) return false; - // Check the init is readFileSync-derived - return isReadFileSyncDerived(decl.init); - } - s = s.upper; - } - return false; - } - /** * Returns true if a RegExpLiteral has at least one FRAGILE bare \n — a \n * that is not adequately protected against CRLF. @@ -283,48 +187,7 @@ const rule = { * @returns {boolean} */ function isRegexUsedOnFileContent(regexNode) { - const FILE_METHODS = new Set(['match', 'test', 'exec', 'replace', 'replaceAll', 'split', 'matchAll']); - const parent = regexNode.parent; - if (!parent) return false; - - // Shape A: str.match(regex) — regex is an argument; parent is CallExpression - if (parent.type === 'CallExpression') { - const callee = parent.callee; - if ( - callee && - callee.type === 'MemberExpression' && - !callee.computed && - callee.property.type === 'Identifier' && - FILE_METHODS.has(callee.property.name) - ) { - // regex must actually be one of the arguments (not the callee) - if (parent.arguments.includes(regexNode)) { - return isReadFileSyncDerived(callee.object); - } - } - return false; - } - - // Shape B: /regex/.test(str) — regex is the callee object. - // In this case, regexNode.parent is the MemberExpression (/regex/.test) - if (parent.type === 'MemberExpression' && !parent.computed) { - if ( - parent.object === regexNode && - parent.property.type === 'Identifier' && - FILE_METHODS.has(parent.property.name) - ) { - // parent.parent should be the CallExpression - const callExpr = parent.parent; - if (callExpr && callExpr.type === 'CallExpression' && callExpr.callee === parent) { - const args = callExpr.arguments; - if (args && args.length > 0) { - return isReadFileSyncDerived(args[0]); - } - } - } - } - - return false; + return isPatternUsedOnFileContent(regexNode, sourceCode); } /** @@ -372,7 +235,7 @@ const rule = { const argVal = stringValue(args[0]); if (argVal === '\n') { // Is the receiver derived from readFileSync? - if (isReadFileSyncDerived(callee.object)) { + if (isReadFileSyncDerived(callee.object, sourceCode)) { g1Violations.push(node); } } diff --git a/eslint-rules/no-unbounded-quantifier.cjs b/eslint-rules/no-unbounded-quantifier.cjs new file mode 100644 index 000000000..4d0197a08 --- /dev/null +++ b/eslint-rules/no-unbounded-quantifier.cjs @@ -0,0 +1,153 @@ +'use strict'; + +/** + * no-unbounded-quantifier + * + * Flags a lazy any-scan or an unbounded quantifier over a broad character + * class in a regex applied to caller-supplied document content — the ReDoS/ + * catastrophic-backtracking shape #2128 fixed (eight commits total; CodeQL + * has flagged the class, #663). ADR-3212 §5/§7 (epic #3212 Phase 4, #3415). + * + * This rule is NOT part of the ADR-1703 portability-rule family (see + * `docs/contributing/cross-platform-portability-rules.md`) and is not listed + * in `tests/portability-rule-disable-ban.test.cjs`'s `PROTECTED_RULES` — its + * `eslint-disable-next-line` suppressions, added after empirical + * benign-verification of a specific site, are an intentional and permitted + * part of this rule's design, unlike the ADR-1703 rules' zero-escape-hatch ban. + * + * ## What this enforces + * + * A RegExpLiteral (or `new RegExp('literal string')`, no interpolation — + * interpolated-value construction is `no-adhoc-regex-escape`'s territory) + * whose pattern contains an UNBOUNDED quantifier (`*`, `+`, `*?`, `+?`, or an + * open-ended `{n,}`) applied to: + * - `[\s\S]` / `[\S\s]` (the standard any-char-including-newline idiom) + * - `.` when the pattern carries the `s` (dotAll) flag + * - a broad negated class `[^...]` whose excluded set is short (1-2 units, + * an escape sequence counting as one unit) — e.g. `[^)]`, `[^\n]`, + * `[^)\n]` — matches nearly the same "everything" shape as `[\s\S]`, + * and is the exact shape #2128 fixed (`[^)\n]*` → `[^)\n]{0,200}`) + * + * AND whose match target is data-flow-traced to a `readFileSync` result + * (direct, chained, or via a same-scope variable binding — see + * `lib/readfilesync-trace.cjs`, shared with `no-crlf-fragile-split`). + * + * A quantifier that already carries an explicit closed bound (`{0,200}`, + * `{1,50}`) is NOT flagged — that is the #2128 fix shape this rule exists to + * make the default. + * + * ## Known boundaries + * + * Scoped to file/document content by data-flow, identically to + * `no-crlf-fragile-split`'s G2/G3 — a regex over a short in-memory constant, + * a flag string, or a non-readFileSync-derived value is out of scope by + * design (ADR-3212 §5: "most [of the census] are benign"). A regex stored far + * from its use, or content obtained via a non-readFileSync read (fs.readFile + * callback, streams), may not be caught. + * + * DEFECT category: CWE-1333 (Inefficient Regular Expression Complexity). + */ + +const { isPatternUsedOnFileContent } = require('./lib/readfilesync-trace.cjs'); + +/** + * Walks a regex pattern SOURCE STRING (not the compiled RegExp) looking for + * an unbounded quantifier applied to a broad-match atom. Returns true on the + * first fragile occurrence found. + * + * A "broad-match atom" immediately preceding the quantifier is one of: + * - the literal 6-char sequence `[\s\S]` or `[\S\s]` + * - `.` (only counted as broad when `hasDotAll` is true) + * - a character class `[^X]` where X (with escapes counted as one unit) is + * 1-2 units long + * + * An "unbounded quantifier" is `*`, `+`, `*?`, `+?`, or `{n,}` (no upper + * bound). `{n,m}` (closed) is never flagged. + * + * @param {string} pattern + * @param {boolean} hasDotAll + * @returns {boolean} + */ +function hasUnboundedBroadQuantifier(pattern, hasDotAll) { + let i = 0; + const len = pattern.length; + + while (i < len) { + let atomEnd = -1; + + if (pattern.startsWith('[\\s\\S]', i) || pattern.startsWith('[\\S\\s]', i)) { + atomEnd = i + 6; + } else if (hasDotAll && pattern[i] === '.') { + atomEnd = i + 1; + } else if (pattern[i] === '[' && pattern[i + 1] === '^') { + let j = i + 2; + let units = 0; + let closed = false; + while (j < len) { + if (pattern[j] === ']') { closed = true; break; } + if (units > 2) break; + if (pattern[j] === '\\' && j + 1 < len) { j += 2; units++; continue; } + j++; units++; + } + if (closed && units >= 1 && units <= 2) { + atomEnd = j + 1; + } + } + + if (atomEnd !== -1) { + const quant = pattern.slice(atomEnd, atomEnd + 8); + if (/^(\*\??|\+\??)/.test(quant)) return true; + const openEnded = quant.match(/^\{(\d+),\}/); + if (openEnded) return true; + } + + i++; + } + + return false; +} + +/** @type {import('eslint').Rule.RuleModule} */ +const rule = { + meta: { + type: 'problem', + docs: { + description: + 'Disallow an unbounded quantifier over a broad character class in a regex applied to file content (ReDoS risk, CWE-1333)', + category: 'Best Practices', + }, + schema: [], + messages: { + unboundedQuantifier: + 'Unbounded quantifier over a broad character class ([\\s\\S]/./[^X]) applied to ' + + 'readFileSync content is a catastrophic-backtracking risk (CWE-1333, #2128 class). ' + + 'Bound it explicitly (e.g. {0,200}) or replace with a scanner from src/token-scanner.cts.', + }, + }, + + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + + function checkPattern(node, pattern, flags) { + if (!hasUnboundedBroadQuantifier(pattern, flags.includes('s'))) return; + if (!isPatternUsedOnFileContent(node, sourceCode)) return; + context.report({ node, messageId: 'unboundedQuantifier' }); + } + + return { + Literal(node) { + if (!node.regex) return; + checkPattern(node, node.regex.pattern, node.regex.flags || ''); + }, + NewExpression(node) { + if (node.callee.type !== 'Identifier' || node.callee.name !== 'RegExp') return; + const [patternArg, flagsArg] = node.arguments; + if (!patternArg || patternArg.type !== 'Literal' || typeof patternArg.value !== 'string') return; + const flags = flagsArg && flagsArg.type === 'Literal' && typeof flagsArg.value === 'string' ? flagsArg.value : ''; + checkPattern(node, patternArg.value, flags); + }, + }; + }, +}; + +module.exports = rule; diff --git a/eslint.config.mjs b/eslint.config.mjs index 008aea1b8..3b42b72fa 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,6 +20,7 @@ import noPathLiteralInAssert from './eslint-rules/no-path-literal-in-assert.cjs' import noPosixModeBitAssert from './eslint-rules/no-posix-mode-bit-assert.cjs'; import noUnguardedNonportableExec from './eslint-rules/no-unguarded-nonportable-exec.cjs'; import noCrlfFragileSplit from './eslint-rules/no-crlf-fragile-split.cjs'; +import noUnboundedQuantifier from './eslint-rules/no-unbounded-quantifier.cjs'; import noHardcodedTmp from './eslint-rules/no-hardcoded-tmp.cjs'; import noBareNpmExec from './eslint-rules/no-bare-npm-exec.cjs'; import requireUserprofileWithHome from './eslint-rules/require-userprofile-with-home.cjs'; @@ -42,6 +43,7 @@ const localPlugin = { 'no-posix-mode-bit-assert': noPosixModeBitAssert, 'no-unguarded-nonportable-exec': noUnguardedNonportableExec, 'no-crlf-fragile-split': noCrlfFragileSplit, + 'no-unbounded-quantifier': noUnboundedQuantifier, 'no-hardcoded-tmp': noHardcodedTmp, 'no-bare-npm-exec': noBareNpmExec, 'require-userprofile-with-home': requireUserprofileWithHome, @@ -322,6 +324,8 @@ export default tseslint.config( 'local/no-adhoc-regex-escape': 'error', // ADR-3212 Phase 2 (#3413): widen the CRLF-fragile-split prohibition from tests/ to src/. 'local/no-crlf-fragile-split': 'error', + // ADR-3212 Phase 4 (#3415): bound quantifiers over document content (CWE-1333, #2128 class). + 'local/no-unbounded-quantifier': 'error', // ADR-1703 Phase 5: flag path-returning calls interpolated into content // (markdown @-references, workflow files, generated docs) without POSIX // normalization. Promoted to 'error' after precision review (path.basename @@ -504,6 +508,8 @@ export default tseslint.config( 'local/no-unguarded-nonportable-exec': 'error', // Ban CRLF-fragile file-content splits and regex patterns (ADR-1703 Phase 4) 'local/no-crlf-fragile-split': 'error', + // ADR-3212 Phase 4 (#3415): bound quantifiers over document content (CWE-1333, #2128 class). + 'local/no-unbounded-quantifier': 'error', // Ban hardcoded /tmp/ paths in fs.* calls (ADR-1703 Phase 4) 'local/no-hardcoded-tmp': 'error', // Ban bare npm exec without shell:true (ADR-1703 Phase 4) diff --git a/scripts/ci-test-scope.cjs b/scripts/ci-test-scope.cjs index da1e130ac..d9c9c935f 100644 --- a/scripts/ci-test-scope.cjs +++ b/scripts/ci-test-scope.cjs @@ -300,6 +300,23 @@ const RULES = [ 'tests/require-fs-op-fallback.rule.test.cjs', ], }, + { + // ADR-3212 Phase 4 (#3415): no-unbounded-quantifier and the shared + // readfilesync-trace helper it uses (also now imported by + // no-crlf-fragile-split). NOT part of the ADR-1703 portability family above + // — kept as its own bucket so this rule's tests re-run without pulling in + // the ADR-1703 disable-ban / vocab-drift suites it is not governed by. + name: 'no-unbounded-quantifier + readfilesync-trace (ADR-3212 Phase 4)', + match: path => [ + 'eslint-rules/no-unbounded-quantifier.cjs', + 'eslint-rules/lib/readfilesync-trace.cjs', + 'eslint-rules/no-crlf-fragile-split.cjs', + ].includes(path), + tests: [ + 'tests/no-unbounded-quantifier.rule.test.cjs', + 'tests/readfilesync-trace-parity.test.cjs', + ], + }, ]; /** diff --git a/scripts/lint-allow-test-rule-refs.ceiling.json b/scripts/lint-allow-test-rule-refs.ceiling.json index 05b0a1f86..aa077980c 100644 --- a/scripts/lint-allow-test-rule-refs.ceiling.json +++ b/scripts/lint-allow-test-rule-refs.ceiling.json @@ -1,4 +1,4 @@ { - "maxFiles": 301, + "maxFiles": 303, "grace": 3 } diff --git a/src/commands.cts b/src/commands.cts index 50ac3f45a..d3b17815f 100644 --- a/src/commands.cts +++ b/src/commands.cts @@ -762,6 +762,7 @@ function cmdEffortSync(cwd: string, raw: boolean, opts?: { dryRun?: boolean; con const rendered = renderEffortForRuntime(runtime, universalEffort); const newEffortValue = rendered.value; + // eslint-disable-next-line local/no-unbounded-quantifier -- lazy `*?` bounded by the `^---$/m` closing anchor, no nested quantifier, measured linear to 5MB (no-closing-marker adversarial input) const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content); if (!fmMatch) { skipped++; continue; } diff --git a/src/milestone.cts b/src/milestone.cts index d998dc30d..e80fb2c1a 100644 --- a/src/milestone.cts +++ b/src/milestone.cts @@ -824,6 +824,12 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo platformWriteSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`); } else { // Insert after the header line(s) for reverse chronological order (newest first) + // #3415: empirically verified linear-time up to 5MB adversarial input (worst-case + // no-newline-at-all forcing full [^\r\n]* backtrack: 0.11ms@10KB -> 6.9ms@5MB). + // Non-global, `^`-anchored (no /m) so this is a single match attempt at position 0 + // only — never rescanned at every offset — with no nested repeated group, so it + // cannot exhibit the #2128-class catastrophic backtracking. + // eslint-disable-next-line local/no-unbounded-quantifier -- single ^-anchored non-global attempt at pos 0, measured linear to 5MB, no nested quantifier const headerMatch = existing.match(/^(#{1,3}\s+[^\r\n]*\r?\n(?:\r?\n)?)/); if (headerMatch) { const header = headerMatch[1]; diff --git a/src/roadmap.cts b/src/roadmap.cts index 058878bdb..b599a628d 100644 --- a/src/roadmap.cts +++ b/src/roadmap.cts @@ -1090,6 +1090,14 @@ function cmdRoadmapAnnotateDependencies(cwd: string, phaseNum: string | null | u // Review fix (F2): `(?:^|\n)` anchors the match to start-of-line so mid-line // occurrences like `***Plans:***` embedded in a sentence or `OpenPlans: foo` // do not trigger a false match. Groups 1 and 2 retain the same semantics. + // #3415: empirically verified linear-time to 10.9MB / 320,000 lines of adversarial + // checklist input (0.31ms@1000 lines -> 8.4ms@320,000 lines). The outer `+` group has + // no trailing constraint after it in the pattern, so a successful greedy pass never + // needs to explore alternate `\r?\n?` boundary partitions to satisfy something later — + // it accepts the first complete parse and stops, which rules out the #2128-class + // ambiguous-boundary blowup despite the nested-quantifier shape. Non-global match on + // already phase-sliced content, not the whole file. + // eslint-disable-next-line local/no-unbounded-quantifier -- outer `+` has no trailing constraint to force re-partitioning; measured linear to 10.9MB const plansBlockMatch = phaseSection.match(/(?:^|\r?\n)(\*{0,2}Plans\*{0,2}:[^\r\n]*\r?\n)((?:\s*-\s*\[[ x]\][^\r\n]*\r?\n?)+)/i); if (!plansBlockMatch) return; diff --git a/tests/agent-classification-parity.test.cjs b/tests/agent-classification-parity.test.cjs index 59deba498..f789896d7 100644 --- a/tests/agent-classification-parity.test.cjs +++ b/tests/agent-classification-parity.test.cjs @@ -299,6 +299,7 @@ describe('agent-classification-parity: AGENTS.md section structure is the single // --- (c) parenthetical slug list --- // The prose lists short slugs without "gsd-" prefix, e.g.: // (pattern-mapper, debug-session-manager, ...) + // eslint-disable-next-line local/no-unbounded-quantifier -- parses docs/AGENTS.md, a maintainer-authored repo doc with bounded prose, not adversarial input const parenMatch = rawAgentsMd.match(/\(([^)]+)\)\s+have concise stubs/); assert.ok( parenMatch, diff --git a/tests/agent-frontmatter.test.cjs b/tests/agent-frontmatter.test.cjs index 569dc07c2..f9a80b21c 100644 --- a/tests/agent-frontmatter.test.cjs +++ b/tests/agent-frontmatter.test.cjs @@ -142,6 +142,7 @@ describe('SPAWN: spawn type consistency', () => { const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); for (const file of files) { const content = fs.readFileSync(path.join(dir, file), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow/command markdown, bounded prose, not adversarial input const matches = content.matchAll(/subagent_type="([^"]+)"/g); for (const match of matches) { const agentType = match[1]; @@ -179,6 +180,7 @@ describe('SPAWN: spawn type consistency', () => { const content = fs.readFileSync(path.join(dir, file), 'utf-8'); // Find all named subagent_type references (excluding general-purpose // and #1689 runtime placeholders) + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow/command markdown, bounded prose, not adversarial input const matches = [...content.matchAll(/subagent_type="([^"]+)"/g)]; const namedAgents = matches .map(m => m[1]) @@ -198,6 +200,7 @@ describe('SPAWN: spawn type consistency', () => { // Every spawned agent type must appear in the listing for (const agent of new Set(namedAgents)) { const agentTypesMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content /([\s\S]*?)<\/available_agent_types>/ ); assert.ok( @@ -251,6 +254,7 @@ describe('COLOR: color frontmatter must be a documented named color', () => { for (const agent of ALL_AGENTS) { test(`${agent} color: is a documented named color`, () => { const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md frontmatter block, fixed-size author-controlled content const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); const frontmatter = fmMatch ? fmMatch[1] : ''; const colorMatch = frontmatter.match(/^color:\s*(.+)$/m); @@ -1461,6 +1465,7 @@ describe('Bug #2990: cleanup tail fast-forwards $branch and deletes the temp bra test('recovery sentinel JSON shape records reviewfix_branch alongside worktree_path', () => { // Find the writeFileSync call that constructs the sentinel JSON. // Parse the JSON.stringify argument list to extract the field names. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input const match = md.match(/fs\.writeFileSync\(sentinelPath,\s*JSON\.stringify\(\{([^}]+)\}/); assert.notEqual(match, null, 'expected JSON.stringify({...}) inside the sentinel write'); const fields = match[1].split(',').map(s => s.trim().split(':')[0].trim()).filter(Boolean); diff --git a/tests/agent-required-reading-consistency.test.cjs b/tests/agent-required-reading-consistency.test.cjs index 11e67cda4..f79ea4423 100644 --- a/tests/agent-required-reading-consistency.test.cjs +++ b/tests/agent-required-reading-consistency.test.cjs @@ -196,6 +196,7 @@ describe('debugger agent references bug patterns', () => { test('reference is inside block', () => { const content = fs.readFileSync(DEBUGGER_AGENT_PATH, 'utf-8'); const reqReadMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md content, fixed-size author-controlled content /([\s\S]*?)<\/required_reading>/ ); assert.ok(reqReadMatch, 'Debugger agent should have a block'); diff --git a/tests/atomic-write-coverage.test.cjs b/tests/atomic-write-coverage.test.cjs index 83be53bc2..aa0032f36 100644 --- a/tests/atomic-write-coverage.test.cjs +++ b/tests/atomic-write-coverage.test.cjs @@ -86,6 +86,7 @@ describe('atomic write coverage (#1972)', () => { // hand-written: const { platformWriteSync } = require('./shell-command-projection.cjs') // tsc-compiled: const x = require("./shell-command-projection.cjs"); x.platformWriteSync(...) const hasImport = + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded lib/*.cjs source file, not adversarial input /platformWriteSync[^)]*\}\s*=\s*require\(['"]\.\/shell-command-projection\.cjs['"]\)/s.test(content) || /require\(['"]\.\/shell-command-projection\.cjs['"]\)/.test(content); assert.ok( diff --git a/tests/autonomous-allowed-tools.test.cjs b/tests/autonomous-allowed-tools.test.cjs index 8cfd1404c..b8335e8eb 100644 --- a/tests/autonomous-allowed-tools.test.cjs +++ b/tests/autonomous-allowed-tools.test.cjs @@ -21,6 +21,7 @@ describe('commands/gsd/autonomous.md allowed-tools', () => { const content = fs.readFileSync(filePath, 'utf-8'); // Extract the YAML frontmatter block between the first pair of --- delimiters + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own command .md frontmatter, fixed-size author-controlled content const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(frontmatterMatch, 'autonomous.md must have YAML frontmatter'); diff --git a/tests/autonomous-interactive.test.cjs b/tests/autonomous-interactive.test.cjs index 9b913dca0..f1858f707 100644 --- a/tests/autonomous-interactive.test.cjs +++ b/tests/autonomous-interactive.test.cjs @@ -107,6 +107,7 @@ describe('autonomous --interactive flag (#1413)', () => { test('success criteria include --interactive requirements', () => { const content = fs.readFileSync(workflowPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const criteriaMatch = content.match(/([\s\S]*?)<\/success_criteria>/); const criteria = criteriaMatch ? criteriaMatch[1] : ''; assert.ok(criteria.includes('--interactive'), diff --git a/tests/autonomous-to-flag.test.cjs b/tests/autonomous-to-flag.test.cjs index f77aee6af..1fa345b71 100644 --- a/tests/autonomous-to-flag.test.cjs +++ b/tests/autonomous-to-flag.test.cjs @@ -139,6 +139,7 @@ describe('autonomous --to N flag (#1644)', () => { test('success criteria include --to N requirements', () => { const content = fs.readFileSync(workflowPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const criteriaMatch = content.match(/([\s\S]*?)<\/success_criteria>/); const criteria = criteriaMatch ? criteriaMatch[1] : ''; assert.ok(criteria.includes('--to'), diff --git a/tests/check-update-config-dir.test.cjs b/tests/check-update-config-dir.test.cjs index 430ee8814..286a6e833 100644 --- a/tests/check-update-config-dir.test.cjs +++ b/tests/check-update-config-dir.test.cjs @@ -31,10 +31,12 @@ describe('detectConfigDir search order (#1860)', () => { const content = fs.readFileSync(CHECK_UPDATE_PATH, 'utf8'); // Extract the search order array from the for..of loop in detectConfigDir + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks/gsd-check-update.js source, not adversarial input const arrayMatch = content.match(/for\s*\(const dir of\s*\[([^\]]+)\]/); assert.ok(arrayMatch, 'should find the for..of search array in detectConfigDir'); const arrayLiteral = arrayMatch[1]; + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks/gsd-check-update.js source, not adversarial input const entries = arrayLiteral.match(/'([^']+)'/g).map(s => s.replace(/'/g, '')); const claudeIndex = entries.indexOf('.claude'); @@ -89,6 +91,7 @@ describe('detectConfigDir runtime behavior (#1860)', () => { const hookSource = fs.readFileSync(CHECK_UPDATE_PATH, 'utf8'); // Extract detectConfigDir function body (from 'function detectConfigDir' to the closing brace) + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own hook script source, fixed-size author-controlled content const fnMatch = hookSource.match(/(function detectConfigDir\(baseDir\)\s*\{[\s\S]*?\r?\n\})/); assert.ok(fnMatch, 'should be able to extract detectConfigDir function from hook source'); const fnSource = fnMatch[1]; @@ -126,6 +129,7 @@ describe('detectConfigDir runtime behavior (#1860)', () => { fs.writeFileSync(path.join(openCodeVersionDir, 'VERSION'), '1.0.0\n'); const hookSource = fs.readFileSync(CHECK_UPDATE_PATH, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own hook script source, fixed-size author-controlled content const fnMatch = hookSource.match(/(function detectConfigDir\(baseDir\)\s*\{[\s\S]*?\r?\n\})/); assert.ok(fnMatch, 'should be able to extract detectConfigDir function from hook source'); const fnSource = fnMatch[1]; diff --git a/tests/claude-imperative-reference.test.cjs b/tests/claude-imperative-reference.test.cjs index ab1f1da69..452199695 100644 --- a/tests/claude-imperative-reference.test.cjs +++ b/tests/claude-imperative-reference.test.cjs @@ -138,8 +138,11 @@ test('bin/install.js contains no `runtime === "claude"` / `runtime !== "claude"` // pattern (a comment explaining "not a string-equality branch") do not // false-positive — only LIVE code counts. const codeOnly = src + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded bin/install.js source, not adversarial input .replace(/\/\*[\s\S]*?\*\//g, '') // block comments + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded bin/install.js source, not adversarial input .replace(/\/\/[^\r\n]*/g, '') // line comments (CRLF-safe) + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded bin/install.js source, not adversarial input .replace(/`[^`]*`/g, ''); // backtick / inline-code spans const offenders = codeOnly.match(/runtime\s*[!=]==\s*'claude'/g) || []; assert.deepEqual( diff --git a/tests/code-review.test.cjs b/tests/code-review.test.cjs index 91215cd01..653c7bfd3 100644 --- a/tests/code-review.test.cjs +++ b/tests/code-review.test.cjs @@ -243,6 +243,7 @@ describe('CR-AGENT: code review agent frontmatter', () => { test('gsd-code-fixer.md success_criteria consistent with rollback strategy (git checkout)', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-code-fixer.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md content, fixed-size author-controlled content const successCriteria = content.match(/([\s\S]*?)<\/success_criteria>/)?.[1] || ''; assert.ok(successCriteria.includes('git checkout'), 'gsd-code-fixer success_criteria must reference git checkout rollback'); @@ -290,6 +291,7 @@ describe('CR-AGENT: code review agent frontmatter', () => { test('#2825 gsd-code-fixer.md records where verification ran (main checkout vs worktree)', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-code-fixer.md'), 'utf-8'); assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md content, fixed-size author-controlled content /verification[\s\S]*(main checkout|worktree)|(main checkout|worktree)[\s\S]*verification/i.test(content), 'gsd-code-fixer REVIEW-FIX.md must record where verification ran (main checkout vs worktree) so a reader knows if the numbers are reproducible (#2825)', ); @@ -417,6 +419,7 @@ describe('CR-WORKFLOW: code review workflow structure', () => { const content = fs.readFileSync(path.join(WORKFLOWS_DIR, 'code-review.md'), 'utf-8'); // mapfile is bash 4+ only; macOS ships bash 3.2. Dedup must use portable while-read. // Note: 'mapfile' may appear in platform_notes documentation — check bash code blocks only + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const codeBlocks = content.match(/```bash[\s\S]*?```/g) || []; const hasMapfileInCode = codeBlocks.some(block => block.includes('mapfile -t')); assert.ok(!hasMapfileInCode, @@ -427,6 +430,7 @@ describe('CR-WORKFLOW: code review workflow structure', () => { test('code-review-fix.md uses portable while-read loop for array construction (not mapfile)', () => { const content = fs.readFileSync(path.join(WORKFLOWS_DIR, 'code-review-fix.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const codeBlocks = content.match(/```bash[\s\S]*?```/g) || []; const hasMapfileInCode = codeBlocks.some(block => block.includes('mapfile -t')); assert.ok(!hasMapfileInCode, @@ -498,6 +502,7 @@ describe('CR-INTEGRATION: workflow integration points', () => { test('execute-phase.md resolves code-review capability hook', () => { const content = fs.readFileSync(path.join(WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input const gateMatch = content.match(/]*>([\s\S]*?)<\/step>/); assert.ok(gateMatch, 'execute-phase.md missing code_review_gate step'); const gateContent = gateMatch[1]; @@ -514,6 +519,7 @@ describe('CR-INTEGRATION: workflow integration points', () => { const content = fs.readFileSync(path.join(PLUGIN_WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8'); // Extract code_review_gate section to check + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const gateMatch = content.match(/([\s\S]*?)<\/step>/); if (gateMatch) { const gateContent = gateMatch[1]; diff --git a/tests/command-routing-hub.test.cjs b/tests/command-routing-hub.test.cjs index e1b06f2a9..f434419fb 100644 --- a/tests/command-routing-hub.test.cjs +++ b/tests/command-routing-hub.test.cjs @@ -1631,6 +1631,7 @@ describe('bug-853 — manager/autonomous gate background dispatch by runtime', ( test('manager.md compound action preamble uses FLATTEN language (not hardcoded runtime names)', () => { // allow-test-rule: source-text-is-the-product (see #1708) const compoundActionSection = MANAGER.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own manager.md content, fixed-size author-controlled content /### Compound Action \(background \+ inline\)[\s\S]*?Inline verification:/, ); assert.ok(compoundActionSection, 'manager.md must document compound action runtime dispatch'); diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index f22824052..9ede32218 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -4406,6 +4406,7 @@ describe('#2279: map-codebase date stamp instructions overwrite existing dates', const content = fs.readFileSync( path.join(REPO_ROOT, 'gsd-core', 'workflows', 'map-codebase.md'), 'utf-8' ); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input const stampLines = content.match(/Set all date stamps[^\r\n]*/g) || []; assert.ok(stampLines.length >= 4, `must have ≥4 "Set all date stamps" instructions (4 spawn prompts + 1 sequential); got ${stampLines.length}`); diff --git a/tests/commit-files-pathspec.test.cjs b/tests/commit-files-pathspec.test.cjs index c72388301..548e78322 100644 --- a/tests/commit-files-pathspec.test.cjs +++ b/tests/commit-files-pathspec.test.cjs @@ -2876,6 +2876,7 @@ describe('workflow call sites declare --files (#2269)', () => { commitLine, 'secure-phase.md step 7 commit invocation not found — did the workflow drop or rename its SECURITY.md commit?', ); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses a single line from maintainer-authored secure-phase.md, bounded, not adversarial input const filesArg = /--files\s+"([^"]+)"/.exec(commitLine); // Two different failures, two different messages. This test derives the // scope from the workflow's own quoted --files value, so an UNQUOTED diff --git a/tests/completion-ratio-scope-withholding.test.cjs b/tests/completion-ratio-scope-withholding.test.cjs index 6c3741640..538d06ca7 100644 --- a/tests/completion-ratio-scope-withholding.test.cjs +++ b/tests/completion-ratio-scope-withholding.test.cjs @@ -790,6 +790,7 @@ describe('H. self-consistency — after state sync, STATE.md body and frontmatte // the two halves could disagree within the SAME write: frontmatter // (already scoped) omitted percent while the body (hardcoded // SCOPE.COMPLETE) rendered one. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const fmMatch = after.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, 'STATE.md must carry a frontmatter block after a write'); assert.ok(!/^\s*percent:/m.test(fmMatch[1]), 'frontmatter progress: block must not carry a percent key'); diff --git a/tests/config-field-docs.test.cjs b/tests/config-field-docs.test.cjs index 2630841ec..6007b70ba 100644 --- a/tests/config-field-docs.test.cjs +++ b/tests/config-field-docs.test.cjs @@ -64,6 +64,7 @@ describe('config-field-docs', () => { // Extract CONFIG_DEFAULTS keys from config-loader.cjs source (moved from core.cjs by ADR-857 phase 2e) const coreSource = fs.readFileSync(CORE_PATH, 'utf-8'); const defaultsMatch = coreSource.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own config-loader.cjs source, fixed-size author-controlled content /const CONFIG_DEFAULTS\s*=\s*\{([\s\S]*?)\r?\n\};/ ); assert.ok(defaultsMatch, 'Could not find CONFIG_DEFAULTS in config-loader.cjs'); diff --git a/tests/config-schema.property.test.cjs b/tests/config-schema.property.test.cjs index 3b7767853..4282d2de5 100644 --- a/tests/config-schema.property.test.cjs +++ b/tests/config-schema.property.test.cjs @@ -1073,6 +1073,7 @@ describe('feat-3210: workflow and config contracts', () => { ); // Parse: the block must exist and be closed + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const stepMatch = workflow.match(/([\s\S]*?)<\/step>/); assert.ok( stepMatch, diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index c60dc671d..874142ba0 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -840,6 +840,7 @@ describe('Copilot agent conversion - real files', () => { assert.ok(toolsLine.includes("'execute'"), 'Bash mapped to execute'); assert.ok(toolsLine.includes("'search'"), 'Grep/Glob deduplicated to search'); // Input tools count > output tools count (deduplication occurred) + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agents/gsd-executor.md frontmatter, bounded, not adversarial input const inputTools = content.match(/^tools:\s*\[([^\]]+)\]/m)?.[1].split(',').length ?? 0; const outputTools = toolsLine.replace(/^tools:\s*\[/, '').replace(/\].*$/, '').split(',').length; assert.ok(inputTools === 0 || outputTools <= inputTools, 'deduplication reduced or preserved tool count'); diff --git a/tests/copilot-upgrades.test.cjs b/tests/copilot-upgrades.test.cjs index 065f8a3eb..ec8ec2045 100644 --- a/tests/copilot-upgrades.test.cjs +++ b/tests/copilot-upgrades.test.cjs @@ -142,6 +142,7 @@ test('copilot .agent.md frontmatter has no background-dispatch field (negotiated assert.ok(agentFiles.length > 0, 'at least one .agent.md must be installed'); const sample = fs.readFileSync(path.join(agentsDir, agentFiles[0]), 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own installed agent .md frontmatter, fixed-size author-controlled content const frontmatterMatch = sample.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(frontmatterMatch, 'agent file must have a frontmatter block'); assert.ok(!/^background:/m.test(frontmatterMatch[1]), diff --git a/tests/debug-session-management.test.cjs b/tests/debug-session-management.test.cjs index 99789cab6..baaf50d20 100644 --- a/tests/debug-session-management.test.cjs +++ b/tests/debug-session-management.test.cjs @@ -213,6 +213,7 @@ describe('debug skill dispatch and sub-orchestrator (#2148, #2151)', () => { assert.ok(debugMatch, 'DEBUG.md must state a "N-field structured reasoning record" claim for reasoning_checkpoint'); const claimedCount = /^\d+$/.test(debugMatch[1]) ? parseInt(debugMatch[1], 10) : NUMWORDS[debugMatch[1].toLowerCase()]; assert.ok(typeof claimedCount === 'number', `unrecognized field-count token: ${debugMatch[1]}`); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md content, fixed-size author-controlled content const yamlBlock = agentContent.match(/reasoning_checkpoint:\s*\r?\n([\s\S]*?)```/); assert.ok(yamlBlock, 'gsd-debugger.md must define a fenced reasoning_checkpoint YAML block'); const keys = new Set(); diff --git a/tests/declarative-reference-antigravity.test.cjs b/tests/declarative-reference-antigravity.test.cjs index 7a0f69bc7..2c93de425 100644 --- a/tests/declarative-reference-antigravity.test.cjs +++ b/tests/declarative-reference-antigravity.test.cjs @@ -432,12 +432,14 @@ describe('/gsd:update detects local Antigravity (.agent / .agents) installs (#50 test('execution_context classifier maps /.agents/ and /.agent/ paths to antigravity (update.md)', () => { const hasAgentsClassifierRule = + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored update.md workflow, bounded prose, not adversarial input /\/\.agents\/[^\r\n]*->[^\r\n]*antigravity/.test(UPDATE_MD); assert.ok( hasAgentsClassifierRule, 'update.md classifier must map a `/.agents/` path to the `antigravity` runtime', ); const hasAgentClassifierRule = + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored update.md workflow, bounded prose, not adversarial input /\/\.agent\/[^\r\n]*->[^\r\n]*antigravity/.test(UPDATE_MD); assert.ok( hasAgentClassifierRule, diff --git a/tests/declarative-reference-augment.test.cjs b/tests/declarative-reference-augment.test.cjs index 4b33ab2cf..9dce51d7e 100644 --- a/tests/declarative-reference-augment.test.cjs +++ b/tests/declarative-reference-augment.test.cjs @@ -202,6 +202,7 @@ test('legitimate isAugment destructure/enumeration sites survive (not eliminated const file = path.join(__dirname, '..', 'bin', 'install.js'); const src = fs.readFileSync(file, 'utf8'); assert.ok(/isAugment/.test(src), 'isAugment must still be destructured from runtimeFlags() for non-conversion uses'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded bin/install.js source, not adversarial input assert.ok(/_DESCRIPTOR_AGENTS_RUNTIMES\s*=\s*new Set\(\[[^\]]*'augment'/.test(src), 'augment must remain in _DESCRIPTOR_AGENTS_RUNTIMES (descriptor-driven agent layout)'); }); diff --git a/tests/discuss-checkpoint.test.cjs b/tests/discuss-checkpoint.test.cjs index 2c90efc92..17806025b 100644 --- a/tests/discuss-checkpoint.test.cjs +++ b/tests/discuss-checkpoint.test.cjs @@ -74,6 +74,7 @@ describe('discuss-phase incremental checkpoint saves (#1485)', () => { test('success criteria include checkpoint requirements', () => { const content = fs.readFileSync(workflowPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const criteriaMatch = content.match(/([\s\S]*?)<\/success_criteria>/); const criteria = criteriaMatch ? criteriaMatch[1] : ''; assert.ok(criteria.includes('checkpoint') || criteria.includes('Checkpoint'), diff --git a/tests/discuss-mode.test.cjs b/tests/discuss-mode.test.cjs index 67f952457..42a212d51 100644 --- a/tests/discuss-mode.test.cjs +++ b/tests/discuss-mode.test.cjs @@ -37,6 +37,7 @@ describe('workflow.discuss_mode config', () => { path.join(__dirname, '..', 'commands', 'gsd', 'discuss-phase.md'), 'utf8' ); // Extract the block + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own command .md content, fixed-size author-controlled content const processMatch = command.match(/([\s\S]*?)<\/process>/); assert.ok(processMatch, 'should have a block'); const processBlock = processMatch[1]; diff --git a/tests/edit-phase.test.cjs b/tests/edit-phase.test.cjs index a8347c7df..bcbcf0875 100644 --- a/tests/edit-phase.test.cjs +++ b/tests/edit-phase.test.cjs @@ -289,6 +289,7 @@ describe('edit-phase workflow: phase number and position preservation', () => { test('anti_patterns block renumbering', () => { const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const antiPatterns = content.match(/([\s\S]*?)<\/anti_patterns>/i); assert.ok(antiPatterns, 'workflow should have anti_patterns section'); assert.ok( @@ -314,6 +315,7 @@ describe('edit-phase workflow: phase number and position preservation', () => { describe('edit-phase workflow: milestone scope guard (#3262)', () => { test('workflow captures the milestone scope before writing the updated phase', () => { const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const writeStep = content.match(/([\s\S]*?)<\/step>/); assert.ok(writeStep, 'write_updated_phase step must exist'); assert.match( @@ -326,6 +328,7 @@ describe('edit-phase workflow: milestone scope guard (#3262)', () => { test('workflow re-derives the milestone scope after the write and rolls back on mismatch', () => { const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const writeStep = content.match(/([\s\S]*?)<\/step>/); assert.ok(writeStep, 'write_updated_phase step must exist'); assert.match(writeStep[1], /SCOPE_AFTER/i, 'the post-write re-derivation must be present'); @@ -344,6 +347,7 @@ describe('edit-phase workflow: milestone scope guard (#3262)', () => { test('milestone scope guard success criterion is checked (#3262)', () => { const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const criteria = content.match(/([\s\S]*?)<\/success_criteria>/); assert.ok(criteria, 'workflow should have a success_criteria section'); assert.match( @@ -380,6 +384,7 @@ describe('edit-phase: documentation registration', () => { ); // Locate the edit-phase.md row in the Workflows table and assert the // "Invoked by" column documents /gsd-phase --edit (not the deleted form). + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored docs/INVENTORY.md, bounded table rows, not adversarial input const rowMatch = inventory.match(/^\|\s*`edit-phase\.md`\s*\|[^|]*\|\s*([^|]+?)\s*\|$/m); assert.ok(rowMatch, 'docs/INVENTORY.md must contain an edit-phase.md workflow row'); const invokedBy = rowMatch[1]; diff --git a/tests/effort-surface-axis.test.cjs b/tests/effort-surface-axis.test.cjs index 72e498323..3f847f154 100644 --- a/tests/effort-surface-axis.test.cjs +++ b/tests/effort-surface-axis.test.cjs @@ -402,6 +402,7 @@ describe('#2481 — ADR-443 mechanism callers, as they actually exist', () => { for (const e of fs.readdirSync(abs, { withFileTypes: true })) { const full = path.join(abs, e.name); if (e.isDirectory()) walk(path.relative(REPO_ROOT, full)); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow/reference/agent markdown, bounded prose, not adversarial input else if (e.name.endsWith('.md') && /resolve-execution[^\r\n]*--effort\s/.test(fs.readFileSync(full, 'utf8'))) { hits.push(path.relative(REPO_ROOT, full)); } diff --git a/tests/execute-phase-active-flags.test.cjs b/tests/execute-phase-active-flags.test.cjs index 4caa72688..c30015b61 100644 --- a/tests/execute-phase-active-flags.test.cjs +++ b/tests/execute-phase-active-flags.test.cjs @@ -25,6 +25,7 @@ describe('execute-phase command: active flags are explicit', () => { test('objective says documented flags are not implied active', () => { const content = fs.readFileSync(COMMAND_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own command .md content, fixed-size author-controlled content const objectiveMatch = content.match(/([\s\S]*?)<\/objective>/); assert.ok(objectiveMatch, 'should have section'); assert.ok( @@ -107,6 +108,7 @@ function assertMakefileCheckBeforeNpmTest(filePath, label) { const content = fs.readFileSync(filePath, 'utf-8'); // Must check for Makefile with test target + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input const hasMakefileCheck = /Makefile.*grep.*test:|grep.*test:.*Makefile/s.test(content) || (content.includes('Makefile') && content.includes('"^test:"')); assert.ok( diff --git a/tests/execute-phase-wave.test.cjs b/tests/execute-phase-wave.test.cjs index 3ec636cff..7f141e985 100644 --- a/tests/execute-phase-wave.test.cjs +++ b/tests/execute-phase-wave.test.cjs @@ -40,6 +40,7 @@ describe('execute-phase command: --wave flag', () => { test('objective describes wave-filter execution', () => { const content = fs.readFileSync(COMMAND_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own command .md content, fixed-size author-controlled content const objectiveMatch = content.match(/([\s\S]*?)<\/objective>/); assert.ok(objectiveMatch, 'should have section'); assert.ok(objectiveMatch[1].includes('--wave N'), 'objective should mention --wave N'); @@ -486,6 +487,7 @@ describe('bug #2410: execute-phase emits checkpoint heartbeats', () => { test('workflow emits a wave-start heartbeat (A: wave-boundary checkpoint)', () => { assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored execute-phase.md workflow, bounded prose, not adversarial input /\[checkpoint\][^\r\n]*wave \{N\}\/\{M\} starting/.test(workflow), 'workflow should emit a wave-start [checkpoint] marker before spawning agents' ); @@ -493,6 +495,7 @@ describe('bug #2410: execute-phase emits checkpoint heartbeats', () => { test('workflow emits a wave-complete heartbeat (A: wave-boundary checkpoint)', () => { assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored execute-phase.md workflow, bounded prose, not adversarial input /\[checkpoint\][^\r\n]*wave \{N\}\/\{M\} complete/.test(workflow), 'workflow should emit a wave-complete [checkpoint] marker after spot-checks' ); @@ -500,6 +503,7 @@ describe('bug #2410: execute-phase emits checkpoint heartbeats', () => { test('workflow emits a plan-start heartbeat (B: plan-boundary checkpoint)', () => { assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored execute-phase.md workflow, bounded prose, not adversarial input /\[checkpoint\][^\r\n]*plan \{plan_id\} starting/.test(workflow), 'workflow should emit a plan-start [checkpoint] marker before each Task() dispatch' ); @@ -507,6 +511,7 @@ describe('bug #2410: execute-phase emits checkpoint heartbeats', () => { test('workflow emits a plan-complete heartbeat (B: plan-boundary checkpoint)', () => { assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored execute-phase.md workflow, bounded prose, not adversarial input /\[checkpoint\][^\r\n]*plan \{plan_id\} complete/.test(workflow), 'workflow should emit a plan-complete [checkpoint] marker after executor returns' ); @@ -514,10 +519,12 @@ describe('bug #2410: execute-phase emits checkpoint heartbeats', () => { test('workflow handles plan failure and checkpoint-gate heartbeats too', () => { assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored execute-phase.md workflow, bounded prose, not adversarial input /\[checkpoint\][^\r\n]*plan \{plan_id\} failed/.test(workflow), 'workflow should emit a plan-failed [checkpoint] marker on executor error' ); assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored execute-phase.md workflow, bounded prose, not adversarial input /\[checkpoint\][^\r\n]*plan \{plan_id\} checkpoint/.test(workflow), 'workflow should emit a heartbeat when a plan returns a human-gate checkpoint' ); @@ -565,6 +572,7 @@ describe('bug #2410: execute-phase emits checkpoint heartbeats', () => { assert.ok(spawnIdx !== -1 && waitIdx !== -1, 'spawn and wait steps must exist'); const step3 = workflow.slice(spawnIdx, waitIdx); assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses a slice of maintainer-authored execute-phase.md workflow, bounded prose, not adversarial input /\[checkpoint\][^\r\n]*plan \{plan_id\} starting/.test(step3), 'plan-start heartbeat should be emitted inside step 3 (spawn executor agents)' ); @@ -576,6 +584,7 @@ describe('bug #2410: execute-phase emits checkpoint heartbeats', () => { assert.ok(waitIdx !== -1 && hookIdx !== -1, 'wait + hook steps must exist'); const step4 = workflow.slice(waitIdx, hookIdx); assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses a slice of maintainer-authored execute-phase.md workflow, bounded prose, not adversarial input /\[checkpoint\][^\r\n]*plan \{plan_id\} complete/.test(step4), 'plan-complete heartbeat should be emitted in step 4 (wait for agents)' ); @@ -585,6 +594,7 @@ describe('bug #2410: execute-phase emits checkpoint heartbeats', () => { assert.ok(reportIdx !== -1 && failureIdx !== -1, 'report + failure steps must exist'); const step6 = workflow.slice(reportIdx, failureIdx); assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses a slice of maintainer-authored execute-phase.md workflow, bounded prose, not adversarial input /\[checkpoint\][^\r\n]*wave \{N\}\/\{M\} complete/.test(step6), 'wave-complete heartbeat should be emitted in step 6 (report completion)' ); diff --git a/tests/execute-phase-worktree-artifacts.test.cjs b/tests/execute-phase-worktree-artifacts.test.cjs index 7d140a243..4b5931a6d 100644 --- a/tests/execute-phase-worktree-artifacts.test.cjs +++ b/tests/execute-phase-worktree-artifacts.test.cjs @@ -41,6 +41,7 @@ describe('execute-phase worktree: shared artifact ownership (#1571)', () => { // Extract the worktree Task() block (between "Worktree mode" and "Sequential mode") const worktreeMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content /\*\*Worktree mode\*\*[\s\S]*?([\s\S]*?)<\/success_criteria>/ ); assert.ok(worktreeMatch, 'should find success_criteria inside the worktree mode Task block'); @@ -57,6 +58,7 @@ describe('execute-phase worktree: shared artifact ownership (#1571)', () => { // Extract the worktree Task() block const worktreeMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content /\*\*Worktree mode\*\*[\s\S]*?([\s\S]*?)<\/success_criteria>/ ); assert.ok(worktreeMatch, 'should find success_criteria inside the worktree mode Task block'); @@ -73,6 +75,7 @@ describe('execute-phase worktree: shared artifact ownership (#1571)', () => { // SUMMARY.md is plan-local and safe for worktree agents to create const worktreeMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content /\*\*Worktree mode\*\*[\s\S]*?([\s\S]*?)<\/success_criteria>/ ); assert.ok(worktreeMatch, 'should find success_criteria inside the worktree mode Task block'); @@ -113,6 +116,7 @@ describe('execute-phase worktree: shared artifact ownership (#1571)', () => { // Extract the sequential mode Task() block const seqMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content /\*\*Sequential mode\*\*[\s\S]*?([\s\S]*?)<\/success_criteria>/ ); assert.ok(seqMatch, 'should find success_criteria inside the sequential mode Task block'); @@ -129,6 +133,7 @@ describe('execute-phase worktree: shared artifact ownership (#1571)', () => { // Extract the sequential mode Task() block const seqMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content /\*\*Sequential mode\*\*[\s\S]*?([\s\S]*?)<\/success_criteria>/ ); assert.ok(seqMatch, 'should find success_criteria inside the sequential mode Task block'); diff --git a/tests/executor-mvp-tdd-section.test.cjs b/tests/executor-mvp-tdd-section.test.cjs index 86763ec9c..3cacacac9 100644 --- a/tests/executor-mvp-tdd-section.test.cjs +++ b/tests/executor-mvp-tdd-section.test.cjs @@ -87,6 +87,7 @@ describe('gsd-executor — state.* calls use the named-only router form (#1863 r // silently dropping the values. Guard them alongside the executor. for (const rel of ['gsd-core/workflows/milestone-summary.md', 'gsd-core/workflows/forensics.md']) { const wf = fs.readFileSync(path.join(__dirname, '..', rel), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input const m = wf.match(/gsd_run query state\.record-session\b(?:[^\r\n]*\\\r?\n)*[^\r\n]*/); assert.ok(m, `${rel} must invoke state.record-session`); assert.ok(m[0].includes('--stopped-at') && m[0].includes('--resume-file'), diff --git a/tests/gates-taxonomy.test.cjs b/tests/gates-taxonomy.test.cjs index 1c9503c29..013a4efc0 100644 --- a/tests/gates-taxonomy.test.cjs +++ b/tests/gates-taxonomy.test.cjs @@ -94,6 +94,7 @@ describe('gates taxonomy (#1715)', () => { test('gsd-plan-checker.md references gates.md in required_reading block', () => { const planChecker = path.join(ROOT, 'agents', 'gsd-plan-checker.md'); const content = fs.readFileSync(planChecker, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md content, fixed-size author-controlled content const match = content.match(/\r?\n([\s\S]*?)\r?\n<\/required_reading>/); assert.ok( match, @@ -108,6 +109,7 @@ describe('gates taxonomy (#1715)', () => { test('gsd-verifier.md references gates.md in required_reading block', () => { const verifier = path.join(ROOT, 'agents', 'gsd-verifier.md'); const content = fs.readFileSync(verifier, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md content, fixed-size author-controlled content const match = content.match(/\r?\n([\s\S]*?)\r?\n<\/required_reading>/); assert.ok( match, diff --git a/tests/gen-context-index.test.cjs b/tests/gen-context-index.test.cjs index e6e90fac3..8959b2953 100644 --- a/tests/gen-context-index.test.cjs +++ b/tests/gen-context-index.test.cjs @@ -148,6 +148,7 @@ describe('gen-context-index.cjs --check (F)', () => { test('checkExitsOneWhenPredicateValueChanged', () => { const real = fs.readFileSync(REAL_CONTEXT_PATH, 'utf8'); const modified = real.replace( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own maintainer-authored CONTEXT.md, bounded, not adversarial input /`RULESET\.PR-SCOPE\.one-concern-per-pr=[^`]*`/, '`RULESET.PR-SCOPE.one-concern-per-pr=CHANGED VALUE FOR TEST`', ); @@ -171,6 +172,7 @@ describe('gen-context-index.cjs --check (F)', () => { test('checkExitsOneWhenPredicateRemoved', () => { const real = fs.readFileSync(REAL_CONTEXT_PATH, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own maintainer-authored CONTEXT.md, bounded, not adversarial input const removed = real.replace(/`RULESET\.PR-SCOPE\.one-concern-per-pr=[^`]*`\r?\n/, ''); assert.notEqual(removed, real, 'fixture setup sanity: the removal must actually apply'); const removedPath = path.join(tmpDir, 'CONTEXT-removed.md'); diff --git a/tests/gsd-check-update-worker-platform-gate.test.cjs b/tests/gsd-check-update-worker-platform-gate.test.cjs index 2dd930574..d90cb222a 100644 --- a/tests/gsd-check-update-worker-platform-gate.test.cjs +++ b/tests/gsd-check-update-worker-platform-gate.test.cjs @@ -41,7 +41,9 @@ const PROJECTION_PATH = path.join( function codeOnly(file) { return fs.readFileSync(file, 'utf8') + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks/lib source, not adversarial input .replace(/\/\*[\s\S]*?\*\//g, '') + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks source, not adversarial input .replace(/(^|[^:])\/\/[^\r\n]*/g, '$1'); } @@ -316,7 +318,9 @@ const { PACKAGE_NAME } = require('../gsd-core/bin/check-latest-version.cjs'); function workerCodeOnly() { const src = fs.readFileSync(WORKER_PATH, 'utf8'); return src + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks source, not adversarial input .replace(/\/\*[\s\S]*?\*\//g, '') + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks source, not adversarial input .replace(/(^|[^:])\/\/[^\r\n]*/g, '$1'); } @@ -415,6 +419,7 @@ describe('bug-2784: update.md cache-clear covers shared cache path', () => { test('gsd-check-update.js hook constructs cache dir from .cache and gsd path segments', () => { const hookContent = fs.readFileSync(CHECK_UPDATE_HOOK, 'utf-8'); // Parse the path.join() call structurally rather than text-grepping. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks/gsd-check-update.js source, not adversarial input const m = hookContent.match(/const cacheDir\s*=\s*path\.join\(([^)]+)\)/); assert.ok( m !== null, @@ -434,6 +439,7 @@ describe('bug-2784: update.md cache-clear covers shared cache path', () => { test('update.md run_update bash commands include rm for shared gsd cache file', () => { const workflowContent = fs.readFileSync(UPDATE_WORKFLOW, 'utf-8'); // Parse the step block structurally, then extract only bash fenced code lines. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const stepMatch = workflowContent.match(/[\s\S]*?<\/step>/); assert.ok(stepMatch, 'update.md must have a block'); const stepContent = stepMatch[0]; diff --git a/tests/gsd-researcher-app-aware.test.cjs b/tests/gsd-researcher-app-aware.test.cjs index 5c8bc501f..a3afed37c 100644 --- a/tests/gsd-researcher-app-aware.test.cjs +++ b/tests/gsd-researcher-app-aware.test.cjs @@ -55,6 +55,7 @@ describe('phase-researcher: Architectural Responsibility Mapping', () => { test('step is a pure reasoning step with no tool calls', () => { // Extract the ARM section content (between the ARM heading and the next ## Step heading) + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored agent markdown, bounded prose, not adversarial input const armHeadingMatch = content.match(/## Step 1\.5[^\r\n]*Architectural Responsibility Map/); assert.ok(armHeadingMatch, 'Must have a Step 1.5 heading for Architectural Responsibility Mapping'); diff --git a/tests/gsd-settings-advanced.test.cjs b/tests/gsd-settings-advanced.test.cjs index 84e3b0bfb..4107bc468 100644 --- a/tests/gsd-settings-advanced.test.cjs +++ b/tests/gsd-settings-advanced.test.cjs @@ -135,6 +135,7 @@ describe('gsd-settings-advanced — file scaffolding', () => { test('command frontmatter has name, description, allowed-tools', () => { const text = fs.readFileSync(COMMAND_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own command .md frontmatter, fixed-size author-controlled content const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, 'command file missing frontmatter block'); const fm = fmMatch[1]; diff --git a/tests/gsd-write-guard.test.cjs b/tests/gsd-write-guard.test.cjs index afcc5e77d..b0dc0a009 100644 --- a/tests/gsd-write-guard.test.cjs +++ b/tests/gsd-write-guard.test.cjs @@ -531,6 +531,7 @@ describe('guard <-> complete-milestone workflow binding (the escape hatch is WIR // binding fails if any reorganize step other than the sentinel-armed one // is (re)introduced without hatch wiring of its own. const src = fs.readFileSync(workflowPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored complete-milestone.md workflow, bounded prose, not adversarial input const names = [...src.matchAll(//g)].map((m) => m[1]); assert.deepEqual(names, ['reorganize_roadmap_and_delete_originals'], 'complete-milestone.md must contain exactly one ROADMAP-reorganize step — the ' + diff --git a/tests/hermes-skills-migration.test.cjs b/tests/hermes-skills-migration.test.cjs index ba44ab499..45939a649 100644 --- a/tests/hermes-skills-migration.test.cjs +++ b/tests/hermes-skills-migration.test.cjs @@ -1247,6 +1247,7 @@ describe('#2284(b) branding protected-region — compari test('sanity: the source file has a block containing "Claude Code:" as a compared-runtime label', () => { assert.ok(//.test(CONTENT)); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const block = CONTENT.match(/[\s\S]*?<\/runtime_compatibility>/)[0]; assert.ok(/\*\*Claude Code:\*\*/.test(block)); }); diff --git a/tests/ingest-docs.test.cjs b/tests/ingest-docs.test.cjs index 7d4b4ff8a..f2239ebac 100644 --- a/tests/ingest-docs.test.cjs +++ b/tests/ingest-docs.test.cjs @@ -228,6 +228,7 @@ describe('gsd-doc-synthesizer agent', () => { assert.match(content, /^tools:\s*.*Read.*Write.*Bash.*/m); }); test('documents default precedence ADR > SPEC > PRD > DOC', () => { + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored gsd-doc-synthesizer agent markdown, bounded prose, not adversarial input const precedenceBlock = content.match(/ADR[^.]*SPEC[^.]*PRD[^.]*DOC/); assert.ok(precedenceBlock, 'default precedence ordering must be documented'); }); diff --git a/tests/init-debug-workflow-contract.test.cjs b/tests/init-debug-workflow-contract.test.cjs index 4436deead..0c14a7c0b 100644 --- a/tests/init-debug-workflow-contract.test.cjs +++ b/tests/init-debug-workflow-contract.test.cjs @@ -79,6 +79,7 @@ describe('debug.md Step 0 init contract (#3149, matrix §F)', () => { test('documents the null-manifest read-everything fallback (row F5)', () => { assert.ok(workflow.includes('section_manifest'), 'the field is documented'); assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input /`null`[^\r\n]*read this workflow in full/i.test(workflow), 'a null section_manifest must be documented as "read everything" — without the rule, ' + 'a null manifest reads as an empty selection and the workflow reads nothing' diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 3a74e5138..788ca065d 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -2846,6 +2846,7 @@ describe('#2376 — init.* path fields resolve when process cwd differs from --c test('gsd-core/workflows/verify-work.md plan_gap_closure step references {state_path}/{roadmap_path}, not bare .planning literals', () => { const wfPath = path.join(__dirname, '..', 'gsd-core', 'workflows', 'verify-work.md'); const content = fs.readFileSync(wfPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const stepMatch = content.match(/[\s\S]*?<\/step>/); assert.ok(stepMatch, 'plan_gap_closure step should exist in verify-work.md'); const step = stepMatch[0]; @@ -2867,6 +2868,7 @@ describe('#2376 — init.* path fields resolve when process cwd differs from --c test('gsd-core/workflows/execute-phase.md verify_phase_goal step references {requirements_path}, not a bare .planning literal', () => { const wfPath = path.join(__dirname, '..', 'gsd-core', 'workflows', 'execute-phase.md'); const content = fs.readFileSync(wfPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const stepMatch = content.match(/[\s\S]*?<\/step>/); assert.ok(stepMatch, 'verify_phase_goal step should exist in execute-phase.md'); const step = stepMatch[0]; diff --git a/tests/inline-plan-threshold.test.cjs b/tests/inline-plan-threshold.test.cjs index 310e2cd3f..3711c1a91 100644 --- a/tests/inline-plan-threshold.test.cjs +++ b/tests/inline-plan-threshold.test.cjs @@ -95,6 +95,7 @@ describe('execute-plan.md routing instruction (#1979)', () => { // Simulate how the grep pattern would behave against sample PLAN.md content // Extract the pattern from execute-plan.md const content = fs.readFileSync(executePlanPath, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored execute-plan.md workflow, bounded prose, not adversarial input const patternMatch = content.match(/TASK_COUNT=\$\(grep -cE '([^']+)'/); assert.ok(patternMatch, 'must find TASK_COUNT grep pattern'); diff --git a/tests/install-runtime-artifacts.test.cjs b/tests/install-runtime-artifacts.test.cjs index 180b463ae..eece01b34 100644 --- a/tests/install-runtime-artifacts.test.cjs +++ b/tests/install-runtime-artifacts.test.cjs @@ -4032,6 +4032,7 @@ describe('#443 resolveInstallTimeEffort: invalid tokens fall through to valid ef ); assert.match(tomlContent, /^model\s*=\s*"gpt-5.6-sol"$/m, `gsd-planner.toml should pin Codex model when runtime:"codex" is configured\nActual:\n${tomlContent.slice(0, 500)}`); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses output of an actual install run against test-controlled config, bounded, not adversarial input const match = tomlContent.match(/^model_reasoning_effort\s*=\s*"([^"]+)"/m); assert.ok(match, `model_reasoning_effort must be present in .toml\nActual:\n${tomlContent.slice(0, 500)}`); assert.ok(VALID_EFFORTS.includes(match[1]), @@ -4820,6 +4821,7 @@ describe('Bug #2973: profile-user.md confirmation message references the skills test('the Display message points at $HOME/.claude/skills/gsd-dev-preferences/SKILL.md', () => { const md = fs.readFileSync(WORKFLOW, 'utf-8'); // Match the structured Display: line; capture the path value. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored profile-user.md workflow, bounded prose, not adversarial input const m = md.match(/Display:\s*"[^"]*Generated\s*\/gsd-dev-preferences\s*at\s*([^"]+)"/); assert.notEqual(m, null, 'expected a Display: "Generated /gsd-dev-preferences at " line'); const referencedPath = m[1].trim(); @@ -5959,6 +5961,7 @@ describe('bug-2808: SKILL.md name: uses hyphen form', () => { const skillContent = fs.readFileSync(skillMdPath, 'utf-8'); // Scope the name: lookup to the YAML frontmatter block so a stray // `name:` line in the body cannot satisfy the assertion. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own generated SKILL.md frontmatter, fixed-size author-controlled content const fmMatch = skillContent.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, `${relPath}: generated SKILL.md must include frontmatter`); const nameLine = fmMatch[1].split(/\r?\n/).find((l) => /^name:\s*/.test(l)); diff --git a/tests/install.test.cjs b/tests/install.test.cjs index 45fc41820..893427fe5 100644 --- a/tests/install.test.cjs +++ b/tests/install.test.cjs @@ -1780,6 +1780,7 @@ describe('#767 Parity: docs/AGENTS.md "Disallowed Tools" rows match READONLY_AGE const sectionEnd = nextSectionIdx === -1 ? agentsDoc.length : nextSectionIdx; const section = agentsDoc.slice(agentHeaderIdx, sectionEnd); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored docs/AGENTS.md table row, bounded, not adversarial input const disallowedMatch = section.match(/\|\s*\*\*Disallowed Tools\*\*\s*\|\s*([^|]+)\|/); assert.ok(disallowedMatch, `docs/AGENTS.md section for ${agent} must have a "Disallowed Tools" table row`); diff --git a/tests/m8-writestatemd-scan-after-lock.test.cjs b/tests/m8-writestatemd-scan-after-lock.test.cjs index 72445e956..4f3d30035 100644 --- a/tests/m8-writestatemd-scan-after-lock.test.cjs +++ b/tests/m8-writestatemd-scan-after-lock.test.cjs @@ -53,6 +53,7 @@ const MINIMAL_STATE_MD = [ /** Parse progress.total_plans out of the STATE.md frontmatter block. */ function readTotalPlans(statePath) { const written = fs.readFileSync(statePath, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const fmMatch = written.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, 'STATE.md must have a frontmatter block after writeStateMd'); const m = fmMatch[1].match(/total_plans:\s*(\d+)/); diff --git a/tests/model-omit-when-inherit-guard.test.cjs b/tests/model-omit-when-inherit-guard.test.cjs index 16198daec..72e92b481 100644 --- a/tests/model-omit-when-inherit-guard.test.cjs +++ b/tests/model-omit-when-inherit-guard.test.cjs @@ -338,6 +338,7 @@ test('#2684: ship.md validates capability-supplied ref.agent before it reaches a // substitutes the raw value textually, so a shell-side test would run only // AFTER a payload like `x"; id; echo "` had already closed the assignment and // executed. Assert the workflow states the in-context ordering explicitly. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored ship.md workflow, bounded prose, not adversarial input const gate = /`(\^\[A-Za-z0-9\]\[[^`]*\]\*\$)`/.exec(content); assert.ok( gate, diff --git a/tests/no-pending-3212-markers.test.cjs b/tests/no-pending-3212-markers.test.cjs new file mode 100644 index 000000000..4e0a4df43 --- /dev/null +++ b/tests/no-pending-3212-markers.test.cjs @@ -0,0 +1,79 @@ +// allow-test-rule: structural-regression-guard see #3415 +// Row 15 of .gsd/phase/chore-3415-prohibition-with-teeth/50-test-matrix.md +// (design doc §4, docs/adr/3212-lexical-seam-consolidation.md:149/167): Phase 4 +// burns down and deletes the grandfather allowlists that Phases 1-3 used to +// grandfather pre-existing #2128-class regex sites, so no file in the tracked +// tree may still carry a `pending #3212` marker after this phase ships. This +// is a structural source-text property (the marker's mere presence, not its +// runtime behavior), so a tracked-tree scan is the only faithful check. +'use strict'; + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); + +const ROOT = path.resolve(__dirname, '..'); +const MARKER = 'pending #3212'; + +// Files that legitimately contain the literal marker string in prose/self- +// reference rather than as an actual (now-forbidden) suppression marker: +// - this file itself (the string appears in this comment/the assertion +// message, describing what it guards against) +// - docs/adr/3212-lexical-seam-consolidation.md — the ADR prose that +// DEFINES the "pending #3212" marker convention and Phase 4's mandate to +// burn it down (lines 149, 167); verified via `git grep -n "pending +// #3212"` to be the only doc referencing the phrase. +const EXCLUDED_RELATIVE_PATHS = new Set([ + 'tests/no-pending-3212-markers.test.cjs', + 'docs/adr/3212-lexical-seam-consolidation.md', +]); + +/** + * Returns the repo's tracked files (repo-relative, forward-slash-normalized), + * so untracked/gitignored files (e.g. .gsd/) are naturally excluded. + * @returns {string[]} + */ +function listTrackedFiles() { + const stdout = execFileSync('git', ['-c', 'safe.directory=*', 'ls-files'], { + cwd: ROOT, + encoding: 'utf8', + timeout: 30000, + }); + return stdout + .split(/\r?\n/) + .map((line) => line.replace(/\\/g, '/')) + .filter((line) => line.length > 0); +} + +test('#3212 epic: no tracked file contains a "pending #3212" marker (design doc §4)', () => { + const fs = require('node:fs'); + const violations = []; + + for (const relPath of listTrackedFiles()) { + if (EXCLUDED_RELATIVE_PATHS.has(relPath)) continue; + + const fullPath = path.join(ROOT, relPath); + let content; + try { + content = fs.readFileSync(fullPath, 'utf8'); + } catch (_) { + // Not a regular readable text file (e.g. a submodule gitlink, a binary + // asset, or a symlink target that moved) — skip; this guard is about + // source-text markers, not filesystem integrity. + continue; + } + + if (content.includes(MARKER)) { + violations.push(relPath); + } + } + + assert.equal( + violations.length, + 0, + `Found ${violations.length} tracked file(s) still carrying a "pending #3212" ` + + `grandfather marker (ADR-3212 Phase 4 requires them all burned down and ` + + `deleted): ${violations.join(', ')}`, + ); +}); diff --git a/tests/no-unbounded-quantifier.rule.test.cjs b/tests/no-unbounded-quantifier.rule.test.cjs new file mode 100644 index 000000000..9f5354ba1 --- /dev/null +++ b/tests/no-unbounded-quantifier.rule.test.cjs @@ -0,0 +1,233 @@ +'use strict'; + +/** + * no-unbounded-quantifier.rule.test.cjs + * + * RuleTester unit tests for the local/no-unbounded-quantifier ESLint rule + * (ADR-3212 §5/§7, epic #3212 Phase 4, #3415). Rows 1-13 per + * .gsd/phase/chore-3415-prohibition-with-teeth/50-test-matrix.md. + * + * NOTE: Fixture code strings must encode actual regex-literal backslashes + * (e.g. `\s`, `\S`, `\n`, `\d`) as doubled `\\s`/`\\S`/`\\n`/`\\d` inside the + * JavaScript string literals used for RuleTester `code` fields, so that the + * ESLint parser receives the intended source text (mirrors + * tests/no-crlf-fragile-split.rule.test.cjs's convention). For row 10's + * `new RegExp('[\s\S]*')` — the pattern is itself a STRING LITERAL in the + * target source, so its `\s`/`\S` need a further doubling in that source + * text (`\\s`/`\\S`), which then needs doubling AGAIN to survive our own + * JS string literal — four backslashes total before each `s`/`S`. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const { RuleTester } = require('eslint'); + +const rule = require('../eslint-rules/no-unbounded-quantifier.cjs'); + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'commonjs', + }, +}); + +// ─── module shape ───────────────────────────────────────────────────────────── + +describe('no-unbounded-quantifier rule module', () => { + test('exports meta and create', () => { + assert.strictEqual(typeof rule.meta, 'object'); + assert.strictEqual(typeof rule.create, 'function'); + assert.strictEqual(rule.meta.type, 'problem'); + assert.ok(rule.meta.messages.unboundedQuantifier, 'unboundedQuantifier message must exist'); + }); +}); + +// ─── INVALID (happy-path, rule fires) ───────────────────────────────────────── + +describe('no-unbounded-quantifier: invalid (unboundedQuantifier fires)', () => { + test('row 1: [\\s\\S]* on readFileSync-derived content', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [], + invalid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/[\\s\\S]*/);", + filename: 'tests/foo.test.cjs', + errors: [{ messageId: 'unboundedQuantifier' }], + }, + ], + }); + }); + + test('row 3: flags the #2128 [^)\\n]* shape', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [], + invalid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/[^)\\n]*/);", + filename: 'tests/foo.test.cjs', + errors: [{ messageId: 'unboundedQuantifier' }], + }, + ], + }); + }); + + test('row 5: flags dotAll . with unbounded *', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [], + invalid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/.*text/s);", + filename: 'tests/foo.test.cjs', + errors: [{ messageId: 'unboundedQuantifier' }], + }, + ], + }); + }); + + test('row 10: flags new RegExp() built from a literal string', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [], + invalid: [ + { + // Source text: new RegExp('[\\s\\S]*').test(fs.readFileSync(p, 'utf8')) + code: "const ok = new RegExp('[\\\\s\\\\S]*').test(fs.readFileSync(p, 'utf8'));", + filename: 'tests/foo.test.cjs', + errors: [{ messageId: 'unboundedQuantifier' }], + }, + ], + }); + }); + + test('row 12: flags a lazy [\\s\\S]+? as unbounded too', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [], + invalid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/[\\s\\S]+?/);", + filename: 'tests/foo.test.cjs', + errors: [{ messageId: 'unboundedQuantifier' }], + }, + ], + }); + }); + + test('row 13: flags an open-ended {0,} as unbounded', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [], + invalid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/[\\s\\S]{0,}/);", + filename: 'tests/foo.test.cjs', + errors: [{ messageId: 'unboundedQuantifier' }], + }, + ], + }); + }); +}); + +// ─── VALID (negative) ────────────────────────────────────────────────────────── + +describe('no-unbounded-quantifier: valid cases', () => { + test('row 2: does not flag an already-bounded [\\s\\S]{0,200}', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/[\\s\\S]{0,200}/);", + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + }); + + test('row 4: does not flag the #2128-fixed {0,200} form', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/[^)\\n]{0,200}/);", + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + }); + + test('row 6: does not flag plain . without dotAll', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/.*text/);", + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + }); + + test('row 7: does not flag a wide (3+ unit) negated class', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/[^abc]*/);", + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + }); + + test('row 8: does not flag a narrow class like \\d*', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [ + { + code: "const m = fs.readFileSync(p, 'utf8').match(/\\d*/);", + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + }); + + test('row 9: does not flag a non-readFileSync-derived receiver', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [ + { + code: 'const m = someShortConstant.match(/[\\s\\S]*/);', + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + }); + + test('row 11: does not attempt to inspect a non-literal new RegExp() pattern', () => { + ruleTester.run('no-unbounded-quantifier', rule, { + valid: [ + { + code: "const pat = someVar; readFileSync(p, 'utf8').match(new RegExp(pat));", + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + }); + + test('row 14: does not hang on an adversarial run of unclosed negated classes (quadratic-scan regression)', () => { + // Security review finding: hasUnboundedBroadQuantifier's inner negated-class + // scan previously ran unbounded from every `[^` offset with no closing `]`, + // making this O(n^2). The fix bails once units > 2. This test's only + // assertion is that RuleTester.run() completes at all (a stuck inner scan + // would hang the test rather than fail it) — no wall-clock threshold here. + const adversarialPattern = '[^' + '[^'.repeat(50000); + const result = ruleTester.run('no-unbounded-quantifier', rule, { + valid: [ + { + code: `const m = fs.readFileSync(p, 'utf8').match(new RegExp('${adversarialPattern}'));`, + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + assert.strictEqual(result, undefined); + }); +}); diff --git a/tests/parallel-dependent-plans.test.cjs b/tests/parallel-dependent-plans.test.cjs index dbb0b78a5..9c114daf7 100644 --- a/tests/parallel-dependent-plans.test.cjs +++ b/tests/parallel-dependent-plans.test.cjs @@ -56,6 +56,7 @@ describe('gsd-planner agent: files_modified wave ordering', () => { const content = fs.readFileSync(PLANNER_AGENT_PATH, 'utf-8'); // Look for the assign_waves step block const assignWavesMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md content, fixed-size author-controlled content /([\s\S]*?)<\/step>/ ); assert.ok(assignWavesMatch, 'assign_waves step should exist in gsd-planner.md'); @@ -72,6 +73,7 @@ describe('gsd-planner agent: files_modified wave ordering', () => { test('assign_waves step treats files_modified overlap same as depends_on dependency', () => { const content = fs.readFileSync(PLANNER_AGENT_PATH, 'utf-8'); const assignWavesMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own agent .md content, fixed-size author-controlled content /([\s\S]*?)<\/step>/ ); assert.ok(assignWavesMatch, 'assign_waves step should exist'); diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index c92c05d41..d787e30ae 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -4370,6 +4370,7 @@ describe('phase complete command', () => { assert.ok(result.success, `Command failed: ${result.error}`); const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses ROADMAP.md the test itself wrote via a fixed fixture string, bounded, not adversarial input const rowMatch = roadmap.match(/^\|[^\r\n]*1\. Foundation[^\r\n]*$/m); assert.ok(rowMatch, 'table row should exist'); const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); @@ -4441,6 +4442,7 @@ describe('phase complete command', () => { assert.ok(result.success, `Command failed: ${result.error}`); const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses ROADMAP.md the test itself wrote via a fixed fixture string, bounded, not adversarial input const rowMatch = roadmap.match(/^\|[^\r\n]*1\. Foundation[^\r\n]*$/m); assert.ok(rowMatch, 'table row should exist'); const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); @@ -4481,6 +4483,7 @@ describe('phase complete command', () => { assert.ok(result.success, `Command failed: ${result.error}`); const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses ROADMAP.md the test itself wrote via a fixed fixture string, bounded, not adversarial input const rowMatch = roadmap.match(/^\|[^\r\n]*1\. Foundation[^\r\n]*$/m); assert.ok(rowMatch, 'table row should exist'); const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); @@ -9823,6 +9826,7 @@ describe('bug-2502: insert-phase must update STATE.md next-phase recommendation' test('insert-phase.md update_project_state step covers next-phase pointer', () => { const content = fs.readFileSync(INSERT_PHASE_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const stepMatch = content.match(/([\s\S]*?)<\/step>/i); assert.ok(stepMatch, 'insert-phase.md must contain update_project_state step'); const stepContent = stepMatch[1]; @@ -10375,6 +10379,7 @@ describe('issue #2334: ghost-REQ-ID classification must probe write surfaces, no `#2334 HIGH 2b FAILED (fixture invariant): checkbox must have been ticked.\n${reqContent}`, ); assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses REQUIREMENTS.md the test itself wrote via build2334GhostSurfaceFixture, bounded fixed-size fixture, not adversarial input /\|\s*KNOWN-01\s*\|[^|]*\|\s*Complete\s*\|/i.test(reqContent), `#2334 HIGH 2b FAILED (fixture invariant): Traceability row must have flipped to Complete.\n${reqContent}`, ); diff --git a/tests/plan-phase-drift-guard.test.cjs b/tests/plan-phase-drift-guard.test.cjs index 86d617d47..019b52b30 100644 --- a/tests/plan-phase-drift-guard.test.cjs +++ b/tests/plan-phase-drift-guard.test.cjs @@ -121,6 +121,7 @@ describe('plan-phase workflow: Artifacts this phase produces section (#22)', () test('quality_gate checklist includes Artifacts this phase produces item', () => { // Find the quality_gate block and confirm the checklist item is there + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const qualityGateMatch = workflow.match(/([\s\S]*?)<\/quality_gate>/); assert.ok( qualityGateMatch, @@ -157,6 +158,7 @@ describe('plan-phase workflow: Artifacts this phase produces section (#22)', () describe('plan-phase workflow: top-level spawn guard (#913)', () => { // Extract the runtime_compatibility block for targeted assertions const rtBlock = (() => { + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const m = workflow.match(/([\s\S]*?)<\/runtime_compatibility>/); return m ? m[1] : ''; })(); @@ -219,6 +221,7 @@ describe('plan-phase workflow: top-level spawn guard (#913)', () => { describe('plan-phase workflow: attempt-based Agent availability gate (#922)', () => { // Extract the runtime_compatibility block for targeted assertions const rtBlock = (() => { + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const m = workflow.match(/([\s\S]*?)<\/runtime_compatibility>/); return m ? m[1] : ''; })(); @@ -501,6 +504,7 @@ describe('bug-2949: sketch --wrap-up dispatch wiring', () => { test('commands/gsd/sketch.md has sketch-wrap-up in execution_context section', () => { const content = fs.readFileSync(SKETCH_COMMAND, 'utf8'); // Find execution_context block + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own command .md content, fixed-size author-controlled content const execCtxMatch = content.match(/([\s\S]*?)<\/execution_context>/); assert.ok(execCtxMatch, 'sketch.md must have an block'); const execCtx = execCtxMatch[1]; @@ -1679,7 +1683,9 @@ describe('plan-phase decision-coverage gate (#2492)', () => { const snippet = md.slice(gateIdx, gateIdx + 800); // Accept either an inline `|| exit 1` or a `|| { ...; exit 1; }` group. const hasJqGuard = + // eslint-disable-next-line local/no-unbounded-quantifier -- parses a bounded slice of maintainer-authored workflow markdown, not adversarial input /jq[^\r\n]*\.data\.passed\s*==\s*true/.test(snippet) || + // eslint-disable-next-line local/no-unbounded-quantifier -- parses a bounded slice of maintainer-authored workflow markdown, not adversarial input /jq[^\r\n]*\(\.passed\s*\/\/\s*\.data\.passed\)\s*==\s*true/.test(snippet); const hasExitOne = /\|\|\s*(?:exit\s+1|\{[\s\S]{0,200}?exit\s+1)/.test(snippet); assert.ok( diff --git a/tests/plan-review-convergence.test.cjs b/tests/plan-review-convergence.test.cjs index 4d0afb0c8..f9ef48649 100644 --- a/tests/plan-review-convergence.test.cjs +++ b/tests/plan-review-convergence.test.cjs @@ -552,6 +552,7 @@ describe('plan-review-convergence workflow: config gate (#2306-v2)', () => { test('workflow defaults config key to false (opt-in, not opt-out)', () => { // The config-get call must default to false, not true + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input const configGetMatch = workflow.match(/config-get\s+workflow\.plan_review_convergence[^\r\n]*/); assert.ok( configGetMatch, @@ -921,6 +922,7 @@ describe('plan-review-convergence CONFIGURATION.md documentation (#2306-v2)', () }); test('CONFIGURATION.md entry documents disabled-by-default behavior', () => { + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored docs/CONFIGURATION.md, bounded prose, not adversarial input const row = configDoc.match(/workflow\.plan_review_convergence[^\r\n]*/); assert.ok(row, 'workflow.plan_review_convergence row must exist in CONFIGURATION.md'); assert.ok( diff --git a/tests/planner-decomposition.test.cjs b/tests/planner-decomposition.test.cjs index 7d6eee9cf..116603270 100644 --- a/tests/planner-decomposition.test.cjs +++ b/tests/planner-decomposition.test.cjs @@ -193,6 +193,7 @@ function read(relativePath) { function extractDeepWorkRules() { const workflow = fs.readFileSync(PLAN_PHASE_WORKFLOW, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const match = workflow.match(/[\s\S]*?<\/deep_work_rules>/); assert.ok(match, 'plan-phase.md must contain a deep_work_rules block'); return match[0]; diff --git a/tests/plugin-manifest.test.cjs b/tests/plugin-manifest.test.cjs index 1a78e243c..ef9ce4119 100644 --- a/tests/plugin-manifest.test.cjs +++ b/tests/plugin-manifest.test.cjs @@ -978,6 +978,7 @@ describe('H: skills surface projection (#1596)', () => { assert.ok(skillDirs.length > 0, 'must have at least one skill dir'); for (const dir of skillDirs) { const raw = fs.readFileSync(path.join(SKILLS_DIR, dir.name, 'SKILL.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own generated SKILL.md frontmatter, fixed-size author-controlled content const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, `${dir.name}/SKILL.md must have frontmatter`); const fm = fmMatch[1]; diff --git a/tests/progress-mvp-display.test.cjs b/tests/progress-mvp-display.test.cjs index 60d91ca86..661782c11 100644 --- a/tests/progress-mvp-display.test.cjs +++ b/tests/progress-mvp-display.test.cjs @@ -103,6 +103,7 @@ describe('#14: /gsd:progress --next --auto flag must be documented and propagate // Extract only the … block so this assertion is // scoped to the handoff wiring, not just any occurrence in the file. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own command .md content, fixed-size author-controlled content const processMatch = command.match(/([\s\S]*?)<\/process>/); assert.ok( processMatch, diff --git a/tests/readfilesync-trace-parity.test.cjs b/tests/readfilesync-trace-parity.test.cjs new file mode 100644 index 000000000..3c08f97a4 --- /dev/null +++ b/tests/readfilesync-trace-parity.test.cjs @@ -0,0 +1,145 @@ +'use strict'; + +/** + * readfilesync-trace-parity.test.cjs + * + * Row 14 of .gsd/phase/chore-3415-prohibition-with-teeth/50-test-matrix.md: + * `no-crlf-fragile-split.cjs` and `no-unbounded-quantifier.cjs` both import + * their readFileSync-derivation data-flow tracing from the shared + * `eslint-rules/lib/readfilesync-trace.cjs` module (extracted from + * no-crlf-fragile-split, ADR-1703 Phase 4 / ADR-3212 §5-6, #3415). There is + * no separate "pre-extraction inline copy" left to diff against — this test + * instead asserts the two RULES currently AGREE on the shared data-flow + * classification: for a fixture whose receiver is NOT readFileSync-derived, + * BOTH rules must report zero errors, even though each rule's own + * pattern-shape condition (bare `\n` for crlf; broad-atom + unbounded + * quantifier for unbounded-quantifier) would independently match the regex + * content. For a fixture whose receiver IS readFileSync-derived (direct, + * chained, or via a same-scope variable), each rule's own pattern-shape + * condition is ALSO satisfied by every fixture below, so each fires. + * + * `isPatternUsedOnFileContent`/`isReadFileSyncDerived` are not exercised + * standalone: both take a live `sourceCode` (parent pointers via + * `sourceCode.getScope`/`node.parent`) that only exists inside a real + * ESLint visitor, so a RuleTester run against both consuming rules is the + * faithful way to exercise the shared module identically to production. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const { RuleTester } = require('eslint'); + +const crlfRule = require('../eslint-rules/no-crlf-fragile-split.cjs'); +const quantifierRule = require('../eslint-rules/no-unbounded-quantifier.cjs'); +const trace = require('../eslint-rules/lib/readfilesync-trace.cjs'); + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'commonjs', + }, +}); + +// ─── shared module shape ──────────────────────────────────────────────────── + +describe('readfilesync-trace shared module shape', () => { + test('exports isReadFileSyncDerived(node, sourceCode) and isPatternUsedOnFileContent(regexNode, sourceCode)', () => { + assert.strictEqual(typeof trace.isReadFileSyncDerived, 'function'); + assert.strictEqual(trace.isReadFileSyncDerived.length, 2); + assert.strictEqual(typeof trace.isPatternUsedOnFileContent, 'function'); + assert.strictEqual(trace.isPatternUsedOnFileContent.length, 2); + }); +}); + +// ─── shared fixtures ───────────────────────────────────────────────────────── +// +// Every fixture's regex carries BOTH a bare \n (crlf rule's own pattern-shape +// trigger) AND an unbounded [\s\S]* (unbounded-quantifier rule's own +// pattern-shape trigger), so any divergence between the two rules can only +// come from disagreement on the shared readFileSync-derived classification, +// never from a pattern-shape mismatch. + +const READFILESYNC_DERIVED_FIXTURES = [ + { + name: 'direct call: fs.readFileSync(p, "utf8").match(...)', + code: "const m = fs.readFileSync(p, 'utf8').match(/[\\s\\S]*\\n/);", + }, + { + name: 'chained call: fs.readFileSync(p, "utf8").toString().match(...)', + code: "const m = fs.readFileSync(p, 'utf8').toString().match(/[\\s\\S]*\\n/);", + }, + { + name: 'scope variable: const content = fs.readFileSync(...); content.match(...)', + code: [ + "const content = fs.readFileSync(filePath, 'utf8');", + 'const m = content.match(/[\\s\\S]*\\n/);', + ].join('\n'), + }, +]; + +const NOT_READFILESYNC_DERIVED_FIXTURES = [ + { + name: 'plain identifier: someShortConstant.match(...)', + code: 'const m = someShortConstant.match(/[\\s\\S]*\\n/);', + }, + { + name: 'scope variable NOT from readFileSync: const content = "hello"; content.match(...)', + code: ["const content = 'hello';", 'const m = content.match(/[\\s\\S]*\\n/);'].join('\n'), + }, +]; + +// ─── parity: readFileSync-derived receivers — both rules fire ─────────────── + +describe('readfilesync-trace parity: readFileSync-derived receivers — both rules fire', () => { + for (const fixture of READFILESYNC_DERIVED_FIXTURES) { + test(`crlf and unbounded-quantifier both fire on: ${fixture.name}`, () => { + ruleTester.run('no-crlf-fragile-split', crlfRule, { + valid: [], + invalid: [ + { + code: fixture.code, + filename: 'tests/foo.test.cjs', + errors: [{ messageId: 'crlfFragileRegex' }], + }, + ], + }); + ruleTester.run('no-unbounded-quantifier', quantifierRule, { + valid: [], + invalid: [ + { + code: fixture.code, + filename: 'tests/foo.test.cjs', + errors: [{ messageId: 'unboundedQuantifier' }], + }, + ], + }); + }); + } +}); + +// ─── parity: non-readFileSync-derived receivers — both rules stay silent ──── + +describe('readfilesync-trace parity: non-readFileSync-derived receivers — both rules agree on zero', () => { + for (const fixture of NOT_READFILESYNC_DERIVED_FIXTURES) { + test(`crlf and unbounded-quantifier both report zero on: ${fixture.name}`, () => { + ruleTester.run('no-crlf-fragile-split', crlfRule, { + valid: [ + { + code: fixture.code, + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + ruleTester.run('no-unbounded-quantifier', quantifierRule, { + valid: [ + { + code: fixture.code, + filename: 'tests/foo.test.cjs', + }, + ], + invalid: [], + }); + }); + } +}); diff --git a/tests/reapply-patches.test.cjs b/tests/reapply-patches.test.cjs index cf1164d62..fa5e015b7 100644 --- a/tests/reapply-patches.test.cjs +++ b/tests/reapply-patches.test.cjs @@ -328,6 +328,7 @@ describe('reapply-patches workflow contract (#1469)', () => { const updatePath = path.join(__dirname, '..', 'commands', 'gsd', 'update.md'); const content = fs.readFileSync(updatePath, 'utf8'); const blocks = [ + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own command .md content, fixed-size author-controlled content ...content.matchAll(/([\s\S]*?)<\/execution_context(?:_extended)?>/g), ].map((m) => m[1]); assert.ok(blocks.length > 0, 'update.md must define at least one block'); @@ -381,6 +382,7 @@ describe('reapply-patches gated hunk verification (#1999)', () => { // assert it both names the table and defines an explicit gate // condition tied to the `verified` column. const content = fs.readFileSync(workflowPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored reapply-patches.md workflow, bounded prose, not adversarial input const step5Match = content.match(/^##\s+Step 5[^\r\n]*\r?\n([\s\S]*?)(?=^##\s|Z)/m); assert.ok(step5Match, 'reapply-patches workflow must contain a "## Step 5" section'); const step5 = step5Match[1]; @@ -405,6 +407,7 @@ describe('reapply-patches gated hunk verification (#1999)', () => { test('Step 5 also halts when the Hunk Verification Table is absent (Step 4 produced nothing)', () => { // Independent gate: missing-table is a separate halt path from any-no-row. const content = fs.readFileSync(workflowPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored reapply-patches.md workflow, bounded prose, not adversarial input const step5Match = content.match(/^##\s+Step 5[^\r\n]*\r?\n([\s\S]*?)(?=^##\s|Z)/m); assert.ok(step5Match, 'Step 5 section must exist'); const step5 = step5Match[1]; diff --git a/tests/research-agent-profiles.test.cjs b/tests/research-agent-profiles.test.cjs index 329c6893c..67f813b0c 100644 --- a/tests/research-agent-profiles.test.cjs +++ b/tests/research-agent-profiles.test.cjs @@ -413,6 +413,7 @@ describe('gsd-project-researcher agent registration (#2419)', () => { test('new-project.md lists gsd-project-researcher in available_agent_types', () => { const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const agentTypesMatch = content.match(/([\s\S]*?)<\/available_agent_types>/); assert.ok(agentTypesMatch, 'new-project.md must have section'); assert.ok( @@ -423,6 +424,7 @@ describe('gsd-project-researcher agent registration (#2419)', () => { test('new-milestone.md lists gsd-project-researcher in available_agent_types', () => { const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const agentTypesMatch = content.match(/([\s\S]*?)<\/available_agent_types>/); assert.ok(agentTypesMatch, 'new-milestone.md must have section'); assert.ok( diff --git a/tests/review-lane-invocation.test.cjs b/tests/review-lane-invocation.test.cjs index 3c558fdeb..81d4a9979 100644 --- a/tests/review-lane-invocation.test.cjs +++ b/tests/review-lane-invocation.test.cjs @@ -495,6 +495,7 @@ describe('#2358 review.md temp paths are run-scoped, not phase-only', () => { ); // The old isolation key must be gone entirely from path construction. assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored review.md workflow, bounded prose, not adversarial input !/\/tmp\/gsd-review[^\r\n]*\{phase\}/.test(reviewMdContent), 'no temp path may still be keyed on a bare {phase} placeholder' ); diff --git a/tests/roadmap.test.cjs b/tests/roadmap.test.cjs index 634b33b39..7ad7afdb9 100644 --- a/tests/roadmap.test.cjs +++ b/tests/roadmap.test.cjs @@ -1115,6 +1115,7 @@ describe('roadmap update-plan-progress command', () => { assert.ok(result.success, `Command failed: ${result.error}`); const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses ROADMAP.md the test itself wrote via a fixed fixture string, bounded, not adversarial input const rowMatch = roadmap.match(/^\|[^\r\n]*50\. Build[^\r\n]*$/m); assert.ok(rowMatch, 'table row should exist'); const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); @@ -1892,6 +1893,7 @@ const THREE_PLAN_ROADMAP = `# Roadmap describe('bug #2661: execute-plan.md update_roadmap gating', () => { const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); const stepMatch = content.match( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content /([\s\S]*?)<\/step>/ ); const step = stepMatch && stepMatch[1]; diff --git a/tests/runtime-name-policy.test.cjs b/tests/runtime-name-policy.test.cjs index 53b00b818..a1cfc940c 100644 --- a/tests/runtime-name-policy.test.cjs +++ b/tests/runtime-name-policy.test.cjs @@ -58,6 +58,7 @@ describe('runtime-name-policy windsurf alias parity — manifest vs FALLBACK_ALI // FALLBACK_ALIASES source text IS the product contract for runtimes that can't load the manifest at runtime; verifying // both surfaces contain the same windsurf aliases catches manual-mirror drift. const src = fs.readFileSync(srcPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded src/runtime-name-policy.cts source, not adversarial input const match = src.match(/windsurf:\s*\[([^\]]+)\]/); assert.ok(match, 'FALLBACK_ALIASES windsurf row must exist in src/runtime-name-policy.cts'); const srcAliases = match[1] diff --git a/tests/security-dead-exports.regression.test.cjs b/tests/security-dead-exports.regression.test.cjs index 708ed0b2f..f04e893f0 100644 --- a/tests/security-dead-exports.regression.test.cjs +++ b/tests/security-dead-exports.regression.test.cjs @@ -81,7 +81,9 @@ describe('#2198 regression: dead scan exports removed, docs corrected', () => { const fullPath = path.join(PROJECT_ROOT, relPath); const source = fs.readFileSync(fullPath, 'utf-8'); assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks/*.js source, not adversarial input !source.match(/require\s*\(\s*['"][^'"]*security\.(cjs|js)['"]\s*\)/) && + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks/*.js source, not adversarial input !source.match(/import\s+.*from\s+['"][^'"]*security\.(cjs|js)['"]\s*;?/), `${relPath} must not require/import security.cjs — hooks inline patterns for independence` ); diff --git a/tests/security-scan.security.test.cjs b/tests/security-scan.security.test.cjs index e1dec6d03..f15ad1228 100644 --- a/tests/security-scan.security.test.cjs +++ b/tests/security-scan.security.test.cjs @@ -563,6 +563,7 @@ describe('security-scan.yml workflow', () => { test('workflow does not use direct github context in run commands', () => { const content = fs.readFileSync(workflowPath, 'utf-8'); // Extract only run: blocks and check they don't contain ${{ }} + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own GitHub Actions workflow yml, fixed-size author-controlled content const runBlocks = content.match(/run:\s*\|?\s*\r?\n([\s\S]*?)(?=\r?\n\s*-|\r?\n\s*\w+:|Z)/g) || []; for (const block of runBlocks) { assert.ok( diff --git a/tests/settings-integrations.test.cjs b/tests/settings-integrations.test.cjs index 6d3cd031d..0dc3e7396 100644 --- a/tests/settings-integrations.test.cjs +++ b/tests/settings-integrations.test.cjs @@ -123,6 +123,7 @@ describe('#2529 workflow — agent_skills injection', () => { const src = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); assert.ok(src.includes('agent_skills'), 'workflow must reference agent_skills'); assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input /agent_skills\.<[^>]+>|agent_skills\.\w+/.test(src), 'workflow must reference agent_skills. or concrete agent_skills.' ); @@ -144,6 +145,7 @@ describe('#2529 workflow — API key masking', () => { assert.ok(src.includes('****'), 'workflow must document the **** mask pattern'); // Must explicitly state that plaintext is not displayed assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored workflow markdown, bounded prose, not adversarial input /never\s+(echo|display|log|show)[^.]*plaintext|plaintext[^.]*never\s+(echo|display|log|shown)|plaintext[^.]*not\s+(echoed|displayed|logged|shown)|not\s+(echoed|displayed|logged|shown)[^.]*plaintext/i.test(src), 'workflow must explicitly forbid displaying plaintext API keys' ); diff --git a/tests/skill-manifest.test.cjs b/tests/skill-manifest.test.cjs index 7f07920cd..1c9928507 100644 --- a/tests/skill-manifest.test.cjs +++ b/tests/skill-manifest.test.cjs @@ -577,6 +577,7 @@ describe('gsd-health --context flag is wired into command + workflow', () => { const raw = fs.readFileSync(HEALTH_WORKFLOW, 'utf-8'); // Extract just the context_check step's body so a stray reference // elsewhere in the file can't satisfy this assertion. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const stepMatch = raw.match(/([\s\S]*?)<\/step>/); assert.ok(stepMatch, 'context_check step must be a closed ... block'); const stepBody = stepMatch[1]; diff --git a/tests/state-rebuild-cli.test.cjs b/tests/state-rebuild-cli.test.cjs index 44fcc3c97..70f1030f6 100644 --- a/tests/state-rebuild-cli.test.cjs +++ b/tests/state-rebuild-cli.test.cjs @@ -117,6 +117,7 @@ function readLiveState(cwd) { const statePath = path.join(cwd, '.planning', 'STATE.md'); const content = fs.readFileSync(statePath, 'utf8'); // Strip ## Rebuild Log and everything after for shape assertions. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test wrote via a fixture, fixed-size test-controlled content return content.replace(/^## Rebuild Log[\s\S]*$/m, ''); } diff --git a/tests/state.test.cjs b/tests/state.test.cjs index b068ebe0c..67b9e2233 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -75,6 +75,7 @@ function captureStdout(fn) { function readShippedStateTemplateBody(replacements) { const templatePath = path.join(__dirname, '..', 'gsd-core', 'templates', 'state.md'); const template = fs.readFileSync(templatePath, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own state.md template, fixed-size author-controlled content const fencedDocument = template.match(/```markdown\r?\n([\s\S]*?)```/); assert.ok(fencedDocument, 'gsd-core/templates/state.md must contain a fenced markdown document'); @@ -1988,6 +1989,7 @@ describe('cmdStateResolveBlocker (state resolve-blocker)', () => { assert.ok(!updated.includes('- Single blocker'), 'resolved blocker should be removed'); // Section should contain "None" placeholder, not be empty + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const sectionMatch = updated.match(/## Blockers\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(sectionMatch, 'Blockers section should still exist'); assert.ok(sectionMatch[1].includes('None'), 'Blockers section should contain None placeholder'); @@ -2311,6 +2313,7 @@ Progress: [..........] 0% ); // Extract the Current Position section + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const posMatch = content.match(/## Current Position\s*\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(posMatch, 'Current Position section should exist'); const posSection = posMatch[1]; @@ -2412,6 +2415,7 @@ Progress: [..........] 0% const content = fs.readFileSync( path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8' ); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const posMatch = content.match(/## Current Position\s*\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(posMatch, 'Current Position section should exist after advance-plan'); const posSection = posMatch[1]; @@ -3115,6 +3119,7 @@ describe('#3052: planned-phase preserves same-date last_activity_desc', () => { const stateContent = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); // Extract only the frontmatter (between --- fences) to check the desc + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const fmMatch = stateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/); const frontmatter = fmMatch ? fmMatch[1] : ''; assert.ok( @@ -3210,6 +3215,7 @@ Progress: [##########] 20% ); // Current Position Status: line must also be "Ready to execute" + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const posMatch = stateContent.match(/## Current Position\s*\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(posMatch, 'Current Position section not found'); const posStatusMatch = posMatch[1].match(/^Status:\s*(.+)/m); @@ -3265,6 +3271,7 @@ Progress: [##########] 20% const stateContent = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); // Locate the Current Position section and verify the Status line there. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const posMatch = stateContent.match(/## Current Position\s*\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(posMatch, 'Current Position section not found'); const posStatusMatch = posMatch[1].match(/^Status:\s*(.+)/m); @@ -6139,6 +6146,7 @@ describe('#1255 — begin/complete-phase advance status for pipe-table STATE.md' const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8'); // Primary assertion: frontmatter status must advance to 'executing' + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const fmMatch = after.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, 'STATE.md must have YAML frontmatter after begin-phase'); const fm = fmMatch[1]; @@ -6181,6 +6189,7 @@ describe('#1255 — begin/complete-phase advance status for pipe-table STATE.md' const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8'); // Extract the ## Current Position section only, to avoid matching Configuration rows + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const cpMatch = after.match(/##\s*Current Position\s*\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(cpMatch, '## Current Position section must exist'); const cpSection = cpMatch[1]; @@ -6193,6 +6202,7 @@ describe('#1255 — begin/complete-phase advance status for pipe-table STATE.md' // Last activity cell must include date + narrative (not bare date) assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md generated by the tool under test against a bounded fixture project, not adversarial input /\|\s*Last activity\s*\|[^|]*—\s*Phase 1 execution started\s*\|/i.test(cpSection), `Current Position Last activity cell must include narrative '— Phase 1 execution started'; got Current Position:\n${cpSection}` ); @@ -6214,6 +6224,7 @@ describe('#1255 — begin/complete-phase advance status for pipe-table STATE.md' const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8'); // Primary assertion: frontmatter status must be 'completed' + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const fmMatch = after.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, 'STATE.md must have YAML frontmatter after complete-phase'); const fm = fmMatch[1]; @@ -6255,6 +6266,7 @@ describe('#1255 — begin/complete-phase advance status for pipe-table STATE.md' const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8'); // Extract the ## Current Position section only, to avoid matching Configuration rows + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const cpMatch = after.match(/##\s*Current Position\s*\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(cpMatch, '## Current Position section must exist'); const cpSection = cpMatch[1]; @@ -6277,6 +6289,7 @@ describe('#1255 — begin/complete-phase advance status for pipe-table STATE.md' // Bug 2: Last activity cell must include date + narrative (not bare date) assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md generated by the tool under test against a bounded fixture project, not adversarial input /\|\s*Last activity\s*\|[^|]*—\s*Phase 1 marked complete\s*\|/i.test(cpSection), `Current Position Last activity cell must include narrative '— Phase 1 marked complete'; got Current Position:\n${cpSection}` ); @@ -6316,6 +6329,7 @@ Last activity: 2026-06-01 -- Roadmap created ); assert.ok(result.success, `begin-phase failed on inline format: ${result.error || result.output}`); const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const fmMatch = after.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, 'must have frontmatter'); const fm = fmMatch[1]; @@ -6424,6 +6438,7 @@ describe('#1257 — planned-phase and begin-phase pipe-table regressions', () => // Extract the ## Configuration section (stops before ## Current Position) // to avoid false-positive from the Current Position table (which IS updated // by updateCurrentPositionFields). + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const cfgMatch = after.match(/##\s*Configuration\s*\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(cfgMatch, '## Configuration section must exist'); const cfgSection = cfgMatch[1]; @@ -6449,6 +6464,7 @@ describe('#1257 — planned-phase and begin-phase pipe-table regressions', () => ); const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const fmMatch = after.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, 'STATE.md must have YAML frontmatter after planned-phase'); const fm = fmMatch[1]; @@ -6480,12 +6496,14 @@ describe('#1257 — planned-phase and begin-phase pipe-table regressions', () => const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8'); // Extract ## Current Position section only + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const cpMatch = after.match(/##\s*Current Position\s*\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(cpMatch, '## Current Position section must exist'); const cpSection = cpMatch[1]; // The pipe-table Phase cell must be updated to reflect the executing phase assert.ok( + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md generated by the tool under test against a bounded fixture project, not adversarial input /\|\s*Phase\s*\|[^|]*1[^|]*EXECUTING[^|]*\|/i.test(cpSection), `Current Position pipe-table Phase cell must contain phase 1 EXECUTING; got Current Position:\n${cpSection}` ); @@ -6514,6 +6532,7 @@ describe('#1257 — planned-phase and begin-phase pipe-table regressions', () => const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8'); // Extract ## Current Position section only + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const cpMatch = after.match(/##\s*Current Position\s*\r?\n([\s\S]*?)(?=\r?\n##|$)/i); assert.ok(cpMatch, '## Current Position section must exist'); const cpSection = cpMatch[1]; @@ -11513,6 +11532,7 @@ describe('buildStateFrontmatter cache invalidation (#1967)', () => { // Read back and parse frontmatter to verify it reflects 2 phases, not 1 const result = fs.readFileSync(statePath, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses STATE.md this test just wrote via a fixture, fixed-size test-controlled content const fmMatch = result.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, 'STATE.md should have frontmatter after writeStateMd'); diff --git a/tests/trae-imperative-reference.test.cjs b/tests/trae-imperative-reference.test.cjs index fe5e5a149..1930f37b2 100644 --- a/tests/trae-imperative-reference.test.cjs +++ b/tests/trae-imperative-reference.test.cjs @@ -390,12 +390,14 @@ describe('#2658: trae runtime detection and instruction path (folded from fix-26 test('new-project.md recognizes /.trae/ path and TRAE_CONFIG_DIR before the claude fallback', () => { const content = fs.readFileSync(path.join(workflowsDir, 'new-project.md'), 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const pathBlock = content.match(/Derive `RUNTIME`[\s\S]*?Otherwise → `RUNTIME=claude`/); assert.ok(pathBlock, 'runtime-detection path block must exist'); assert.ok( /Path contains `\/\.trae\/` → `RUNTIME=trae`/.test(pathBlock[0]), 'path-based detection must recognize /.trae/ before the claude fallback', ); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const envBlock = content.match(/if \[ -n "\$CODEX_HOME" \][\s\S]*?else RUNTIME="claude"; fi/); assert.ok(envBlock, 'env-var fallback block must exist'); assert.ok( @@ -406,6 +408,7 @@ describe('#2658: trae runtime detection and instruction path (folded from fix-26 test('ingest-docs.md carries the same trae detection (found during this remediation, not just new-project.md)', () => { const content = fs.readFileSync(path.join(workflowsDir, 'ingest-docs.md'), 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const block = content.match(/\*\*Detect runtime\*\*[\s\S]*?else → `RUNTIME=claude`/); assert.ok(block, 'runtime-detection block must exist'); assert.ok( diff --git a/tests/update-custom-backup.test.cjs b/tests/update-custom-backup.test.cjs index 3913679c8..0eabd153c 100644 --- a/tests/update-custom-backup.test.cjs +++ b/tests/update-custom-backup.test.cjs @@ -690,6 +690,7 @@ describe('bug #3050: update backup skips unreadable files non-fatally', () => { 'utf8', ); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const hasTryCatch = /try\s*\{[\s\S]*copyFileSync\([\s\S]*\}[\s\S]*catch\s*\(err\)/.test(content); assert.ok(hasTryCatch, 'backup copy loop must catch per-file copy errors'); diff --git a/tests/update-workflow.test.cjs b/tests/update-workflow.test.cjs index 9f28e91ba..1149fc194 100644 --- a/tests/update-workflow.test.cjs +++ b/tests/update-workflow.test.cjs @@ -214,6 +214,7 @@ const src3130 = fs3130.readFileSync(UPDATE_WF_3130, 'utf8'); __t3130('bug #3130: update.md contains no bare npx invocations (cache-stale form)', () => { // Any occurrence of `npx -y @opengsd/gsd-core@` without `--package=` // is the stale form that triggers the two failure modes. + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored update.md workflow, bounded prose, not adversarial input const stale = (src3130.match(/npx -y @opengsd\/gsd-core@\S+[^\r\n]*/g) || []); assert3130.deepEqual( stale, diff --git a/tests/verification-status.test.cjs b/tests/verification-status.test.cjs index 9e45b8d44..e8a22741b 100644 --- a/tests/verification-status.test.cjs +++ b/tests/verification-status.test.cjs @@ -1292,6 +1292,7 @@ describe('#2868: verification status CLI drives the execute-phase stranded-phase // the jq form in order to explain why it is not used, and an assertion // over the whole file would fire on its own rationale. const content = fs.readFileSync(QUICK_VERIFICATION, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const fences = content.match(/```bash\r?\n[\s\S]*?```/g) || []; const statusFence = fences.find((f) => f.includes('gsd_run query verification.status')); diff --git a/tests/verifier-behavior-unverified.test.cjs b/tests/verifier-behavior-unverified.test.cjs index 3901a7e17..300960df3 100644 --- a/tests/verifier-behavior-unverified.test.cjs +++ b/tests/verifier-behavior-unverified.test.cjs @@ -82,6 +82,7 @@ test('PARITY: per-truth state never leaks into the overall-status vocabulary', ( test('overall-status enum in verification.cts is unchanged (no per-truth leak)', () => { const cts = fs.readFileSync(path.join(ROOT, 'src', 'verification.cts'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded src/verification.cts source, not adversarial input const m = cts.match(/VERIFIER_STATUSES[^=]*=\s*\[([^\]]*)\]/); assert.ok(m, 'VERIFIER_STATUSES array must be present'); assert.doesNotMatch(m[1], /present_behavior_unverified/i); diff --git a/tests/verify.test.cjs b/tests/verify.test.cjs index c40e6071b..ce616a0ff 100644 --- a/tests/verify.test.cjs +++ b/tests/verify.test.cjs @@ -2101,6 +2101,7 @@ describe('bug-967 verify key-links strict file-path contract', () => { // Also assert the corrected example actually uses a path-like value // (must contain at least one '/' and not start with 'http') + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored docs/reference/plan-md.md, bounded prose, not adversarial input const toMatch = content.match(/key_links:[\s\S]*?to:\s*"([^"]+)"/); assert.ok( toMatch, diff --git a/tests/workflow-fragments.test.cjs b/tests/workflow-fragments.test.cjs index 90842f963..ac1a0ca5a 100644 --- a/tests/workflow-fragments.test.cjs +++ b/tests/workflow-fragments.test.cjs @@ -574,6 +574,7 @@ describe('REASON enum and docs "Fails closed" bullets stay in parity', () => { const docPath = path.join(__dirname, '..', 'docs', 'reference', 'workflow-fragments.md'); const docText = fs.readFileSync(docPath, 'utf8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own docs .md content, fixed-size author-controlled content const sectionMatch = /## Fails closed\r?\n([\s\S]*?)\r?\n## /.exec(docText); assert.ok(sectionMatch, 'docs/reference/workflow-fragments.md must have a "## Fails closed" section'); const sectionText = sectionMatch[1]; diff --git a/tests/workflow-size-budget.test.cjs b/tests/workflow-size-budget.test.cjs index f74c4a859..fa85509c5 100644 --- a/tests/workflow-size-budget.test.cjs +++ b/tests/workflow-size-budget.test.cjs @@ -282,6 +282,7 @@ describe('SIZE: discuss-phase progressive disclosure (#717 byte budget)', () => const parent = fs.readFileSync(path.join(WORKFLOWS_DIR, 'discuss-phase.md'), 'utf-8'); // The template reference must appear inside or near the write_context step, // not in the top-level block (which would defeat lazy load). + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const requiredReadingMatch = parent.match(/([\s\S]*?)<\/required_reading>/); if (requiredReadingMatch) { assert.ok( @@ -471,6 +472,7 @@ describe('workflow progressive disclosure — MVP bodies lazy-loaded (#720)', () test('plan-phase.md does not list MVP bodies in ', () => { const planPhaseContent = fs.readFileSync(path.join(WORKFLOWS_DIR, 'plan-phase.md'), 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const requiredReadingMatch = planPhaseContent.match(/([\s\S]*?)<\/required_reading>/); if (requiredReadingMatch) { const block = requiredReadingMatch[1]; diff --git a/tests/workspace.test.cjs b/tests/workspace.test.cjs index 531e7ba26..384277d10 100644 --- a/tests/workspace.test.cjs +++ b/tests/workspace.test.cjs @@ -375,6 +375,7 @@ describe('workspace command files', () => { // Strip UTF-8 BOM if present (some editors inject on save under Windows); // a BOM byte at offset 0 defeats the ^--- anchor, making fmMatch null. const raw = fs.readFileSync(filePath, 'utf8').replace(/^\ufeff/, ''); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own command .md frontmatter, fixed-size author-controlled content const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); assert.ok(fmMatch, `${path.basename(filePath)} must start with a YAML frontmatter block`); const fm = {}; diff --git a/tests/worktree-cleanup.test.cjs b/tests/worktree-cleanup.test.cjs index 0ef835575..76ebbba05 100644 --- a/tests/worktree-cleanup.test.cjs +++ b/tests/worktree-cleanup.test.cjs @@ -801,6 +801,7 @@ describe('bug #48: orchestrator cwd-drift guard at execute_waves entry', () => { describe('bug #48: orchestrator fail-closed handling of verify-only halts', () => { const content = fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow .md content, fixed-size author-controlled content const withoutDispatchNote = content.replace(/[\s\S]*?<\/worktree_branch_check>/g, ''); test('orchestrator documents a fail-closed rule for executor exit 42 / FATAL (#48)', () => { assert.ok(/exit 42|FATAL/.test(withoutDispatchNote), 'execute-phase.md must reference executor exit 42 / FATAL outside the dispatch note (#48)'); diff --git a/tests/worktree-safety.test.cjs b/tests/worktree-safety.test.cjs index f31842410..b07886eba 100644 --- a/tests/worktree-safety.test.cjs +++ b/tests/worktree-safety.test.cjs @@ -6406,6 +6406,7 @@ test('bug-3542: gsd-executor.md prohibits `git stash` family inside worktrees', content, ); const hasGitShow = /`git show /i.test(content); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored gsd-executor.md agent markdown, bounded prose, not adversarial input const hasGitDiffRef = /`git diff [^`]*\$?\{?ref\}?|`git diff [A-Z]+:/i.test(content); assert.ok( hasThrowawayBranch || hasGitShow || hasGitDiffRef, diff --git a/tests/worktree.test.cjs b/tests/worktree.test.cjs index e45fc72be..ecc1c66ab 100644 --- a/tests/worktree.test.cjs +++ b/tests/worktree.test.cjs @@ -141,6 +141,7 @@ function _findCommandIndex(statements, predicate) { describe('canonical worktree-branch-check fragment is the single source of truth', () => { const fragmentContent = fs.readFileSync(WORKTREE_BRANCH_CHECK_FRAGMENT, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow fragment .md content, fixed-size author-controlled content const blockMatch = fragmentContent.match(/([\s\S]*?)<\/worktree_branch_check>/); const block = blockMatch ? blockMatch[1] : ''; @@ -244,6 +245,7 @@ describe('verify-only: worktree_branch_check must NOT run git reset (#48, supers const fragmentContent = fs.readFileSync(WORKTREE_BRANCH_CHECK_FRAGMENT, 'utf-8'); // Extract the worktree_branch_check block from the canonical fragment + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow fragment .md content, fixed-size author-controlled content const blockMatch = fragmentContent.match(/([\s\S]*?)<\/worktree_branch_check>/); assert.ok(blockMatch, 'worktree-branch-check.md must contain a block'); @@ -256,6 +258,7 @@ describe('verify-only: worktree_branch_check must NOT run git reset (#48, supers test('verify-only: execute-phase.md worktree_branch_check must not run git reset at all (#48, supersedes #2015)', () => { const fragmentContent = fs.readFileSync(WORKTREE_BRANCH_CHECK_FRAGMENT, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow fragment .md content, fixed-size author-controlled content const blockMatch = fragmentContent.match(/([\s\S]*?)<\/worktree_branch_check>/); assert.ok(blockMatch, 'worktree-branch-check.md must contain a block'); @@ -268,6 +271,7 @@ describe('verify-only: worktree_branch_check must NOT run git reset (#48, supers test('quick.md worktree_branch_check does not use reset --soft', () => { const fragmentContent = fs.readFileSync(WORKTREE_BRANCH_CHECK_FRAGMENT, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow fragment .md content, fixed-size author-controlled content const blockMatch = fragmentContent.match(/([\s\S]*?)<\/worktree_branch_check>/); assert.ok(blockMatch, 'worktree-branch-check.md must contain a block'); @@ -280,6 +284,7 @@ describe('verify-only: worktree_branch_check must NOT run git reset (#48, supers test('verify-only: quick.md worktree_branch_check must not run git reset at all (#48, supersedes #2015)', () => { const fragmentContent = fs.readFileSync(WORKTREE_BRANCH_CHECK_FRAGMENT, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow fragment .md content, fixed-size author-controlled content const blockMatch = fragmentContent.match(/([\s\S]*?)<\/worktree_branch_check>/); assert.ok(blockMatch, 'worktree-branch-check.md must contain a block'); @@ -372,6 +377,7 @@ describe('bug-2075: worktree deletion safeguards', () => { ); const fragmentContent = fs.readFileSync(WORKTREE_BRANCH_CHECK_FRAGMENT, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow fragment .md content, fixed-size author-controlled content const blockMatch = fragmentContent.match(/([\s\S]*?)<\/worktree_branch_check>/); assert.ok( blockMatch, @@ -397,6 +403,7 @@ describe('bug-2075: worktree deletion safeguards', () => { ); const fragmentContent = fs.readFileSync(WORKTREE_BRANCH_CHECK_FRAGMENT, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow fragment .md content, fixed-size author-controlled content const blockMatch = fragmentContent.match(/([\s\S]*?)<\/worktree_branch_check>/); assert.ok( blockMatch, @@ -422,6 +429,7 @@ describe('bug-2075: worktree deletion safeguards', () => { ); const fragmentContent = fs.readFileSync(WORKTREE_BRANCH_CHECK_FRAGMENT, 'utf-8'); + // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own workflow fragment .md content, fixed-size author-controlled content const blockMatch = fragmentContent.match(/([\s\S]*?)<\/worktree_branch_check>/); assert.ok(blockMatch, 'worktree-branch-check.md must contain a block'); const block = blockMatch[1];