Final convergence review found the shared <tag> seam introduced 3 behavior regressions; fixed all + locked with tests: - #557 REGRESSION: stripTaggedBlocks's attribute-tolerance stripped `<details open>` (the ACTIVE-milestone marker) that the old `<details>`-only regex preserved. The seam now takes `allowAttributes` (default false) — details/decisions strip is attr-INTOLERANT (preserves `<details open>`); only `<task type="…">` opts in. Regression test added to roadmap-parser + markdown-sectionizer suites. - verify.cts actionZones (negative-grep-echo security scan): reverted to a bounded to-first-close scan `<action>([\s\S]{0,20000}?)</action>` so a grep-echo trick can't hide behind an unterminated inner <action> (the seam's stop-at-next-open would drop it). ReDoS-safe via the cap. - check-command-router HTML-comment strip: `(?:-->|$)` fallback wiped to EOF (fail-closed spurious gate block) — replaced with stop-at-next-open so an unclosed `<!--` leaves downstream tags intact. - Updated the extractTaggedBlocks nested-tag tests to the new (stop-at-next-open) behavior: `<x><x>inner</x></x>` -> ['inner']. All vectors still linear; every fix verified in-process. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -206,7 +206,15 @@ function scanNegativeGrepCommentEcho(content: string): { errors: string[]; warni
|
||||
// while a prose echo on the same line is still caught.
|
||||
const cmdSpanRe =
|
||||
/grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n]*?(?:==|-eq|=)\s*0\b/g;
|
||||
const actionZones = extractTaggedBlocks(text, 'action');
|
||||
// Security scan: must see the FULL text up to the first </action> — including a
|
||||
// malformed inner <action> — so a grep-echo-0 trick cannot hide behind a
|
||||
// deliberately-unclosed tag. Use a bounded to-first-close scan (ReDoS-safe via
|
||||
// the {0,20000} cap, #2128), NOT the stop-at-next-open extractTaggedBlocks seam
|
||||
// (which would drop the span before an unterminated inner <action>).
|
||||
const actionZones: string[] = [];
|
||||
const actionRe = /<action>([\s\S]{0,20000}?)<\/action>/g;
|
||||
let acm: RegExpExecArray | null;
|
||||
while ((acm = actionRe.exec(text)) !== null) actionZones.push(acm[1]);
|
||||
const scannableActionText = actionZones.map((zone) => zone.replace(cmdSpanRe, ' ')).join('\n');
|
||||
|
||||
// 3. Per shell SEGMENT (split lines on && / ||) extract count-grep literals and
|
||||
@@ -363,7 +371,7 @@ function scanFileWideNegativeGateConflict(content: string): { warnings: string[]
|
||||
reqText: string; // <action>+<acceptance_criteria> text (requirement side)
|
||||
}
|
||||
const tasks: TaskInfo[] = [];
|
||||
for (const tc of extractTaggedBlocks(text, 'task')) {
|
||||
for (const tc of extractTaggedBlocks(text, 'task', true)) {
|
||||
// Extract task name.
|
||||
const namem = extractTaggedBlocks(tc, 'name');
|
||||
const name = namem.length ? namem[0].trim() : 'unnamed';
|
||||
@@ -565,7 +573,7 @@ function cmdVerifyPlanStructure(cwd: string, filePath: string, raw: boolean): vo
|
||||
}
|
||||
|
||||
const tasks: Record<string, unknown>[] = [];
|
||||
for (const taskContent of extractTaggedBlocks(content, 'task')) {
|
||||
for (const taskContent of extractTaggedBlocks(content, 'task', true)) {
|
||||
const nameArr = extractTaggedBlocks(taskContent, 'name');
|
||||
const taskName = nameArr.length ? nameArr[0].trim() : 'unnamed';
|
||||
const hasFiles = /<files>/.test(taskContent);
|
||||
|
||||
Reference in New Issue
Block a user