* test(#3741): REPLAN/PLANNING substrings must not count as plans * fix(#3741): anchor the loose plan fallback's PLAN token * chore(#3741): changeset fragment (pr number backfilled after PR creation) * chore(#3741): backfill changeset PR number (3950) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/eager-cats-squeak.md
Normal file
5
.changeset/eager-cats-squeak.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3950
|
||||
---
|
||||
**`total_plans` no longer counts REPLAN/PLANNING documents as plans** — a phase directory carrying a `REPLAN-INPUTS` or `PLANNING-NOTES` document no longer inflates the plan count that STATE.md derives on every state-mutating call. (#3741)
|
||||
@@ -83,7 +83,18 @@ function isRootPlanFile(fileName: string): boolean {
|
||||
// fallback so legacy `<N>-PLAN-<NN>-SUMMARY.md` names (which contain the
|
||||
// substring "PLAN") are not double-counted as plans. (#500 RC2)
|
||||
if (isRootSummaryFile(fileName)) return false;
|
||||
return /\.md$/i.test(fileName) && /PLAN/i.test(fileName);
|
||||
// #3741: the PLAN token must be DELIMITED — anchored at the start or after
|
||||
// a hyphen, and followed only by an optional `-<digits>…` suffix before
|
||||
// `.md` (the `…` keeps the legacy slug form `3-PLAN-01-setup.md` that
|
||||
// gsd-plan-phase writes, per #3128). A bare substring test counted
|
||||
// REPLAN-INPUTS / PLANNING-INPUTS / PLANNING-NOTES as plans, inflating
|
||||
// planCount and STATE.md's derived total_plans. Delimited keeps the
|
||||
// fallback's deliberate permissiveness for legacy single-token names
|
||||
// (`plan.md`, `Plan.md`, `01-PLAN-02.md`, `3-PLAN-01-setup.md`) while
|
||||
// excluding any name where PLAN is merely embedded in a larger word
|
||||
// (REPLAN, PLANNING) — the same anchoring discipline isNestedPlanFile
|
||||
// already applies.
|
||||
return /(^|-)PLAN(-\d+.*)?\.md$/i.test(fileName);
|
||||
}
|
||||
|
||||
function isNestedPlanFile(fileName: string): boolean {
|
||||
@@ -105,7 +116,7 @@ function isNestedSummaryFile(fileName: string): boolean {
|
||||
* `allPlanFiles` ENTRY (root form bare, nested form `plans/`-prefixed, exactly
|
||||
* as those arrays store them) — root `<phase>-<NN>-PLAN.md`/bare `PLAN.md`,
|
||||
* or nested `plans/PLAN-<NN>....md`/`plans/<x>-PLAN-<NN>....md` — WITHOUT
|
||||
* `isRootPlanFile`'s loose `/\.md$/i && /PLAN/i` fallback.
|
||||
* `isRootPlanFile`'s loose delimited-PLAN fallback.
|
||||
*
|
||||
* The `plans/` prefix check is load-bearing, not cosmetic: `isNestedPlanFile`
|
||||
* matches ANY basename containing `-PLAN-<digits>...md` with no anchor
|
||||
|
||||
@@ -380,6 +380,56 @@ describe('scope field — UNREADABLE / TRUNCATED / COMPLETE independence', () =>
|
||||
// the same fallback) — these tests pin the current behavior at both
|
||||
// altitudes (scanPhasePlans and getPhaseFileStats) so a future change to
|
||||
// either rule is caught rather than silently drifting.
|
||||
// ─── #3741: the loose fallback's PLAN token must be delimited ─────────────
|
||||
// The legacy fallback was a bare substring test, so any *.md basename
|
||||
// containing "PLAN" counted: REPLAN-INPUTS, PLANNING-INPUTS, hand-authored
|
||||
// PLANNING-NOTES — inflating planCount and STATE.md's derived total_plans.
|
||||
describe('#3741: the loose /PLAN/i fallback counts only a delimited PLAN token', () => {
|
||||
test('REPLAN-INPUTS is not a plan — substring PLAN inside REPLAN must not count', (t) => {
|
||||
const dir = createTempDir('gsd-3741-replan-');
|
||||
t.after(() => cleanup(dir));
|
||||
writeFile(dir, '01-01-PLAN.md', planBody());
|
||||
writeFile(dir, '01-01-REPLAN-INPUTS.md', 'Not a plan. Planning inputs document.\n');
|
||||
const scan = planScan(dir);
|
||||
assert.strictEqual(scan.planCount, 1, '#3741: exactly the canonical plan counts');
|
||||
assert.ok(!scan.planFiles.includes('01-01-REPLAN-INPUTS.md'));
|
||||
});
|
||||
|
||||
test('PLANNING-* and *-NOTES documents are not plans', (t) => {
|
||||
const dir = createTempDir('gsd-3741-notes-');
|
||||
t.after(() => cleanup(dir));
|
||||
writeFile(dir, '01-01-PLAN.md', planBody());
|
||||
writeFile(dir, '01-PLANNING-INPUTS.md', 'inputs\n');
|
||||
writeFile(dir, 'PLANNING-NOTES.md', 'notes\n');
|
||||
writeFile(dir, 'REPLAN-NOTES.md', 'notes\n');
|
||||
const scan = planScan(dir);
|
||||
assert.strictEqual(scan.planCount, 1);
|
||||
assert.ok(!scan.planFiles.includes('01-PLANNING-INPUTS.md'));
|
||||
assert.ok(!scan.planFiles.includes('PLANNING-NOTES.md'));
|
||||
assert.ok(!scan.planFiles.includes('REPLAN-NOTES.md'));
|
||||
});
|
||||
|
||||
test('legacy PLAN-token+numeric names still count via the anchored fallback', (t) => {
|
||||
const dir = createTempDir('gsd-3741-legacy-');
|
||||
t.after(() => cleanup(dir));
|
||||
writeFile(dir, '01-PLAN-02.md', planBody());
|
||||
const scan = planScan(dir);
|
||||
assert.strictEqual(scan.planCount, 1);
|
||||
assert.ok(scan.planFiles.includes('01-PLAN-02.md'));
|
||||
});
|
||||
|
||||
test('legacy PLAN-token + numeric + slug names still count (#3128 shape, #3741 review)', (t) => {
|
||||
const dir = createTempDir('gsd-3741-slug-');
|
||||
t.after(() => cleanup(dir));
|
||||
// gsd-plan-phase writes this form; #3128 is the historical bug where
|
||||
// dropping it made plan_count read 0. The anchor must keep it.
|
||||
writeFile(dir, '3-PLAN-01-setup.md', planBody());
|
||||
const scan = planScan(dir);
|
||||
assert.strictEqual(scan.planCount, 1);
|
||||
assert.ok(scan.planFiles.includes('3-PLAN-01-setup.md'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('case sensitivity: plan.md/Plan.md counted, summary.md/Summary.md NOT (#3183 asymmetry)', () => {
|
||||
test('lowercase plan.md is counted as a plan (loose /PLAN/i fallback is case-insensitive)', (t) => {
|
||||
const dir = createTempDir('gsd-plan-scan-case-');
|
||||
|
||||
Reference in New Issue
Block a user