* test(#3577): pin table-declared phase resolution across all four surfaces Failing-first regression for #3577: a GFM table phase listing (Phase header, id in the first data cell) declared real phases that roadmap.analyze, roadmap.get-phase, init.phase-op, and the milestone filter all reported as absent (phase_count: 0 / found: false). Rows pin the lookup, the scope probe, the analyzer, schema discrimination against the canonical RoadmapProgress table, fenced-example exclusion, heading+table union without double-count, decimal ids, and the 999 icebox exclusion. * fix(#3577): recognize markdown-table phase rows across the enumeration family A GFM table whose header leads with Phase and whose data rows carry the id in the first cell is a phase listing — the #2199 bullet blind spot's table sibling. collectTablePhaseRows (schema-discriminated against the canonical RoadmapProgress table via matchTableSchema, fence-aware via stripFencedCode, digit-bearing id shape, 999 icebox excluded) now feeds: the milestone filter's sole owner scanMilestonePhaseIds, window classification hasPhaseEntries, both roadmap lookup chains (getRoadmapPhaseInternal + cmdRoadmapGetPhase, as last-resort tiers after heading and bullet), and roadmap analyze's enumerator (with the same disk enrichment contract as headings and a zero-pad-tolerant duplicate guard). init.phase-op resolves through its existing getRoadmapPhaseInternal fallback. * fix(#3577): GFM table termination + icebox word boundary in the table scan Review findings: the row harvest broke only on blank lines, so prose after a table (a bare date line) could be harvested as a phase id — rows now stop at the first non-row line per GFM semantics; the 999 icebox exclusion gains the heading scan's word boundary so 9991 is kept. * fix(#3577): sanction collectTablePhaseRows in the enumeration drift scanner The scan's local 999-only exclusion mirrors its parent owner scanMilestonePhaseIds' deliberate NOT-isSentinelPhaseId choice (a leading 0 is a real decimal phase, #2554), so it cannot route through the sentinel owner — function-scoped exemption with the documented reason, same entry shape as the #3262 owner's. * chore(#3577): add changeset fragment * chore(#3577): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -26,7 +26,7 @@ const { SCOPE } = planningScopeMod;
|
||||
type Scope = planningScopeMod.Scope;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import roadmapParserModule = require('./roadmap-parser.cjs');
|
||||
const { stripShippedMilestones, extractCurrentMilestone, extractCurrentMilestoneScoped, replaceInCurrentMilestone, listMilestoneHeadings, scanMilestonePhaseIds } = roadmapParserModule;
|
||||
const { stripShippedMilestones, extractCurrentMilestone, extractCurrentMilestoneScoped, replaceInCurrentMilestone, listMilestoneHeadings, scanMilestonePhaseIds, collectTablePhaseRows } = roadmapParserModule;
|
||||
import { tokenizeHeadings } from './markdown-sectionizer.cjs';
|
||||
import { updateTableCell } from './markdown-table.cjs';
|
||||
import { clampPercent } from './phase-lifecycle.cjs';
|
||||
@@ -311,6 +311,22 @@ function cmdRoadmapGetPhase(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
if (!malformed) malformed = (milestoneResult?.error ? milestoneResult : (fullResult?.error ? fullResult : null));
|
||||
}
|
||||
|
||||
// #3577: no heading or checklist entry matched — fall back to a
|
||||
// markdown-table row declaration (the same last-resort tier
|
||||
// getRoadmapPhaseInternal gained). Zero-pad-tolerant id compare (#3572
|
||||
// lesson: the declared form may be padded).
|
||||
const stripPad = (s: string) => s.replace(/^0+(?=.)/, '');
|
||||
const tableHit = collectTablePhaseRows(milestoneContent).find((tr) => stripPad(tr.id) === stripPad(phaseNum))
|
||||
?? collectTablePhaseRows(fullContent).find((tr) => stripPad(tr.id) === stripPad(phaseNum));
|
||||
if (tableHit) {
|
||||
output(
|
||||
{ found: true, phase_number: phaseNum, phase_name: tableHit.name ?? `Phase ${tableHit.id}`, goal: null, section: tableHit.row.trim() },
|
||||
raw,
|
||||
tableHit.row.trim(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (malformed) {
|
||||
output(malformed, raw, '');
|
||||
return;
|
||||
@@ -459,6 +475,41 @@ function collectAnalyzePhases(content: string, phasesDir: string, phaseDirNames:
|
||||
roadmap_complete: roadmapComplete,
|
||||
});
|
||||
}
|
||||
|
||||
// #3577: markdown-table row declarations join the enumeration — same
|
||||
// enrichment contract as headings (disk counts when the directory exists),
|
||||
// zero-pad-tolerant duplicate guard so an id declared in BOTH a heading and
|
||||
// a table counts once.
|
||||
const stripPadA = (s: string) => s.replace(/^0+(?=.)/, '');
|
||||
const seen = new Set(phases.map((ph) => stripPadA(ph.number)));
|
||||
for (const tr of collectTablePhaseRows(content)) {
|
||||
if (seen.has(stripPadA(tr.id))) continue;
|
||||
const dirMatchA = matchPhaseDirs(phaseDirNames, normalizePhaseName(tr.id)).matches[0];
|
||||
let tPlanCount = 0;
|
||||
let tSummaryCount = 0;
|
||||
let tHasContext = false;
|
||||
let tHasResearch = false;
|
||||
if (dirMatchA) {
|
||||
const counts = countPhasePlansAndSummaries(path.join(phasesDir, dirMatchA));
|
||||
tPlanCount = counts.planCount;
|
||||
tSummaryCount = counts.summaryCount;
|
||||
tHasContext = fs.existsSync(path.join(phasesDir, dirMatchA, 'CONTEXT.md'));
|
||||
tHasResearch = fs.existsSync(path.join(phasesDir, dirMatchA, 'RESEARCH.md'));
|
||||
}
|
||||
phases.push({
|
||||
number: tr.id,
|
||||
name: tr.name ?? `Phase ${tr.id}`,
|
||||
goal: null,
|
||||
mode: null,
|
||||
depends_on: null,
|
||||
plan_count: tPlanCount,
|
||||
summary_count: tSummaryCount,
|
||||
has_context: tHasContext,
|
||||
has_research: tHasResearch,
|
||||
disk_status: dirMatchA ? 'ok' : 'no_directory',
|
||||
roadmap_complete: false,
|
||||
});
|
||||
}
|
||||
return phases;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user