refactor(#1390): migrate check-command-router + gap-checker onto markdown-sectionizer seam (epic #1372 T3) (#1392)
- check-command-router: stripCommentsAndFences delegates fenced-code
stripping to seam's stripFencedCode; HTML-comment stripping stays
caller-side. extractPlanDesignatedSections replaces hand-rolled
split(/\r?\n/) + /^#{1,6}\s+/ heading walk with collectSections
driven by DESIGNATED_HEADINGS_RE.
- gap-checker: parseRequirements checkbox-bullet detection migrates
to iterateBullets (checkbox markers); **ID** extracted caller-side.
Table-row path and separator-row skip stay caller-side.
- Adds refactor-1390-t3-characterization.test.cjs (43 behavioral
tests) that were green before and remain green after.
- T1 fail-loud / could-not-parse gate semantics unchanged (verified
by decisions.test.cjs 59/59 and direct gate invocation).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import phaseLocatorMod = require('./phase-locator.cjs');
|
||||
const { findPhaseInternal } = phaseLocatorMod;
|
||||
import { extractDecisions } from './decisions.cjs';
|
||||
import type { Decision } from './decisions.cjs';
|
||||
import { stripFencedCode, collectSections } from './markdown-sectionizer.cjs';
|
||||
import { checkUiPresence } from './ui-safety-gate.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import verifyModule = require('./verify.cjs');
|
||||
@@ -134,10 +135,11 @@ const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objec
|
||||
const XML_DECISION_TAGS_RE = /<(?:objective|tasks?|action)(?:\s[^>]*)?>([\s\S]*?)<\/(?:objective|tasks?|action)>/gi;
|
||||
|
||||
function stripCommentsAndFences(text: string): string {
|
||||
return text
|
||||
.replace(/<!--[\s\S]*?-->/g, ' ')
|
||||
.replace(/```[\s\S]*?```/g, ' ')
|
||||
.replace(/~~~[\s\S]*?~~~/g, ' ');
|
||||
// HTML-comment stripping stays caller-side (the seam does not strip HTML comments).
|
||||
const htmlStripped = text.replace(/<!--[\s\S]*?-->/g, ' ');
|
||||
// Fenced-code stripping: delegate to the canonical CommonMark-correct seam.
|
||||
// replaces the prior independent regex copy (```` ``` ``` ```` + `~~~ ~~~`).
|
||||
return stripFencedCode(htmlStripped).text;
|
||||
}
|
||||
|
||||
function extractYamlBlock(frontmatter: string, key: string): string {
|
||||
@@ -174,16 +176,18 @@ function extractPlanDesignatedSections(planContent: string | null | undefined):
|
||||
if (block) parts.push(block);
|
||||
}
|
||||
|
||||
// Replace hand-rolled split(/\r?\n/) + heading walk with the seam's collectSections.
|
||||
// stopPredicate fires on EVERY heading (collectSections needs to start a section at
|
||||
// each heading), then we filter to designated ones — same semantics as the prior
|
||||
// inDesignated flag: emit the heading line + body only when DESIGNATED_HEADINGS_RE matches.
|
||||
const sections = collectSections(body, () => true);
|
||||
const bodyParts: string[] = [];
|
||||
let inDesignated = false;
|
||||
for (const line of body.split(/\r?\n/)) {
|
||||
const heading = /^#{1,6}\s+/.test(line);
|
||||
if (heading) {
|
||||
inDesignated = DESIGNATED_HEADINGS_RE.test(line);
|
||||
if (inDesignated) bodyParts.push(line);
|
||||
continue;
|
||||
for (const section of sections) {
|
||||
const headingLine = '#'.repeat(section.heading.level) + ' ' + section.heading.text;
|
||||
if (DESIGNATED_HEADINGS_RE.test(headingLine)) {
|
||||
bodyParts.push(headingLine);
|
||||
if (section.body) bodyParts.push(section.body);
|
||||
}
|
||||
if (inDesignated) bodyParts.push(line);
|
||||
}
|
||||
parts.push(bodyParts.join('\n'));
|
||||
parts.push(extractXmlTagBodies(cleaned));
|
||||
|
||||
@@ -28,6 +28,7 @@ const { escapeRegex } = phaseId;
|
||||
import planningWorkspace = require('./planning-workspace.cjs');
|
||||
const { planningPaths, planningDir, findContextMdIn } = planningWorkspace;
|
||||
import { parseDecisions, extractDecisions } from './decisions.cjs';
|
||||
import { iterateBullets } from './markdown-sectionizer.cjs';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -81,18 +82,26 @@ function parseRequirements(reqMd: unknown): ReqItem[] {
|
||||
|
||||
// Prefix-agnostic ID format: REQ-01, TST-01, BACK-07, INSP-04, etc.
|
||||
const ID_PATTERN = '[A-Z][A-Z0-9]*-[A-Za-z0-9_-]+';
|
||||
const idRe = new RegExp(`^(${ID_PATTERN})$`);
|
||||
|
||||
const checkboxRe = new RegExp(`^\\s*-\\s*\\[[x ]\\]\\s*\\*\\*(${ID_PATTERN})\\*\\*\\s*(.*)$`, 'gm');
|
||||
let cm = checkboxRe.exec(reqMd);
|
||||
while (cm !== null) {
|
||||
const id = cm[1];
|
||||
// Checkbox-bullet path: migrate to seam's iterateBullets (checkbox markers).
|
||||
// The **ID** is extracted from the bullet text caller-side — the seam provides
|
||||
// the raw text; we parse the bold-ID prefix from it here.
|
||||
const boldIdRe = new RegExp(`^\\*\\*(${ID_PATTERN})\\*\\*\\s*(.*)$`);
|
||||
for (const bullet of iterateBullets(reqMd)) {
|
||||
if (bullet.marker !== 'checkbox-unchecked' && bullet.marker !== 'checkbox-checked') continue;
|
||||
const m = boldIdRe.exec(bullet.text);
|
||||
if (!m) continue;
|
||||
const id = m[1];
|
||||
if (!idRe.test(id)) continue;
|
||||
if (!seen.has(id)) {
|
||||
seen.add(id);
|
||||
out.push({ id, text: (cm[2] || '').trim() });
|
||||
out.push({ id, text: (m[2] || '').trim() });
|
||||
}
|
||||
cm = checkboxRe.exec(reqMd);
|
||||
}
|
||||
|
||||
// Pipe-table-row path and separator-row skip stay caller-side
|
||||
// (table parsing is out of seam scope per ADR-1372 T3 spec).
|
||||
const tableFirstCellRe = new RegExp(`^\\s*\\|\\s*(${ID_PATTERN})\\s*\\|`);
|
||||
const separatorRowRe = /^\s*\|[\s:|-]+\|\s*$/;
|
||||
const lines = reqMd.split(/\r?\n/);
|
||||
|
||||
Reference in New Issue
Block a user