* fix(#1159): phase complete ignores historical/deferred requirement metadata Defect A: `phase complete` emitted a false "has unresolved gaps" warning when a VERIFICATION.md file's frontmatter contained `status: passed` but the body contained `previous_status: gaps_found`. The full-text regex `/status: gaps_found/` matched the substring inside `previous_status: gaps_found`, producing a spurious warning. Fixed by reading only the frontmatter `status` key via `extractFrontmatter` in both `phase.cts` (phase complete warning check) and `commands.cts` (determinePhaseStatus). Defect B: requirement IDs under explicitly deferred/backlog/future/v2 section headings in REQUIREMENTS.md were flagged as missing from the Traceability table. Fixed by splitting the body into markdown sections, detecting deferred-intent headings via a keyword regex, and skipping those sections when collecting IDs to check against the table. Both fixes include boundary tests (false positive suppressed; genuine gap/missing warnings still fire) in 4-phase-complete-cjs-regression.test.cjs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#1159): address adversarial-review findings (subheading + case sensitivity) - Defect B: Replace section-split approach with line-by-line depth-tracking to correctly propagate deferred status to sub-headings and ignore headings inside fenced code blocks. Previously `## Future Backlog` / `### Sub` would leak sub-heading IDs (split created a new non-deferred section per heading). - Defect A/commands: restore case-insensitive status matching via toLowerCase() to match the prior /status:\s*passed/i regex semantics. - Add test #1159-B-4 covering the nested-subheading case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changeset): backfill PR number for #1159 fix Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -110,9 +110,17 @@ function determinePhaseStatus(plans: number, summaries: number, phaseDir: string
|
||||
const verificationFile = files.find(f => f === 'VERIFICATION.md' || f.endsWith('-VERIFICATION.md'));
|
||||
if (verificationFile) {
|
||||
const content = platformReadSync(path.join(phaseDir, verificationFile)) || '';
|
||||
if (/status:\s*passed/i.test(content)) return 'Complete';
|
||||
if (/status:\s*human_needed/i.test(content)) return 'Needs Review';
|
||||
if (/status:\s*gaps_found/i.test(content)) return 'Executed';
|
||||
// #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false
|
||||
// matches from historical body metadata such as `previous_status: gaps_found`.
|
||||
// Full-text regexes like /status:\s*gaps_found/ match the substring inside
|
||||
// `previous_status: gaps_found`, producing incorrect phase status labels.
|
||||
const fm = extractFrontmatter(content) as Record<string, unknown>;
|
||||
// Normalise to lower-case to preserve the prior case-insensitive behaviour
|
||||
// while reading only the frontmatter `status` key (not the full body text).
|
||||
const fmStatus = typeof fm['status'] === 'string' ? fm['status'].trim().toLowerCase() : '';
|
||||
if (fmStatus === 'passed') return 'Complete';
|
||||
if (fmStatus === 'human_needed') return 'Needs Review';
|
||||
if (fmStatus === 'gaps_found') return 'Executed';
|
||||
// Verification exists but unrecognized status — treat as executed
|
||||
return 'Executed';
|
||||
}
|
||||
|
||||
@@ -1373,8 +1373,16 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
(f) => f.includes('-VERIFICATION') && f.endsWith('.md'),
|
||||
)) {
|
||||
const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
|
||||
if (/status: human_needed/.test(content)) warnings.push(`${file}: needs human verification`);
|
||||
if (/status: gaps_found/.test(content)) warnings.push(`${file}: has unresolved gaps`);
|
||||
// #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false positives
|
||||
// from historical metadata in the file body (e.g. `previous_status: gaps_found`).
|
||||
// A full-text regex like /status: gaps_found/ matches the substring inside
|
||||
// `previous_status: gaps_found`, producing spurious warnings even when the
|
||||
// current frontmatter status is `passed`.
|
||||
const verFm = extractFrontmatter(content) as Record<string, unknown>;
|
||||
// Normalise to lower-case so `status: Passed` (title-case) is not missed.
|
||||
const verStatus = typeof verFm['status'] === 'string' ? verFm['status'].trim().toLowerCase() : '';
|
||||
if (verStatus === 'human_needed') warnings.push(`${file}: needs human verification`);
|
||||
if (verStatus === 'gaps_found') warnings.push(`${file}: has unresolved gaps`);
|
||||
}
|
||||
} catch {
|
||||
/* intentionally empty */
|
||||
@@ -1495,12 +1503,58 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
}
|
||||
}
|
||||
|
||||
// #1159 (Defect B): collect requirement IDs only from ACTIVE sections.
|
||||
// Requirements under headings whose text contains "deferred", "backlog",
|
||||
// "future", or "v2" (case-insensitive) are explicitly out of current scope
|
||||
// and must not be flagged as missing from the Traceability table.
|
||||
//
|
||||
// Strategy: walk lines, track heading depth, and toggle a "deferred" flag
|
||||
// when a heading matching the pattern is encountered. A sub-heading (higher
|
||||
// depth) that is ITSELF in a deferred parent remains deferred unless it
|
||||
// opens a same-or-shallower heading that does NOT match the pattern.
|
||||
// Lines inside fenced code blocks (``` or ~~~) are treated as content, not
|
||||
// headings, to avoid false deferred-section detection from code examples.
|
||||
const DEFERRED_HEADING_RE = /\b(?:deferred|backlog|future|v\d+)\b/i;
|
||||
const bodyReqIds: string[] = [];
|
||||
const bodyReqPattern = /\*\*([A-Z][A-Z0-9]*-\d+)\*\*/g;
|
||||
let bodyMatch: RegExpExecArray | null;
|
||||
while ((bodyMatch = bodyReqPattern.exec(reqContent)) !== null) {
|
||||
const id = bodyMatch[1];
|
||||
if (!bodyReqIds.includes(id)) bodyReqIds.push(id);
|
||||
// deferredDepth: the heading level that opened the current deferred block,
|
||||
// or 0 when we are in an active section.
|
||||
let deferredDepth = 0;
|
||||
let inFence = false;
|
||||
for (const line of reqContent.split(/\r?\n/)) {
|
||||
// Track fenced code blocks (``` or ~~~).
|
||||
if (/^\s*(?:```|~~~)/.test(line)) {
|
||||
inFence = !inFence;
|
||||
continue;
|
||||
}
|
||||
if (inFence) continue; // ignore content inside a code fence
|
||||
|
||||
const headingM = line.match(/^(#{1,6})\s+(.*)/);
|
||||
if (headingM) {
|
||||
const depth = headingM[1].length;
|
||||
const text = headingM[2];
|
||||
if (deferredDepth > 0 && depth > deferredDepth) {
|
||||
// Sub-heading inside a deferred block: stays deferred regardless of name.
|
||||
continue;
|
||||
}
|
||||
// Heading at same level or shallower than current deferred opener,
|
||||
// or no active deferred block yet.
|
||||
if (DEFERRED_HEADING_RE.test(text)) {
|
||||
deferredDepth = depth; // enter a deferred block
|
||||
} else {
|
||||
deferredDepth = 0; // back in an active section
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deferredDepth > 0) continue; // skip content in deferred sections
|
||||
|
||||
// Collect bold REQ-ID patterns from active-section lines.
|
||||
const reqPat = /\*\*([A-Z][A-Z0-9]*-\d+)\*\*/g;
|
||||
let bodyMatch: RegExpExecArray | null;
|
||||
while ((bodyMatch = reqPat.exec(line)) !== null) {
|
||||
const id = bodyMatch[1];
|
||||
if (!bodyReqIds.includes(id)) bodyReqIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
const traceabilityHeadingMatch = reqContent.match(/^#{1,6}\s+Traceability\b/im);
|
||||
|
||||
Reference in New Issue
Block a user