admin_reason: missing-secondary-reviewer — self-authored overnight sweep; isolated adversarial review round completed (MEDIUM table-bleed finding fixed with RED/GREEN evidence) and sha-pinned bench 46331/0 on the merged head.
This commit is contained in:
26
src/init.cts
26
src/init.cts
@@ -141,7 +141,6 @@ const { resolveCapabilityRuntimeState } = capabilityStateMod;
|
||||
void stripShippedMilestones;
|
||||
|
||||
// Accept all bold/colon variants of the Requirements header (#2769)
|
||||
const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
|
||||
|
||||
// #2056/#2104: isForeignPrefixedPhaseQuery is imported from phase-id.cts
|
||||
// (the canonical predicate). parsePhasePrefix is no longer needed locally.
|
||||
@@ -939,9 +938,14 @@ function cmdInitExecutePhase(
|
||||
has_reviews: false,
|
||||
};
|
||||
});
|
||||
const reqMatch = (roadmapPhase?.['section'] as string | undefined)?.match(REQUIREMENTS_HEADER_RE);
|
||||
const reqExtracted = reqMatch
|
||||
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
||||
// #4731: multiline-aware — the Requirements field may hard-wrap, so the
|
||||
// value is extracted past the line break before the ID scan.
|
||||
const phaseSection = roadmapPhase?.['section'] as string | undefined;
|
||||
const reqLine = phaseSection
|
||||
? roadmapParser.extractPhaseFieldMultiline(phaseSection, 'Requirements')
|
||||
: null;
|
||||
const reqExtracted = reqLine
|
||||
? reqLine.replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
||||
: null;
|
||||
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
||||
|
||||
@@ -1109,9 +1113,14 @@ function cmdInitPlanPhase(
|
||||
has_reviews: false,
|
||||
};
|
||||
});
|
||||
const reqMatch = (roadmapPhase?.['section'] as string | undefined)?.match(REQUIREMENTS_HEADER_RE);
|
||||
const reqExtracted = reqMatch
|
||||
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
||||
// #4731: multiline-aware — the Requirements field may hard-wrap, so the
|
||||
// value is extracted past the line break before the ID scan.
|
||||
const phaseSection = roadmapPhase?.['section'] as string | undefined;
|
||||
const reqLine = phaseSection
|
||||
? roadmapParser.extractPhaseFieldMultiline(phaseSection, 'Requirements')
|
||||
: null;
|
||||
const reqExtracted = reqLine
|
||||
? reqLine.replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
||||
: null;
|
||||
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
||||
|
||||
@@ -2841,8 +2850,7 @@ function cmdInitManager(cwd: string, raw: boolean): void {
|
||||
: content.length;
|
||||
const section = content.slice(sectionStart, sectionEnd);
|
||||
|
||||
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
||||
const goal = goalMatch ? goalMatch[1].trim() : null;
|
||||
const goal = roadmapParser.extractPhaseFieldMultiline(section, 'Goal');
|
||||
|
||||
const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
||||
const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
|
||||
|
||||
@@ -3896,16 +3896,22 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
);
|
||||
|
||||
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
||||
const reqMatch = sectionText.match(
|
||||
/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i,
|
||||
);
|
||||
// #4731: multiline-aware — hard-wrapped Requirements read past the
|
||||
// line break before the ID scan. The shared extractor also stops at
|
||||
// headings and table rows, so a Requirements field followed by the
|
||||
// Traceability table cannot bleed other phases' REQ-IDs into the
|
||||
// citation scan (isolated-review MEDIUM on the inline lookahead,
|
||||
// whose lazy capture swallowed everything to section end).
|
||||
const reqLine = sectionText
|
||||
? roadmapParserMod.extractPhaseFieldMultiline(sectionText, 'Requirements')
|
||||
: null;
|
||||
|
||||
const originalReqContent = fs.readFileSync(reqPath, 'utf-8');
|
||||
let reqContent = originalReqContent;
|
||||
|
||||
// #2316: `citedReqIds` — the REQ-IDs ROADMAP's own **Requirements:**
|
||||
// line for this phase actually cites — is hoisted out of the
|
||||
// `if (reqMatch)` block (previously scoped only inside it) so the
|
||||
// `if (reqLine)` block (previously scoped only inside it) so the
|
||||
// ghost-ID cross-check below (~#2316-1) can consult it. `TBD` is the
|
||||
// literal placeholder `phase.add`/`-batch`/`-insert` seed
|
||||
// (`**Requirements**: TBD`, src/phase.cts:833,920,1078) — never a
|
||||
@@ -3918,18 +3924,18 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
// `else`, discarding this fact silently instead of surfacing it.
|
||||
const traceabilityWriteMisses: string[] = [];
|
||||
|
||||
if (reqMatch) {
|
||||
if (reqLine) {
|
||||
// #2334 HIGH 3 + #3697: selection and under-selection detection both
|
||||
// live in `analyzeRequirementsLine` (module scope, above), extracted in
|
||||
// round 3 so the parser is directly testable — a closure in here is
|
||||
// reachable only by spawning the CLI, which no fast-check property test
|
||||
// can do. `citedReqIds` is byte-identical to the expression that stood
|
||||
// here; nothing about what phase-complete MARKS has changed.
|
||||
const reqLineAnalysis = analyzeRequirementsLine(reqMatch[1]);
|
||||
const reqLineAnalysis = analyzeRequirementsLine(reqLine);
|
||||
citedReqIds = reqLineAnalysis.citedReqIds;
|
||||
const reqLineWarning = formatRequirementsLineWarning(
|
||||
phaseNum,
|
||||
reqMatch[1],
|
||||
reqLine,
|
||||
reqLineAnalysis,
|
||||
);
|
||||
if (reqLineWarning) {
|
||||
|
||||
@@ -2255,5 +2255,45 @@ export = {
|
||||
// #3641: the scope axis's phase-ENTRY predicate, exported so roadmap
|
||||
// validate's V004 document-level check routes through the same single
|
||||
// owner (and its convention gate) instead of a private inline copy.
|
||||
extractPhaseFieldMultiline,
|
||||
hasPhaseEntries,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* #4731 — extract a bold-labeled phase field (Goal, Requirements, ...) reading
|
||||
* past hard-wrapped continuation lines. The roadmapper soft-wraps long fields
|
||||
* at ~85 chars, so a single-line capture silently truncated every wrapped
|
||||
* Goal/Requirements. Stops at: another `**Field**` label line, a blank line,
|
||||
* or any heading. Wrapped lines are trimmed and joined with single spaces; a
|
||||
* single-line field is returned unchanged (trimmed). Returns null when the
|
||||
* label is absent.
|
||||
*/
|
||||
function extractPhaseFieldMultiline(section: string, label: string): string | null {
|
||||
// #2769 label shapes: `**X:**`, `**X**:`, and the spaced `**X** :`.
|
||||
const labelRe = new RegExp(
|
||||
'\\*\\*' + label + '(?::\\*\\*|\\*\\*\\s*:?)\\s*([^\\n]+)',
|
||||
'i',
|
||||
);
|
||||
const match = section.match(labelRe);
|
||||
if (!match) return null;
|
||||
const startIdx = match.index ?? 0;
|
||||
const after = section.slice(startIdx + match[0].length);
|
||||
const firstLine = match[1].trim();
|
||||
const contLines = [];
|
||||
const lines = after.split('\n');
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
const raw = lines[li];
|
||||
// The split's first element is the remainder of the captured first line's
|
||||
// own newline — an empty leading element is the line break, not a blank
|
||||
// continuation line.
|
||||
if (li === 0 && !raw.trim()) continue;
|
||||
if (!raw.trim()) break;
|
||||
if (/^\s*\*\*[A-Z][A-Za-z ]*:?(\*\*)?:?\s/.test(raw)) break;
|
||||
if (/^\s*#{1,4}\s/.test(raw)) break;
|
||||
if (/^\s*\|/.test(raw)) break;
|
||||
contLines.push(raw.trim());
|
||||
}
|
||||
return [firstLine, ...contLines].join(' ').trim() || null;
|
||||
}
|
||||
|
||||
|
||||
@@ -231,9 +231,9 @@ function searchPhaseInContent(content: string, escapedPhase: string, phaseNum: s
|
||||
|
||||
const section = content.slice(headerIndex, sectionEnd).trim();
|
||||
|
||||
// Extract goal if present (supports both **Goal:** and **Goal**: formats)
|
||||
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
||||
const goal = goalMatch ? goalMatch[1].trim() : null;
|
||||
// Extract goal if present (supports both **Goal:** and **Goal**: formats).
|
||||
// #4731: multiline-aware — hard-wrapped Goals read past the line break.
|
||||
const goal = roadmapParserModule.extractPhaseFieldMultiline(section, 'Goal');
|
||||
|
||||
// Mode: vertical-MVP slice mode flag. Lowercased + trimmed for canonical
|
||||
// comparison; unrecognized values are preserved verbatim for forward-compat.
|
||||
@@ -509,8 +509,7 @@ function collectAnalyzePhases(
|
||||
const sectionEnd = nextHeader ? sectionStart + nextHeader.index! : content.length;
|
||||
const section = content.slice(sectionStart, sectionEnd);
|
||||
|
||||
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
||||
const goal = goalMatch ? goalMatch[1].trim() : null;
|
||||
const goal = roadmapParserModule.extractPhaseFieldMultiline(section, 'Goal');
|
||||
|
||||
const modeMatch = section.match(/\*\*Mode(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
||||
const mode = modeMatch ? modeMatch[1].trim().toLowerCase() : null;
|
||||
@@ -1645,6 +1644,7 @@ function cmdRoadmapAnnotateDependencies(cwd: string, phaseNum: string | null | u
|
||||
}, raw, updated ? `annotated ${waves.length} wave(s), ${crossCuttingTruths.length} constraint(s)` : 'skipped (already annotated or no plan list)');
|
||||
}
|
||||
|
||||
|
||||
export = {
|
||||
cmdRoadmapGetPhase,
|
||||
getRoadmapPhaseWithFallback,
|
||||
|
||||
Reference in New Issue
Block a user