fix(#4741): the plan checkbox tick respects the superseded exclusion (#4851)

* test(#4741): a superseded plan must not be ticked from its summary (failing first)

* fix(#4741): the plan checkbox tick respects the superseded-plan exclusion

* fix(#4741): review fold-ins — changeset typo, dedupe planId derivation

* test(#4741): exercise a halted summary on the active plan in the #2830 pin

* chore(#4741): backfill changeset PR number (4851)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-18 06:33:30 -04:00
committed by GitHub
parent 11b3091df0
commit e1f72cd324
3 changed files with 182 additions and 6 deletions

View File

@@ -0,0 +1,6 @@
---
type: Fixed
pr: 4851
---
**A superseded plan no longer reads as executed in the ROADMAP** — the roadmap update-plan-progress checkbox tick now skips plans the count already excludes as superseded, so the "N/M plans executed" line and the checkboxes below it always agree. (#4741)

View File

@@ -1235,9 +1235,17 @@ function cmdRoadmapUpdatePlanProgress(cwd: string, phaseNum: string | null | und
}
// Mark completed plan checkboxes (e.g. "- [ ] 50-01-PLAN.md", "- [ ] 50-01:", or "- [ ] **50-01**")
for (const summaryFile of phaseInfo!.summaries) {
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
if (!planId) continue;
// #4741: tick only plans the phase's own COUNT still counts. `plans` is the
// superseded-filtered set (#2349 via scanPhasePlans) while `summaries` is
// the raw *-SUMMARY.md listing — a superseded plan can carry a SUMMARY
// (e.g. `status: halted`), and ticking it read as "executed" right under a
// count line that excludes it. The prefix match mirrors the checkbox regex
// below (rows match by planId prefix, which the PLAN-01.md naming shape
// relies on), so non-superseded plans tick exactly as before.
const tickableSummaries = phaseInfo!.summaries
.map((summaryFile) => ({ summaryFile, planId: summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '') }))
.filter(({ planId }) => planId !== '' && phaseInfo!.plans.some((planFile) => planFile.startsWith(planId)));
for (const { planId } of tickableSummaries) {
const planEscaped = escapeRegex(planId);
const planCheckboxPattern = new RegExp(
`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`,
@@ -1319,9 +1327,9 @@ function cmdRoadmapUpdatePlanProgress(cwd: string, phaseNum: string | null | und
if (withRows !== roadmapContent) {
roadmapContent = withRows;
// Mark any newly-inserted rows that already have summaries as complete
for (const summaryFile of phaseInfo!.summaries) {
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
if (!planId) continue;
// (#4741: same superseded-filtered tick list as the loop above — a
// pre-existing superseded row must stay unchecked on this path too).
for (const { planId } of tickableSummaries) {
const planEscaped = escapeRegex(planId);
const planCheckboxPattern = new RegExp(
`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`,

View File

@@ -5117,3 +5117,165 @@ describe('#3957 (epic #3473 B9): no-op decline reports the real condition', () =
});
});
});
// ─── #4741: a superseded plan must not be ticked from its SUMMARY ────────────
describe('roadmap update-plan-progress — superseded plans (#4741)', () => {
// The issue's self-contained fixture: phase 1 with an active plan (01-01)
// and a superseded plan (01-02, `status: superseded` in the PLAN
// frontmatter), each with a SUMMARY file. The count excludes the superseded
// plan (#2349) while the checkbox tick iterated the raw summaries — so the
// ROADMAP read "1/1 plans executed" above two checked rows.
function write4741World(tmpDir, opts = {}) {
const {
supersededPlanStatus = 'superseded',
supersededSummaryStatus = 'halted',
includeSupersededSummary = true,
activeSummaryStatus = 'complete',
roadmap = null,
} = opts;
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-demo'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
roadmap ?? [
'# Roadmap',
'',
'### Phase 1: Demo',
'',
'**Goal:** demo',
'',
'**Plans:** 2 plans',
'',
'Plans:',
'',
'- [ ] 01-01-PLAN.md — first',
'- [ ] 01-02-PLAN.md — second',
'',
].join('\n'),
);
const plan = (num, extra) =>
`---\nphase: 01-demo\nplan: ${num}\ntype: execute\nwave: 1\ndepends_on: []\nfiles_modified: []\nautonomous: true${extra ? `\n${extra}` : ''}\n---\n`;
fs.writeFileSync(path.join(tmpDir, '.planning', 'phases', '01-demo', '01-01-PLAN.md'), plan('01'));
fs.writeFileSync(
path.join(tmpDir, '.planning', 'phases', '01-demo', '01-02-PLAN.md'),
plan('02', `status: ${supersededPlanStatus}`),
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'phases', '01-demo', '01-01-SUMMARY.md'),
`---\nphase: 01-demo\nplan: 01\nstatus: ${activeSummaryStatus}\n---\n`,
);
if (includeSupersededSummary) {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'phases', '01-demo', '01-02-SUMMARY.md'),
`---\nphase: 01-demo\nplan: 02\nstatus: ${supersededSummaryStatus}\n---\n`,
);
}
}
function runUpdate(tmpDir) {
const result = runGsdTools('roadmap update-plan-progress 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
return JSON.parse(result.output);
}
test('#4741: a superseded plan is not ticked even when its SUMMARY exists', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
write4741World(tmpDir);
const output = runUpdate(tmpDir);
assert.strictEqual(output.plan_count, 1, 'the superseded plan is excluded from the count');
assert.strictEqual(output.summary_count, 1);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(roadmap.includes('- [x] 01-01-PLAN.md'), 'the active executed plan ticks');
assert.ok(
roadmap.includes('- [ ] 01-02-PLAN.md'),
'the superseded plan must stay unchecked — a plan the tool does not count must not read as executed',
);
assert.ok(roadmap.includes('1/1 plans executed'), 'the count line agrees with the checkboxes');
});
test('#4741: the superseded filter ignores the SUMMARY\'s own status', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
write4741World(tmpDir, { supersededSummaryStatus: 'complete' });
runUpdate(tmpDir);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(
roadmap.includes('- [ ] 01-02-PLAN.md'),
'a superseded plan with a COMPLETE summary still must not read as executed',
);
});
test('#4741: the inserted-rows tick path respects the superseded filter too', (t) => {
// Loop 2 fires when a countable plan's row is MISSING (insertion path):
// pre-existing superseded row + missing countable row. Before the fix the
// insertion path's tick loop iterated the raw summaries and ticked the
// pre-existing superseded row as well.
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
write4741World(tmpDir, {
roadmap: [
'# Roadmap',
'',
'### Phase 1: Demo',
'',
'**Goal:** demo',
'',
'**Plans:** 2 plans',
'',
'Plans:',
'',
'- [ ] 01-02-PLAN.md — second',
'',
].join('\n'),
});
const output = runUpdate(tmpDir);
assert.strictEqual(output.plan_count, 1);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(roadmap.includes('- [x] 01-01-PLAN.md'), 'the inserted countable row is ticked (it has a summary)');
assert.ok(
roadmap.includes('- [ ] 01-02-PLAN.md'),
'the pre-existing superseded row must stay unchecked even on the insertion path',
);
});
test('#4741 negative space: a superseded plan without a summary stays unchecked', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
write4741World(tmpDir, { includeSupersededSummary: false });
const output = runUpdate(tmpDir);
assert.strictEqual(output.plan_count, 1);
assert.strictEqual(output.summary_count, 1);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(roadmap.includes('- [ ] 01-02-PLAN.md'), 'no summary → no tick (existing behavior preserved)');
});
test('#4741 negative space: a halted summary on an ACTIVE plan still ticks (#2830)', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
// #2830: `status: halted` on a plan that still COUNTS is executed-by-design.
// The active plan's SUMMARY carries `halted` here; the superseded plan's
// row must still stay unchecked in the same document.
write4741World(tmpDir, { activeSummaryStatus: 'halted' });
const output = runUpdate(tmpDir);
assert.strictEqual(output.plan_count, 1, 'only the superseded plan is excluded');
assert.strictEqual(output.summary_count, 1);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(roadmap.includes('- [x] 01-01-PLAN.md'), 'a halted summary on an ACTIVE plan ticks (#2830)');
assert.ok(
roadmap.includes('- [ ] 01-02-PLAN.md'),
'and the superseded plan stays unchecked in the same document',
);
assert.ok(roadmap.includes('1/1 plans executed'), 'numbers and checkboxes agree');
});
});