refactor(#3309): consolidate adviseRemedy() into the shared types leaf

adviseRemedy() was defined identically in two of the eight rule-group
files (config-validation.cts, agent-install.cts) while the other six
repeated the same {action: ADVISE, risk: NONE, args: {command}} object
literal inline ~20+ times. Found by the Standards-axis orthogonal
review (Duplicated Code smell).

Moves the one-line helper into health-diagnostic-types.cts, the leaf
module every rule-group file already imports for its enums/types, and
uses it consistently across all 8 files. Pure mechanical refactor — no
ADVISE remedy's command text, code, or action changed.
This commit is contained in:
sim
2026-08-13 02:56:38 -04:00
parent 03258a07a2
commit 8e1ff76204
8 changed files with 44 additions and 98 deletions

View File

@@ -26,10 +26,9 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports -- health-diagnostic-types.cjs is an export= CommonJS module
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
type Rule = healthDiagnosticMod.Rule;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Remedy = healthDiagnosticMod.Remedy;
// 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');
@@ -41,10 +40,6 @@ const { SCOPE } = planningScopeMod;
import { PACKAGE_NAME } from '../package-identity.cjs';
function adviseRemedy(command: string): Remedy {
return { action: REMEDY_ACTION.ADVISE, risk: REMEDY_RISK.NONE, args: { command } };
}
/**
* `check(snapshot)` for W010 — see module header for the exact 4-way
* branching this ports from `verify.cts:1992-2027`, and its message/fix

View File

@@ -56,9 +56,8 @@ type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnaps
// eslint-disable-next-line @typescript-eslint/no-require-imports -- health-diagnostic-types.cjs is an export= CommonJS module
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK, adviseRemedy } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Remedy = healthDiagnosticMod.Remedy;
type Rule = healthDiagnosticMod.Rule;
import { VALID_PROFILES, VALID_TIERS, VALID_PHASE_TYPES } from '../model-catalog.cjs';
@@ -66,10 +65,6 @@ import { VALID_PROFILES, VALID_TIERS, VALID_PHASE_TYPES } from '../model-catalog
// verify.cts:2141 — inlined literal, not exported from anywhere; same list.
const VALID_BRANCHING_STRATEGIES = ['none', 'phase', 'milestone'];
function adviseRemedy(command: string): Remedy {
return { action: REMEDY_ACTION.ADVISE, risk: REMEDY_RISK.NONE, args: { command } };
}
// ─── W003 — config.json not found (verify.cts:1777-1785) ───────────────────
function checkW003(snapshot: PlanningSnapshot): Diagnostic[] {

View File

@@ -23,7 +23,7 @@ type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnaps
// eslint-disable-next-line @typescript-eslint/no-require-imports
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK, adviseRemedy } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Rule = healthDiagnosticMod.Rule;
@@ -85,14 +85,9 @@ function checkW019(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W019',
severity: SEVERITY.WARNING,
message: `Unrecognized .planning/ file: ${filename} — not a canonical GSD artifact`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: {
command:
'Move to .planning/milestones/ archive subdir or delete if stale. See templates/README.md for the canonical artifact list.',
},
},
remedy: adviseRemedy(
'Move to .planning/milestones/ archive subdir or delete if stale. See templates/README.md for the canonical artifact list.',
),
});
}
return diagnostics;

View File

@@ -55,7 +55,7 @@ type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnaps
// eslint-disable-next-line @typescript-eslint/no-require-imports
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Rule = healthDiagnosticMod.Rule;
@@ -77,11 +77,7 @@ function checkW005(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W005',
severity: SEVERITY.WARNING,
message: `Phase directory "${name}" doesn't follow NN-name format`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'Rename to match pattern (e.g., 01-setup)' },
},
remedy: adviseRemedy('Rename to match pattern (e.g., 01-setup)'),
});
}
}
@@ -127,14 +123,9 @@ function checkW023(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W023',
severity: SEVERITY.WARNING,
message: `Phase directories collide on normalized key "${key}": ${described}`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: {
command:
'Inspect each directory; rename or remove the duplicate so only one directory maps to this phase key',
},
},
remedy: adviseRemedy(
'Inspect each directory; rename or remove the duplicate so only one directory maps to this phase key',
),
});
}
return diagnostics;
@@ -161,11 +152,7 @@ function checkI001(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'I001',
severity: SEVERITY.INFO,
message: `Phase ${phase.dir} has ${deficit} plan(s) without a matching summary`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'May be in progress' },
},
remedy: adviseRemedy('May be in progress'),
});
}
}
@@ -183,11 +170,7 @@ function checkW009(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W009',
severity: SEVERITY.WARNING,
message: `Phase ${entry.dir}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'Re-run /gsd-plan-phase with --research to regenerate' },
},
remedy: adviseRemedy('Re-run /gsd-plan-phase with --research to regenerate'),
});
}
}

View File

@@ -88,7 +88,7 @@ type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnaps
// eslint-disable-next-line @typescript-eslint/no-require-imports
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Rule = healthDiagnosticMod.Rule;
@@ -158,11 +158,7 @@ function checkW006(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W006',
severity: SEVERITY.WARNING,
message: `Phase ${phaseId} in ROADMAP.md but no directory on disk`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'Create phase directory or remove from roadmap' },
},
remedy: adviseRemedy('Create phase directory or remove from roadmap'),
});
}
return diagnostics;
@@ -205,11 +201,7 @@ function checkW007(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'W007',
severity: SEVERITY.WARNING,
message: `Phase ${token} exists on disk but not in ROADMAP.md`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'Add to roadmap or remove directory' },
},
remedy: adviseRemedy('Add to roadmap or remove directory'),
});
}
return diagnostics;

View File

@@ -35,7 +35,7 @@ type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnaps
// eslint-disable-next-line @typescript-eslint/no-require-imports
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK, adviseRemedy } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Rule = healthDiagnosticMod.Rule;
@@ -52,11 +52,7 @@ function checkE002(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'E002',
severity: SEVERITY.ERROR,
message: 'PROJECT.md not found',
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: '/gsd-new-project' },
},
remedy: adviseRemedy('/gsd-new-project'),
},
];
}
@@ -86,11 +82,7 @@ function checkE003(snapshot: PlanningSnapshot): Diagnostic[] {
code: 'E003',
severity: SEVERITY.ERROR,
message: 'ROADMAP.md not found',
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: '/gsd-new-milestone' },
},
remedy: adviseRemedy('/gsd-new-milestone'),
},
];
}
@@ -161,11 +153,7 @@ function checkW001(snapshot: PlanningSnapshot): Diagnostic[] {
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' },
},
remedy: adviseRemedy('Add section manually'),
});
}
}

View File

@@ -43,7 +43,7 @@
// `planning-snapshot.cjs`, which never touches that module's runtime values.
// eslint-disable-next-line @typescript-eslint/no-require-imports -- export= CommonJS module
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
type Rule = healthDiagnosticMod.Rule;
type Diagnostic = healthDiagnosticMod.Diagnostic;
@@ -161,14 +161,9 @@ const RULE_W002: Rule = {
code: 'W002',
severity: SEVERITY.WARNING,
message: `STATE.md references phase ${ref}, but only phases ${sortedValid.join(', ')} are declared`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: {
command:
'Review STATE.md manually before changing it; /gsd-health --repair will not overwrite an existing STATE.md for phase mismatches',
},
},
remedy: adviseRemedy(
'Review STATE.md manually before changing it; /gsd-health --repair will not overwrite an existing STATE.md for phase mismatches',
),
});
}
return diagnostics;
@@ -210,11 +205,7 @@ const RULE_W011: Rule = {
code: 'W011',
severity: SEVERITY.WARNING,
message: `STATE.md says current phase is ${phaseId} (status: ${statusVal || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'Run /gsd-progress to re-derive current position, or manually update STATE.md' },
},
remedy: adviseRemedy('Run /gsd-progress to re-derive current position, or manually update STATE.md'),
},
];
},
@@ -243,11 +234,7 @@ const RULE_W021: Rule = {
code: 'W021',
severity: SEVERITY.WARNING,
message: `Phase ${entry.phaseId}: integer prefix implies ${expectedMilestone} but listed under ${entry.milestone}`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'gsd-tools roadmap upgrade --convention milestone-prefixed' },
},
remedy: adviseRemedy('gsd-tools roadmap upgrade --convention milestone-prefixed'),
});
}
return diagnostics;
@@ -283,13 +270,9 @@ const RULE_W026: Rule = {
code: 'W026',
severity: SEVERITY.WARNING,
message: `STATE says milestone complete but ROADMAP lists ${unstarted.length} unstarted phase(s) (e.g. Phase ${unstarted[0]})`,
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: {
command: 'Run validate consistency or re-run complete-milestone after verifying all phases are done',
},
},
remedy: adviseRemedy(
'Run validate consistency or re-run complete-milestone after verifying all phases are done',
),
},
];
},

View File

@@ -80,12 +80,27 @@ interface Rule {
check: (snapshot: PlanningSnapshot) => Diagnostic[]; // §8.1 rule 1 signature, verbatim
}
// ─── adviseRemedy — shared ADVISE-remedy builder ───────────────────────────
/**
* Every rule-group file needs the same `{action: ADVISE, risk: NONE, args:
* {command}}` shape for a non-repairable finding's `fix` string (§8.3 rule
* 5). Was defined identically in `config-validation.cts` and
* `agent-install.cts`, and repeated inline elsewhere — moved to this shared,
* dependency-free leaf so every rule-group file imports one implementation
* instead of duplicating it.
*/
function adviseRemedy(command: string): Remedy {
return { action: REMEDY_ACTION.ADVISE, risk: REMEDY_RISK.NONE, args: { command } };
}
// ─── Exports ────────────────────────────────────────────────────────────────
const healthDiagnosticTypes = {
SEVERITY,
REMEDY_ACTION,
REMEDY_RISK,
adviseRemedy,
};
// Namespace merge (same binding name as the value above) is how a CommonJS