Merge pull request #1149 from Solvely-Colin/Solvely/health-non-destructive-state-repair
fix(health): preserve state on phase mismatch repair
This commit is contained in:
@@ -609,8 +609,12 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) {
|
||||
// Only warn if phases dir has any content (not just an empty project)
|
||||
if (diskPhases.size > 0) {
|
||||
addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`, 'Run /gsd:health --repair to regenerate STATE.md', true);
|
||||
if (!repairs.includes('regenerateState')) repairs.push('regenerateState');
|
||||
addIssue(
|
||||
'warning',
|
||||
'W002',
|
||||
`STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`,
|
||||
'Review STATE.md manually before changing it; /gsd:health --repair will not overwrite an existing STATE.md for phase mismatches'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ Errors: N | Warnings: N | Info: N
|
||||
```
|
||||
## Warnings
|
||||
|
||||
- [W001] STATE.md references phase 5, but only phases 1-3 exist
|
||||
Fix: Run /gsd:health --repair to regenerate
|
||||
- [W002] STATE.md references phase 5, but only phases 1-3 exist
|
||||
Fix: Review STATE.md manually before changing it; repair will not overwrite an existing STATE.md
|
||||
|
||||
- [W005] Phase directory "1-setup" doesn't follow NN-name format
|
||||
Fix: Rename to match pattern (e.g., 01-setup)
|
||||
@@ -130,7 +130,7 @@ Report final status.
|
||||
| E004 | error | STATE.md not found | Yes |
|
||||
| E005 | error | config.json parse error | Yes |
|
||||
| W001 | warning | PROJECT.md missing required section | No |
|
||||
| W002 | warning | STATE.md references invalid phase | Yes |
|
||||
| W002 | warning | STATE.md references invalid phase | No |
|
||||
| W003 | warning | config.json not found | Yes |
|
||||
| W004 | warning | config.json invalid field value | No |
|
||||
| W005 | warning | Phase directory naming mismatch | No |
|
||||
@@ -148,7 +148,7 @@ Report final status.
|
||||
|--------|--------|------|
|
||||
| createConfig | Create config.json with defaults | None |
|
||||
| resetConfig | Delete + recreate config.json | Loses custom settings |
|
||||
| regenerateState | Create STATE.md from ROADMAP structure | Loses session history |
|
||||
| regenerateState | Create STATE.md from ROADMAP structure when it is missing | Loses session history |
|
||||
| addNyquistKey | Add workflow.nyquist_validation: true to config.json | None — matches existing default |
|
||||
|
||||
**Not repairable (too risky):**
|
||||
|
||||
@@ -191,10 +191,9 @@ describe('validate health command', () => {
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.ok(
|
||||
output.warnings.some(w => w.code === 'W002'),
|
||||
`Expected W002 in warnings: ${JSON.stringify(output.warnings)}`
|
||||
);
|
||||
const w002 = output.warnings.find(w => w.code === 'W002');
|
||||
assert.ok(w002, `Expected W002 in warnings: ${JSON.stringify(output.warnings)}`);
|
||||
assert.strictEqual(w002.repairable, false, 'W002 should not be auto-repairable');
|
||||
});
|
||||
|
||||
// ─── Check 5: config.json valid JSON + valid schema ───────────────────────
|
||||
@@ -613,16 +612,13 @@ describe('validate health --repair command', () => {
|
||||
assert.ok(stateContent.includes('# Session State'), 'regenerated STATE.md should contain "# Session State"');
|
||||
});
|
||||
|
||||
test('backs up existing STATE.md before regenerating', () => {
|
||||
test('does not rewrite existing STATE.md for invalid phase references', () => {
|
||||
writeValidConfigJson(tmpDir);
|
||||
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
|
||||
const originalContent = '# Session State\n\nOriginal content here.\n';
|
||||
fs.writeFileSync(statePath, originalContent);
|
||||
|
||||
// Make STATE.md reference a nonexistent phase so repair is triggered
|
||||
const originalContent = '# Session State\n\nPhase 99 is current.\n';
|
||||
fs.writeFileSync(
|
||||
statePath,
|
||||
'# Session State\n\nPhase 99 is current.\n'
|
||||
originalContent
|
||||
);
|
||||
|
||||
const result = runGsdTools('validate health --repair', tmpDir);
|
||||
@@ -630,19 +626,17 @@ describe('validate health --repair command', () => {
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.ok(
|
||||
Array.isArray(output.repairs_performed),
|
||||
`Expected repairs_performed: ${JSON.stringify(output)}`
|
||||
!Array.isArray(output.repairs_performed) || !output.repairs_performed.some(r => r.action === 'regenerateState'),
|
||||
`Did not expect regenerateState for W002: ${JSON.stringify(output)}`
|
||||
);
|
||||
|
||||
// Verify a .bak- file exists alongside STATE.md
|
||||
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
||||
assert.strictEqual(stateContent, originalContent, 'existing STATE.md should be preserved');
|
||||
|
||||
const planningDir = path.join(tmpDir, '.planning');
|
||||
const planningFiles = fs.readdirSync(planningDir);
|
||||
const backupFile = planningFiles.find(f => f.startsWith('STATE.md.bak-'));
|
||||
assert.ok(backupFile, `Expected a STATE.md.bak- file. Found files: ${planningFiles.join(', ')}`);
|
||||
|
||||
// Verify backup contains the original content
|
||||
const backupContent = fs.readFileSync(path.join(planningDir, backupFile), 'utf-8');
|
||||
assert.ok(backupContent.includes('Phase 99'), 'backup should contain the original STATE.md content');
|
||||
assert.strictEqual(backupFile, undefined, `Did not expect backup file for non-destructive repair. Found: ${planningFiles.join(', ')}`);
|
||||
});
|
||||
|
||||
test('adds nyquist_validation key to config.json via addNyquistKey repair', () => {
|
||||
@@ -688,4 +682,18 @@ describe('validate health --repair command', () => {
|
||||
`Expected repairable_count >= 2, got ${output.repairable_count}. Full output: ${JSON.stringify(output)}`
|
||||
);
|
||||
});
|
||||
|
||||
test('phase mismatch warnings do not count as repairable issues', () => {
|
||||
writeValidConfigJson(tmpDir);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
'# Session State\n\nPhase 99 is the current phase.\n'
|
||||
);
|
||||
|
||||
const result = runGsdTools('validate health', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.repairable_count, 0, `Expected no repairable issues for W002: ${JSON.stringify(output)}`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user