* 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.
ADR-857 rollout phase 2c. Move 11 shared low-level utilities out of core.cts
into a new leaf module src/core-utils.cts: POSIX path normalization (toPosixPath),
filesystem scanning (detectSubRepos, readSubdirectories, getPhaseFileStats,
pathExistsInternal), and small pure helpers (generateSlugInternal,
extractOneLinerFromBody, filterPlanFiles, filterSummaryFiles, timeAgo, and the
private extractCanonicalPlanId). core.cts re-exports the 10 public ones
(callers unchanged); extractCanonicalPlanId stays private (exported from the
leaf for core's fs-search functions).
Cycle-free: core-utils depends only on Node built-ins + already-leafed modules
(phase-id for comparePhaseNum, planning-workspace for findContextMdIn). This is
the shared leaf that unblocks the phase-locator fs-search extraction (2d) —
searchPhaseInDir/findPhaseInternal/getArchivedPhaseDirs can now take their
utilities from a leaf instead of from core.
New-CLI-module checklist done (.gitignore, eslint, INVENTORY 93->94 + row,
manifest, ARCHITECTURE, CONTEXT.md "Core Utilities Module"). Adds
tests/core-utils.test.cjs (88 tests: behavioral + shim-identity + adversarial).
Gates: lint, code-review, security-review, codex adversarial-review (0
findings). Mac 4041 pass; clean-build docker 12935 pass, 0 fail.
Closes#877
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>