refactor(#3309): add root-existence health-diagnostic rules
E002, E003, E004, W001 — PROJECT.md/ROADMAP.md/STATE.md existence and PROJECT.md section-completeness checks, migrated onto the frozen rule table per ADR-3180 §8.2.
This commit is contained in:
172
src/health-diagnostic-rules/root-existence.cts
Normal file
172
src/health-diagnostic-rules/root-existence.cts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Health Diagnostic — Root existence + PROJECT.md rules (Phase 11, #3309,
|
||||
* ADR-3180 §8.2/§8.3/§8.5).
|
||||
*
|
||||
* Group: "Root existence + PROJECT.md" (design doc, "Rule table organization"
|
||||
* table) — E002, E003, E004, W001. E001 (the `.planning/` root missing guard)
|
||||
* stays OUTSIDE the rule table entirely per the design doc's "Two guards that
|
||||
* stay OUTSIDE the rule table entirely" section — it is not a row here.
|
||||
*
|
||||
* Ported behavior-preserving from `cmdValidateHealth`
|
||||
* (`src/verify.cts:1681-1705`), the exact call sites for E002/E003/E004/W001.
|
||||
*
|
||||
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
|
||||
*
|
||||
* ADR-457 build-at-publish: source in src/health-diagnostic-rules/root-existence.cts,
|
||||
* compiled to gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs (gitignored).
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- type-only; erased at compile time, no runtime require emitted
|
||||
import type planningSnapshotMod = require('../planning-snapshot.cjs');
|
||||
|
||||
type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnapshot>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import healthDiagnosticMod = require('../health-diagnostic.cjs');
|
||||
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
|
||||
type Diagnostic = healthDiagnosticMod.Diagnostic;
|
||||
type Rule = healthDiagnosticMod.Rule;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import planningScopeMod = require('../planning-scope.cjs');
|
||||
const { SCOPE } = planningScopeMod;
|
||||
|
||||
// ─── E002 — PROJECT.md not found (verify.cts:1682) ─────────────────────────
|
||||
|
||||
function checkE002(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
if (snapshot.projectSections.exists) return [];
|
||||
return [
|
||||
{
|
||||
code: 'E002',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'PROJECT.md not found',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: '/gsd-new-project' },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ─── E003 — ROADMAP.md not found (verify.cts:1694) ─────────────────────────
|
||||
//
|
||||
// Condition uses `snapshot.milestone.scope === SCOPE.UNREADABLE`
|
||||
// (`getMilestoneInfo`, `src/roadmap-parser.cts`). KNOWN AMBIGUITY (flagged in
|
||||
// this batch's report, not silently papered over): `getMilestoneInfo` returns
|
||||
// `SCOPE.UNREADABLE` for TWO distinct causes it does not otherwise
|
||||
// distinguish — (1) ROADMAP.md absent (`platformReadSync` returns `null` ->
|
||||
// synthetic `Error('missing')`, no errno, `reportUnreadableRoadmap` finds no
|
||||
// `.code` and stays silent) and (2) ROADMAP.md present but unreadable (a real
|
||||
// read fault, e.g. EACCES/EISDIR, which DOES carry an errno and fires
|
||||
// `warnUnusableInput(ROADMAP_UNREADABLE)`). Unlike `config`/`projectSections`,
|
||||
// `milestone` carries no `exists` discriminator, so this rule cannot tell the
|
||||
// two apart from the snapshot alone without adding cwd/fs access to `check`
|
||||
// (forbidden by §8.1 rule 1). This is a best-effort port of the pre-migration
|
||||
// condition (`!fs.existsSync(roadmapPath)`), which itself only asked "does
|
||||
// the file exist" — this rule now also fires (message-mismatched, but
|
||||
// error-preserving) on a present-but-corrupt ROADMAP.md.
|
||||
|
||||
function checkE003(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
if (snapshot.milestone.scope !== SCOPE.UNREADABLE) return [];
|
||||
return [
|
||||
{
|
||||
code: 'E003',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'ROADMAP.md not found',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: '/gsd-new-milestone' },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ─── E004 — STATE.md not found (verify.cts:1697) ───────────────────────────
|
||||
//
|
||||
// Condition uses `snapshot.currentPhaseLabel.scope === SCOPE.UNREADABLE`
|
||||
// (`buildStateFields`, `src/planning-snapshot.cts:210-249`). KNOWN GAP
|
||||
// (flagged in this batch's report): `buildStateFields` collapses TWO distinct
|
||||
// causes into the same `UNREADABLE` scope with no discriminator field at
|
||||
// all — STATE.md absent (`platformReadSync` returns `null`, a real
|
||||
// non-answer, `warnUnusableInput` NOT called) and STATE.md present but
|
||||
// unreadable (any other read error, e.g. EISDIR, corruption,
|
||||
// `warnUnusableInput(STATE_UNREADABLE)` fires). Unlike `config`, there is no
|
||||
// `exists` flag on `currentPhaseLabel` (or on `PlanningSnapshot` generally)
|
||||
// to distinguish "STATE.md was never created" from "STATE.md exists but
|
||||
// could not be read" — this is a REAL gap in the current 15-field
|
||||
// `PlanningSnapshot` shape, not something this rule can work around without
|
||||
// extending that snapshot (out of this batch's scope per the brief). This
|
||||
// rule is therefore a best-effort port: it fires E004 ("STATE.md not found")
|
||||
// for both causes, exactly mirroring what `snapshot.currentPhaseLabel.scope`
|
||||
// can express today.
|
||||
//
|
||||
// Remedy is `regenerateState`, one of the two DESTRUCTIVE-risk actions (loses
|
||||
// session history, design doc "Risk assignment" section) — per §8.3 rule 3
|
||||
// `--repair` will refuse to auto-apply it once `applyRepairs`'s dispatch
|
||||
// wires this rule in; the remedy is still described (ADVISE-shaped for
|
||||
// display, per `applyRepairs`'s own contract) but never executed.
|
||||
|
||||
function checkE004(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
if (snapshot.currentPhaseLabel.scope !== SCOPE.UNREADABLE) return [];
|
||||
return [
|
||||
{
|
||||
code: 'E004',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'STATE.md not found',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.REGENERATE_STATE,
|
||||
risk: REMEDY_RISK.DESTRUCTIVE,
|
||||
args: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ─── W001 — PROJECT.md missing a required section (verify.cts:1684-1690) ──
|
||||
//
|
||||
// `REQUIRED_SECTIONS` carries the exact `## `-prefixed strings
|
||||
// `verify.cts:1685` uses in its message text; membership is tested against
|
||||
// `snapshot.projectSections.value`, which `buildProjectSectionsField`
|
||||
// (`src/planning-snapshot.cts:367-381`) stores WITHOUT the `##` prefix (its
|
||||
// `/^##\s+(.+)$/gm` capture group), so each required string's own `## `
|
||||
// prefix is stripped before the membership check. `projectSections.value ===
|
||||
// null` (PROJECT.md absent OR unreadable) emits zero diagnostics — E002
|
||||
// already reports absence; this rule does not double-report it.
|
||||
|
||||
const REQUIRED_SECTIONS = ['## What This Is', '## Core Value', '## Requirements'];
|
||||
|
||||
function checkW001(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
const { value } = snapshot.projectSections;
|
||||
if (value === null) return [];
|
||||
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
for (const required of REQUIRED_SECTIONS) {
|
||||
const heading = required.replace(/^##\s+/, '');
|
||||
if (!value.includes(heading)) {
|
||||
diagnostics.push({
|
||||
code: 'W001',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `PROJECT.md missing section: ${required}`,
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: 'Add section manually' },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
// ─── Exports ────────────────────────────────────────────────────────────────
|
||||
|
||||
const RULES: Rule[] = [
|
||||
{ code: 'E002', severity: SEVERITY.ERROR, check: checkE002 },
|
||||
{ code: 'E003', severity: SEVERITY.ERROR, check: checkE003 },
|
||||
{ code: 'E004', severity: SEVERITY.ERROR, check: checkE004 },
|
||||
{ code: 'W001', severity: SEVERITY.WARNING, check: checkW001 },
|
||||
];
|
||||
|
||||
export = { RULES };
|
||||
303
tests/health-diagnostic-rules/root-existence.test.cjs
Normal file
303
tests/health-diagnostic-rules/root-existence.test.cjs
Normal file
@@ -0,0 +1,303 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Tests for `src/health-diagnostic-rules/root-existence.cts` (Phase 11,
|
||||
* #3309, ADR-3180 §8.2/§8.3/§8.5) — group "Root existence + PROJECT.md":
|
||||
* E002, E003, E004, W001.
|
||||
*
|
||||
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
|
||||
*
|
||||
* Fixture provenance (§8.5 + CONTRIBUTING "Fixture provenance (#2371)"): all
|
||||
* four rules here are STRUCTURAL ABSENCE conditions (a file, or a section
|
||||
* heading, is missing) — exempt from external-citation provenance per the
|
||||
* design doc's "Fixture provenance" §1: "the fixture *is* the absence — no
|
||||
* format being modeled, only a presence/absence fact." Every fixture is built
|
||||
* via the REAL `buildPlanningSnapshot(cwd)` against a REAL temp directory
|
||||
* (mirrors `tests/planning-snapshot.test.cjs` exactly) — no hand-constructed
|
||||
* fake `PlanningSnapshot` object.
|
||||
*
|
||||
* TDD RED: `src/health-diagnostic-rules/root-existence.cts` does not exist
|
||||
* yet at the start of this batch — this file's
|
||||
* `require('../../gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs')`
|
||||
* throws MODULE_NOT_FOUND until this batch's implementation lands.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { createTempDir, cleanup } = require('../helpers.cjs');
|
||||
|
||||
const rootExistence = require('../../gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs');
|
||||
const { RULES } = rootExistence;
|
||||
|
||||
const { buildPlanningSnapshot } = require('../../gsd-core/bin/lib/planning-snapshot.cjs');
|
||||
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = require('../../gsd-core/bin/lib/health-diagnostic.cjs');
|
||||
|
||||
function planningDirOf(cwd) {
|
||||
return path.join(cwd, '.planning');
|
||||
}
|
||||
|
||||
function writeProject(cwd, content) {
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDirOf(cwd), 'PROJECT.md'), content);
|
||||
}
|
||||
|
||||
function writeRoadmap(cwd, content) {
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDirOf(cwd), 'ROADMAP.md'), content);
|
||||
}
|
||||
|
||||
function writeState(cwd, content) {
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDirOf(cwd), 'STATE.md'), content);
|
||||
}
|
||||
|
||||
// Makes a FILE unreadable-as-a-file: a DIRECTORY node where a regular file is
|
||||
// expected. `platformReadSync`/`fs.readFileSync` throws EISDIR on it,
|
||||
// deterministically and cross-platform — no chmod (mirrors
|
||||
// tests/planning-snapshot.test.cjs's `makeFileUnreadableAsDir`).
|
||||
function makeFileUnreadableAsDir(fullPath) {
|
||||
fs.mkdirSync(fullPath, { recursive: true });
|
||||
}
|
||||
|
||||
function ruleFor(code) {
|
||||
const rule = RULES.find((r) => r.code === code);
|
||||
assert.ok(rule, `rule ${code} not found in RULES`);
|
||||
return rule;
|
||||
}
|
||||
|
||||
// ─── RULES shape ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('RULES (root-existence group)', () => {
|
||||
test('exports exactly 4 rules: E002, E003, E004, W001', () => {
|
||||
assert.deepEqual(
|
||||
RULES.map((r) => r.code).sort(),
|
||||
['E002', 'E003', 'E004', 'W001'],
|
||||
);
|
||||
});
|
||||
|
||||
test('E002/E003/E004 are severity ERROR; W001 is severity WARNING', () => {
|
||||
assert.equal(ruleFor('E002').severity, SEVERITY.ERROR);
|
||||
assert.equal(ruleFor('E003').severity, SEVERITY.ERROR);
|
||||
assert.equal(ruleFor('E004').severity, SEVERITY.ERROR);
|
||||
assert.equal(ruleFor('W001').severity, SEVERITY.WARNING);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── E002 — PROJECT.md not found ────────────────────────────────────────────
|
||||
|
||||
describe('E002 — PROJECT.md not found', () => {
|
||||
test('fires when PROJECT.md is absent', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e002-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('E002').check(snapshot);
|
||||
|
||||
assert.equal(diagnostics.length, 1);
|
||||
assert.deepEqual(diagnostics[0], {
|
||||
code: 'E002',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'PROJECT.md not found',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: '/gsd-new-project' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('does not fire when PROJECT.md exists', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e002-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeProject(cwd, '# My Project\n\n## What This Is\n\ntext\n');
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('E002').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── E003 — ROADMAP.md not found ────────────────────────────────────────────
|
||||
|
||||
describe('E003 — ROADMAP.md not found', () => {
|
||||
test('fires when ROADMAP.md is absent (milestone.scope === UNREADABLE)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e003-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('E003').check(snapshot);
|
||||
|
||||
assert.equal(diagnostics.length, 1);
|
||||
assert.deepEqual(diagnostics[0], {
|
||||
code: 'E003',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'ROADMAP.md not found',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: '/gsd-new-milestone' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('does not fire when ROADMAP.md exists and is readable', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e003-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeState(cwd, '---\nmilestone: v1.0\n---\n');
|
||||
writeRoadmap(cwd, '## v1.0 Current 🚧\n\n### Phase 1: Foo\n');
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('E003').check(snapshot), []);
|
||||
});
|
||||
|
||||
// Documents the KNOWN AMBIGUITY flagged in this batch's implementer report:
|
||||
// `snapshot.milestone.scope` collapses "absent" and "present-but-unreadable"
|
||||
// into the same UNREADABLE scope, so this rule ALSO fires (with its
|
||||
// absence-shaped message) on a present-but-corrupt ROADMAP.md. Asserted
|
||||
// explicitly here rather than left undocumented, per §8.5 fixture-proof.
|
||||
test('KNOWN AMBIGUITY: also fires (message says "not found") when ROADMAP.md exists but is unreadable', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e003-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
makeFileUnreadableAsDir(path.join(planningDirOf(cwd), 'ROADMAP.md'));
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('E003').check(snapshot);
|
||||
assert.equal(diagnostics.length, 1);
|
||||
assert.equal(diagnostics[0].code, 'E003');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── E004 — STATE.md not found ──────────────────────────────────────────────
|
||||
|
||||
describe('E004 — STATE.md not found', () => {
|
||||
test('fires when STATE.md is absent (currentPhaseLabel.scope === UNREADABLE)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e004-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('E004').check(snapshot);
|
||||
|
||||
assert.equal(diagnostics.length, 1);
|
||||
assert.deepEqual(diagnostics[0], {
|
||||
code: 'E004',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'STATE.md not found',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.REGENERATE_STATE,
|
||||
risk: REMEDY_RISK.DESTRUCTIVE,
|
||||
args: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('does not fire when STATE.md exists and is readable', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e004-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeState(cwd, '---\nmilestone: v1.0\n---\n\n## Current Position\n\nPhase: 1 of 2\n');
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('E004').check(snapshot), []);
|
||||
});
|
||||
|
||||
// Documents the KNOWN GAP flagged in this batch's implementer report:
|
||||
// unlike `config`, `currentPhaseLabel` carries no `exists` discriminator —
|
||||
// `buildStateFields` cannot distinguish "STATE.md absent" from
|
||||
// "STATE.md present but unreadable/corrupt" at all, so this rule fires
|
||||
// identically for both. Asserted explicitly here, per §8.5 fixture-proof.
|
||||
test('KNOWN GAP: also fires (message says "not found") when STATE.md exists but is unreadable', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e004-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
makeFileUnreadableAsDir(path.join(planningDirOf(cwd), 'STATE.md'));
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('E004').check(snapshot);
|
||||
assert.equal(diagnostics.length, 1);
|
||||
assert.equal(diagnostics[0].code, 'E004');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W001 — PROJECT.md missing a required section ─────────────────────────
|
||||
|
||||
describe('W001 — PROJECT.md missing section', () => {
|
||||
test('fires once per missing required section (all 3 missing)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w001-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeProject(cwd, '# My Project\n\nno sections at all\n');
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W001').check(snapshot);
|
||||
|
||||
assert.equal(diagnostics.length, 3);
|
||||
assert.deepEqual(
|
||||
diagnostics.map((d) => d.message).sort(),
|
||||
[
|
||||
'PROJECT.md missing section: ## Core Value',
|
||||
'PROJECT.md missing section: ## Requirements',
|
||||
'PROJECT.md missing section: ## What This Is',
|
||||
],
|
||||
);
|
||||
for (const d of diagnostics) {
|
||||
assert.equal(d.code, 'W001');
|
||||
assert.equal(d.severity, SEVERITY.WARNING);
|
||||
assert.deepEqual(d.remedy, {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: 'Add section manually' },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('fires only for the sections actually missing (boundary: 1 of 3 missing)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w001-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeProject(
|
||||
cwd,
|
||||
['# My Project', '', '## What This Is', '', '## Core Value', ''].join('\n'),
|
||||
);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W001').check(snapshot);
|
||||
|
||||
assert.equal(diagnostics.length, 1);
|
||||
assert.equal(diagnostics[0].message, 'PROJECT.md missing section: ## Requirements');
|
||||
});
|
||||
|
||||
test('does not fire when all 3 required sections are present', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w001-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeProject(
|
||||
cwd,
|
||||
[
|
||||
'# My Project',
|
||||
'',
|
||||
'## What This Is',
|
||||
'',
|
||||
'## Core Value',
|
||||
'',
|
||||
'## Requirements',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W001').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire when PROJECT.md is absent — that is E002s job, not W001s', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w001-4-');
|
||||
t.after(() => cleanup(cwd));
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W001').check(snapshot), []);
|
||||
// E002 covers the absence case instead.
|
||||
assert.equal(ruleFor('E002').check(snapshot).length, 1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user