fix(#4786): plan-row detection accepts the bare planId stem — suffix-less hand-written lists tick in place (#4880)
* test(#4786): failing-first — a suffix-less hand-written plan list is ticked in place, never duplicated * fix(#4786): plan-row detection accepts the bare planId stem — a suffix-less hand-written list is ticked in place, never duplicated * chore(#4786): backfill changeset PR number (4880) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/bold-otters-fly.md
Normal file
5
.changeset/bold-otters-fly.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4880
|
||||
---
|
||||
**roadmap update-plan-progress stops inserting a duplicate plan list beside hand-written ones** — plan checkbox rows written without the -PLAN.md suffix (the hand-written form) were not recognized, so the verb inserted its own canonical list above them, leaving two competing lists for the same plans; suffix-less rows are now recognized and ticked in place. (#4786)
|
||||
@@ -1274,9 +1274,28 @@ function cmdRoadmapUpdatePlanProgress(cwd: string, phaseNum: string | null | und
|
||||
// Detection is scoped to the active region so a plan that appears in an
|
||||
// archived <details> block is still correctly detected as missing from the
|
||||
// active milestone section.
|
||||
//
|
||||
// #4786: a plan row may be written WITH the `-PLAN.md` suffix (canonical
|
||||
// template form) or WITHOUT it (hand-written form: `- [x] 659-01 — desc`).
|
||||
// The tick loop above keys on the bare planId stem with a PREFIX match
|
||||
// (pre-existing, out of scope here), while detection required the full
|
||||
// `-PLAN.md` filename — so every suffix-less row was counted missing and
|
||||
// the insertion fired BESIDE the recognized list — 32 checkbox lines for
|
||||
// 16 plans, exit 0. The stem arm below accepts the bare id only up to a
|
||||
// boundary (whitespace / `:` / dashes / `-PLAN.md` / `.md` / `**` / `)`),
|
||||
// so `5-011` never satisfies `5-01`. Deliberately NOT in the boundary set:
|
||||
// `.` — a dotted sub-id (`5-01.5`) is a real distinct plan, and counting
|
||||
// its row as 5-01's presence would suppress a genuine insertion.
|
||||
const missingPlans = phaseInfo!.plans.filter((planFile) => {
|
||||
const planEscaped = escapeRegex(planFile);
|
||||
return !new RegExp(`-\\s*\\[[x ]\\]\\s*(?:\\*\\*)?${planEscaped}`, 'i').test(activeRegion);
|
||||
if (new RegExp(`-\\s*\\[[x ]\\]\\s*(?:\\*\\*)?${planEscaped}`, 'i').test(activeRegion)) return false;
|
||||
const stem = planFile.replace(/-PLAN\.md$/i, '');
|
||||
const stemEscaped = escapeRegex(stem);
|
||||
const stemPresent = new RegExp(
|
||||
`-\\s*\\[[x ]\\]\\s*(?:\\*\\*)?${stemEscaped}(?=$|\\s|:|—|–|-PLAN\\.md|\\.md|\\*\\*|\\))`,
|
||||
'i'
|
||||
).test(activeRegion);
|
||||
return !stemPresent;
|
||||
});
|
||||
|
||||
if (missingPlans.length > 0) {
|
||||
|
||||
@@ -5279,3 +5279,120 @@ describe('roadmap update-plan-progress — superseded plans (#4741)', () => {
|
||||
assert.ok(roadmap.includes('1/1 plans executed'), 'numbers and checkboxes agree');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #4786: suffix-less hand-written plan lists tick in place, never duplicated ──
|
||||
|
||||
describe('#4786: suffix-less hand-written plan lists are recognized, not duplicated', () => {
|
||||
let tmpDir;
|
||||
let roadmapPath;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject('gsd-4786-');
|
||||
roadmapPath = path.join(tmpDir, '.planning', 'ROADMAP.md');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
function writtenAfter() {
|
||||
return fs.readFileSync(roadmapPath, 'utf-8');
|
||||
}
|
||||
|
||||
test('#4786: a suffix-less hand-written plan list is ticked in place, never duplicated', () => {
|
||||
// The issue's measured shape: a hand-written list WITHOUT the -PLAN.md
|
||||
// suffix, carrying em-dash descriptions. The old detection keyed on the
|
||||
// full plan filename, counted every plan "missing", and inserted a
|
||||
// canonical list above the hand-written one (32 checkbox lines for 16
|
||||
// plans, exit 0).
|
||||
fs.writeFileSync(roadmapPath, [
|
||||
'# ROADMAP',
|
||||
'',
|
||||
'### Phase 5: test phase',
|
||||
'',
|
||||
'**Plans:** 2/3 plans executed',
|
||||
'',
|
||||
'Plans:',
|
||||
'',
|
||||
'- [x] 5-01 — first: does the thing with a long hand-written description',
|
||||
'- [x] 5-02 — second: does the other thing through submit(true)',
|
||||
'- [ ] 5-03 — union run, redeploy and demo rebuild, live measurement',
|
||||
'',
|
||||
].join('\n'));
|
||||
createPhaseWithPlans(tmpDir, '5', [
|
||||
'5-01-PLAN.md',
|
||||
'5-02-PLAN.md',
|
||||
'5-03-PLAN.md',
|
||||
]);
|
||||
// All three plans have summaries (the issue's shape: the call lands the
|
||||
// last summary, 15/16 → 16/16). #4741: only plans the count counts are
|
||||
// tickable — a summary-less row is correctly left alone.
|
||||
for (const n of ['5-01', '5-02', '5-03']) {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'phases', '05-test-phase', `${n}-SUMMARY.md`), '# Summary\n');
|
||||
}
|
||||
|
||||
const result = runGsdTools(['roadmap', 'update-plan-progress', '5'], tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const parsed = JSON.parse(result.output);
|
||||
assert.equal(parsed.updated, true, 'the unchecked plan must be ticked');
|
||||
assert.ok(
|
||||
writtenAfter(result).includes('**Plans:** 3/3 plans executed'),
|
||||
'the count line must update to 3/3',
|
||||
);
|
||||
|
||||
const written = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
// The damage was the INSERTION — none may appear.
|
||||
assert.ok(!written.includes('5-01-PLAN.md'), 'no canonical row may be inserted beside a recognized suffix-less list');
|
||||
assert.ok(!written.includes('5-02-PLAN.md'), 'no canonical row may be inserted beside a recognized suffix-less list');
|
||||
assert.ok(!written.includes('5-03-PLAN.md'), 'no canonical row may be inserted beside a recognized suffix-less list');
|
||||
// The tick happened IN PLACE, with the hand-written description byte-identical.
|
||||
assert.ok(
|
||||
written.includes('- [x] 5-03 — union run, redeploy and demo rebuild, live measurement'),
|
||||
`the unchecked suffix-less row must be ticked in place with its description intact; ROADMAP:\n${written}`,
|
||||
);
|
||||
assert.ok(
|
||||
written.includes('- [x] 5-01 — first: does the thing with a long hand-written description'),
|
||||
'already-ticked rows must be byte-identical',
|
||||
);
|
||||
assert.equal(
|
||||
(written.match(/- \[.\] 5-0/g) || []).length,
|
||||
3,
|
||||
`exactly three checkbox rows must remain (no duplication); ROADMAP:\n${written}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('#4786: a stem does not match a longer plan id', () => {
|
||||
// Boundary: the new stem recognition must not let `- [x] 5-011` satisfy
|
||||
// plan 5-01 (5-011 is a different plan). 5-01 carries no recognizable row,
|
||||
// so the #1163 fresh-template insertion still fires for it — that is the
|
||||
// documented contract; the pin is that 5-011 is not treated as 5-01.
|
||||
fs.writeFileSync(roadmapPath, [
|
||||
'# ROADMAP',
|
||||
'',
|
||||
'### Phase 5: test phase',
|
||||
'',
|
||||
'Plans:',
|
||||
'',
|
||||
'- [x] 5-011 — bogus longer id, not plan 5-01',
|
||||
'',
|
||||
].join('\n'));
|
||||
createPhaseWithPlans(tmpDir, '5', [
|
||||
'5-01-PLAN.md',
|
||||
'5-02-PLAN.md',
|
||||
'5-03-PLAN.md',
|
||||
]);
|
||||
|
||||
const result = runGsdTools(['roadmap', 'update-plan-progress', '5'], tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const written = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
assert.ok(written.includes('- [ ] 5-01-PLAN.md'), '5-01 must still count as missing (5-011 is a different plan)');
|
||||
assert.ok(written.includes('- [ ] 5-02-PLAN.md') && written.includes('- [ ] 5-03-PLAN.md'),
|
||||
'all genuinely absent plans still insert');
|
||||
assert.equal(
|
||||
(written.match(/- \[.\] 5-0/g) || []).length,
|
||||
4,
|
||||
`3 inserted rows + the hand-written 5-011 row; ROADMAP:\n${written}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user