* fix(#1988): exclude stray non-plan *-SUMMARY.md from phase completion count Stray remediation/gap-closure summaries (30-FIX-CR02-SUMMARY.md, 30-GAPCLOSURE-SUMMARY.md, …) inflated summary_count, and once summary_count >= plan_count the phase silently flipped to Complete even though several plans had no summary. A summary now counts toward completion only if it pairs with a real plan file. - core-utils.cts: new countMatchedSummaries(planFiles, summaryFiles) — layout-agnostic pairing via the PLAN→SUMMARY marker swap (root/nested/bare) plus the <stem>-SUMMARY.md form (bare PLAN.md↔PLAN-SUMMARY.md); the swap is applied to the basename only so a 'plans/' dir prefix isn't corrupted. - plan-scan.cts: scanPhasePlans.summaryCount/.completed use the matched count (summaryFiles array still holds every summary on disk for listing/reading). Fixes roadmap listing, state sync, verification, workstream inventory. - roadmap.cts: cmdRoadmapUpdatePlanProgress uses the matched count. - tests/roadmap.test.cjs: countMatchedSummaries unit tests (root/nested/bare/ stray) + E2E reproducing the exact #1988 report (4 plans, 1 plan summary, 3 strays → 1/4 In Progress, NOT Complete). Closes #1988 * docs(#1988): backfill changeset pr 2016 * test(#1988): strengthen countMatchedSummaries unit tests for mutation coverage Add direct unit tests for the extended (N-PLAN-MM-slug↔N-MM-SUMMARY), bare (PLAN↔SUMMARY, PLAN↔PLAN-SUMMARY), legacy (N-PLAN-NN↔N-PLAN-NN-SUMMARY), and stray-exclusion pairings so every branch of countMatchedSummaries is exercised (Stryker mutation-score coverage). * test(#1988): move countMatchedSummaries unit tests into core-utils.test.cjs The Stryker core-utils shard runs ONLY tests/core-utils.test.cjs (per scripts/mutation-matrix.cjs), so the unit tests for countMatchedSummaries must live there to be mutation-covered (previously in roadmap.test.cjs, the shard never ran them → mutants survived → Stryker gate failed). The E2E #1988 reproduction stays in roadmap.test.cjs. Added an absolute-path case to guard the lastIndexOf('/') >= 0 boundary.
This commit is contained in:
@@ -186,6 +186,45 @@ function extractCanonicalPlanId(filename: string): string {
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count summaries that correspond to a real plan (#1988).
|
||||
*
|
||||
* A summary counts toward phase completion iff it pairs with an existing plan
|
||||
* file. This excludes stray non-plan summaries — e.g. `30-FIX-CR02-SUMMARY.md`,
|
||||
* `30-GAPCLOSURE-SUMMARY.md` — that inflate the raw `*-SUMMARY.md` count and
|
||||
* silently flip a phase to Complete when plans are actually missing summaries.
|
||||
*
|
||||
* Pairing is layout-agnostic. For each plan, up to three candidate summary
|
||||
* filenames are generated and any match suffices:
|
||||
* 1. marker swap `PLAN`→`SUMMARY` on the basename — root padded
|
||||
* (`30-01-PLAN.md`↔`30-01-SUMMARY.md`), nested (`PLAN-01.md`↔
|
||||
* `SUMMARY-01.md`, incl. a `plans/` prefix), and bare (`PLAN.md`↔
|
||||
* `SUMMARY.md`);
|
||||
* 2. `<stem>-SUMMARY.md` — bare (`PLAN.md`↔`PLAN-SUMMARY.md`) and legacy
|
||||
* (`14-PLAN-01.md`↔`14-PLAN-01-SUMMARY.md`);
|
||||
* 3. extended `<n>-PLAN-<m>…`→`<n>-<m>-SUMMARY.md`
|
||||
* (`3-PLAN-01-setup.md`↔`3-01-SUMMARY.md`).
|
||||
* The swap is applied to the basename only so a lowercase `plans/` dir prefix
|
||||
* isn't corrupted to `SUMMARYs/…`.
|
||||
*/
|
||||
function countMatchedSummaries(planFiles: string[], summaryFiles: string[]): number {
|
||||
const summarySet = new Set(summaryFiles);
|
||||
let matched = 0;
|
||||
for (const plan of planFiles) {
|
||||
const slashIdx = plan.lastIndexOf('/');
|
||||
const dir = slashIdx >= 0 ? plan.slice(0, slashIdx + 1) : '';
|
||||
const base = (dir ? plan.slice(dir.length) : plan).replace(/\.md$/i, '');
|
||||
const candidates: string[] = [
|
||||
dir + base.replace(/PLAN/i, 'SUMMARY') + '.md',
|
||||
dir + base + '-SUMMARY.md',
|
||||
];
|
||||
const extended = base.match(/^(\d+)-PLAN-(\d+)/i);
|
||||
if (extended) candidates.push(dir + extended[1] + '-' + extended[2] + '-SUMMARY.md');
|
||||
if (candidates.some((c) => summarySet.has(c))) matched++;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
export = {
|
||||
toPosixPath,
|
||||
detectSubRepos,
|
||||
@@ -198,4 +237,5 @@ export = {
|
||||
readSubdirectories,
|
||||
timeAgo,
|
||||
extractCanonicalPlanId,
|
||||
countMatchedSummaries,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user