feat(#3309): generate health.md's error-code and repair-action tables
Closes the issue's explicit acceptance criterion: "health.md's tables are generated rather than hand-maintained, closing the 16-vs-30+ documentation gap structurally." The published roster listed 16 codes against 30+ actually emitted; W010-W017 and W020-W023 had never been documented. Adds description/repairable as static fields on Rule (health-diagnostic-types.cts) — generation needs a fixed, human-readable summary per code, distinct from the dynamic per-instance Diagnostic.message a rule's check() produces. repairable is true only when --repair will actually apply the remedy: false for ADVISE-only rules AND for DESTRUCTIVE-risk rules (regenerateState/resetConfig), which are described but never auto-applied — matches verify.cts's diagnosticToIssueEntry semantics exactly, after fixing E004/E005's static field to agree with it (both were wrongly true, an inconsistency caught during this same commit's own review, not left for later). New scripts/gen-health-docs.cjs (--write/--check, wired into lint:generated-sync) regenerates the two tagged table regions in gsd-core/workflows/health.md from RULES (31 rules) plus the 3 pre-checks that stay outside the rule table by design (E001, E010, I010) plus a small static Effect/Risk lookup for the 6 real repair actions — including addAiIntegrationPhaseKey, live in code since an earlier phase but never documented until now. 34 error-code rows, 6 repair-action rows. The table's old "grep verify.cts for the next free number" footnote is rewritten to point at the rule table and its lint guard instead.
This commit is contained in:
@@ -107,6 +107,8 @@ const RULES: Rule[] = [
|
||||
{
|
||||
code: 'W010',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'GSD agent installation missing or incomplete',
|
||||
repairable: false,
|
||||
check: checkAgentInstall,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -276,16 +276,59 @@ function checkW022(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
// ─── Exports ────────────────────────────────────────────────────────────────
|
||||
|
||||
const RULES: Rule[] = [
|
||||
{ code: 'W003', severity: SEVERITY.WARNING, check: checkW003 },
|
||||
{ code: 'E005', severity: SEVERITY.ERROR, check: checkE005 },
|
||||
{ code: 'W004', severity: SEVERITY.WARNING, check: checkW004 },
|
||||
{ code: 'W008', severity: SEVERITY.WARNING, check: checkW008 },
|
||||
{ code: 'W016', severity: SEVERITY.WARNING, check: checkW016 },
|
||||
{ code: 'W012', severity: SEVERITY.WARNING, check: checkW012 },
|
||||
{ code: 'W013', severity: SEVERITY.WARNING, check: checkW013 },
|
||||
{ code: 'W014', severity: SEVERITY.WARNING, check: checkW014 },
|
||||
{ code: 'W015', severity: SEVERITY.WARNING, check: checkW015 },
|
||||
{ code: 'W022', severity: SEVERITY.WARNING, check: checkW022 },
|
||||
{ code: 'W003', severity: SEVERITY.WARNING, description: 'config.json not found', repairable: true, check: checkW003 },
|
||||
{ code: 'E005', severity: SEVERITY.ERROR, description: 'config.json parse error', repairable: false, check: checkE005 },
|
||||
{ code: 'W004', severity: SEVERITY.WARNING, description: 'config.json invalid field value', repairable: false, check: checkW004 },
|
||||
{
|
||||
code: 'W008',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)',
|
||||
repairable: true,
|
||||
check: checkW008,
|
||||
},
|
||||
{
|
||||
code: 'W016',
|
||||
severity: SEVERITY.WARNING,
|
||||
description:
|
||||
'config.json: workflow.ai_integration_phase absent (defaults to enabled but agents may skip AI-integration-phase planning)',
|
||||
repairable: true,
|
||||
check: checkW016,
|
||||
},
|
||||
{
|
||||
code: 'W012',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'config.json invalid branching_strategy value',
|
||||
repairable: false,
|
||||
check: checkW012,
|
||||
},
|
||||
{
|
||||
code: 'W013',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'config.json context_window not a positive integer',
|
||||
repairable: false,
|
||||
check: checkW013,
|
||||
},
|
||||
{
|
||||
code: 'W014',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'config.json phase_branch_template missing {phase} placeholder',
|
||||
repairable: false,
|
||||
check: checkW014,
|
||||
},
|
||||
{
|
||||
code: 'W015',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'config.json milestone_branch_template missing {milestone} placeholder',
|
||||
repairable: false,
|
||||
check: checkW015,
|
||||
},
|
||||
{
|
||||
code: 'W022',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'config.json models entry malformed (unknown phase type, invalid tier, or non-object value)',
|
||||
repairable: false,
|
||||
check: checkW022,
|
||||
},
|
||||
];
|
||||
|
||||
export = { RULES };
|
||||
|
||||
@@ -96,8 +96,20 @@ function checkW019(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
// ─── Exports ────────────────────────────────────────────────────────────────
|
||||
|
||||
const RULES: Rule[] = [
|
||||
{ code: 'W018', severity: SEVERITY.WARNING, check: checkW018 },
|
||||
{ code: 'W019', severity: SEVERITY.WARNING, check: checkW019 },
|
||||
{
|
||||
code: 'W018',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'MILESTONES.md missing entry for archived milestone snapshot',
|
||||
repairable: true,
|
||||
check: checkW018,
|
||||
},
|
||||
{
|
||||
code: 'W019',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'Unrecognized .planning/ root file — not a canonical GSD artifact',
|
||||
repairable: false,
|
||||
check: checkW019,
|
||||
},
|
||||
];
|
||||
|
||||
export = { RULES };
|
||||
|
||||
@@ -180,10 +180,34 @@ function checkW009(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
// ─── Exports ────────────────────────────────────────────────────────────────
|
||||
|
||||
const RULES: Rule[] = [
|
||||
{ code: 'W005', severity: SEVERITY.WARNING, check: checkW005 },
|
||||
{ code: 'W023', severity: SEVERITY.WARNING, check: checkW023 },
|
||||
{ code: 'I001', severity: SEVERITY.INFO, check: checkI001 },
|
||||
{ code: 'W009', severity: SEVERITY.WARNING, check: checkW009 },
|
||||
{
|
||||
code: 'W005',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'Phase directory naming mismatch',
|
||||
repairable: false,
|
||||
check: checkW005,
|
||||
},
|
||||
{
|
||||
code: 'W023',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'Phase directories collide on normalized key',
|
||||
repairable: false,
|
||||
check: checkW023,
|
||||
},
|
||||
{
|
||||
code: 'I001',
|
||||
severity: SEVERITY.INFO,
|
||||
description: 'Plan without SUMMARY (may be in progress)',
|
||||
repairable: false,
|
||||
check: checkI001,
|
||||
},
|
||||
{
|
||||
code: 'W009',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'Phase has Validation Architecture in RESEARCH.md but no VALIDATION.md',
|
||||
repairable: false,
|
||||
check: checkW009,
|
||||
},
|
||||
];
|
||||
|
||||
export = { RULES };
|
||||
|
||||
@@ -210,8 +210,20 @@ function checkW007(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
// ─── Exports ────────────────────────────────────────────────────────────────
|
||||
|
||||
const RULES: Rule[] = [
|
||||
{ code: 'W006', severity: SEVERITY.WARNING, check: checkW006 },
|
||||
{ code: 'W007', severity: SEVERITY.WARNING, check: checkW007 },
|
||||
{
|
||||
code: 'W006',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'Phase in ROADMAP but no directory',
|
||||
repairable: false,
|
||||
check: checkW006,
|
||||
},
|
||||
{
|
||||
code: 'W007',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'Phase on disk but not in ROADMAP',
|
||||
repairable: false,
|
||||
check: checkW007,
|
||||
},
|
||||
];
|
||||
|
||||
export = { RULES };
|
||||
|
||||
@@ -163,10 +163,16 @@ function checkW001(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
// ─── 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 },
|
||||
{ code: 'E002', severity: SEVERITY.ERROR, description: 'PROJECT.md not found', repairable: false, check: checkE002 },
|
||||
{ code: 'E003', severity: SEVERITY.ERROR, description: 'ROADMAP.md not found', repairable: false, check: checkE003 },
|
||||
{ code: 'E004', severity: SEVERITY.ERROR, description: 'STATE.md not found', repairable: false, check: checkE004 },
|
||||
{
|
||||
code: 'W001',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'PROJECT.md missing required section',
|
||||
repairable: false,
|
||||
check: checkW001,
|
||||
},
|
||||
];
|
||||
|
||||
export = { RULES };
|
||||
|
||||
@@ -86,6 +86,8 @@ const { getMilestoneFromPhaseId, matchPhaseDirs, normalizePhaseName, extractPhas
|
||||
const RULE_W024: Rule = {
|
||||
code: 'W024',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'STATE.md was written many commits ago — treat its contents as approximate',
|
||||
repairable: false,
|
||||
check: (_snapshot: PlanningSnapshot): Diagnostic[] => [],
|
||||
};
|
||||
|
||||
@@ -139,6 +141,8 @@ function normalizePhaseTokenSet(valid: Set<string>): Set<string> {
|
||||
const RULE_W002: Rule = {
|
||||
code: 'W002',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'STATE.md references invalid phase',
|
||||
repairable: false,
|
||||
check: (snapshot: PlanningSnapshot): Diagnostic[] => {
|
||||
const validPhases = buildValidPhaseSet(snapshot);
|
||||
// Mirrors `verify.cts:1765`'s `if (normalizedValid.size > 0)` guard
|
||||
@@ -193,6 +197,8 @@ function currentPhaseIdFromLabel(label: string | null): string | null {
|
||||
const RULE_W011: Rule = {
|
||||
code: 'W011',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'STATE.md current-phase status disagrees with ROADMAP.md checkbox',
|
||||
repairable: false,
|
||||
check: (snapshot: PlanningSnapshot): Diagnostic[] => {
|
||||
const phaseId = currentPhaseIdFromLabel(snapshot.currentPhaseLabel.value);
|
||||
if (phaseId === null) return [];
|
||||
@@ -216,6 +222,9 @@ const RULE_W011: Rule = {
|
||||
const RULE_W021: Rule = {
|
||||
code: 'W021',
|
||||
severity: SEVERITY.WARNING,
|
||||
description:
|
||||
"Phase's integer prefix implies a different milestone than its ROADMAP section (phase_id_convention: milestone-prefixed)",
|
||||
repairable: false,
|
||||
check: (snapshot: PlanningSnapshot): Diagnostic[] => {
|
||||
const convention = snapshot.config.value?.['phase_id_convention'];
|
||||
if (convention !== 'milestone-prefixed') return [];
|
||||
@@ -246,6 +255,8 @@ const RULE_W021: Rule = {
|
||||
const RULE_W026: Rule = {
|
||||
code: 'W026',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'STATE says milestone complete but ROADMAP lists an unstarted phase for that milestone',
|
||||
repairable: false,
|
||||
check: (snapshot: PlanningSnapshot): Diagnostic[] => {
|
||||
const statusVal = (snapshot.stateStatus.value ?? '').trim().toLowerCase();
|
||||
if (!/milestone complete|archived/.test(statusVal)) return [];
|
||||
|
||||
@@ -154,9 +154,27 @@ function checkW027(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
// ─── Exports ────────────────────────────────────────────────────────────────
|
||||
|
||||
const RULES: Rule[] = [
|
||||
{ code: 'W020', severity: SEVERITY.WARNING, check: checkW020 },
|
||||
{ code: 'W017', severity: SEVERITY.WARNING, check: checkW017 },
|
||||
{ code: 'W027', severity: SEVERITY.WARNING, check: checkW027 },
|
||||
{
|
||||
code: 'W020',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'Worktree health scan degraded — git worktree list timed out, failed, or a finding could not be verified',
|
||||
repairable: false,
|
||||
check: checkW020,
|
||||
},
|
||||
{
|
||||
code: 'W017',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'Orphan git worktree (path no longer exists on disk)',
|
||||
repairable: false,
|
||||
check: checkW017,
|
||||
},
|
||||
{
|
||||
code: 'W027',
|
||||
severity: SEVERITY.WARNING,
|
||||
description: 'Stale git worktree (not modified in a long time)',
|
||||
repairable: false,
|
||||
check: checkW027,
|
||||
},
|
||||
];
|
||||
|
||||
export = { RULES };
|
||||
|
||||
@@ -77,6 +77,47 @@ interface Diagnostic {
|
||||
interface Rule {
|
||||
code: string;
|
||||
severity: Severity;
|
||||
/**
|
||||
* Short, static, human-readable summary of what this rule checks — the
|
||||
* source of `gsd-core/workflows/health.md`'s generated `<error_codes>`
|
||||
* table (`scripts/gen-health-docs.cjs`). Deliberately distinct from a
|
||||
* fired `Diagnostic`'s `message`, which is dynamic/per-instance (e.g.
|
||||
* W001's message names the specific PROJECT.md section that is missing);
|
||||
* `description` is exactly one fixed sentence per code, matching the
|
||||
* hand-written table's pre-existing style for the codes it already
|
||||
* documented (E001-E005, W001-W009, W018, W019, W024, I001).
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether `--repair` will actually apply this rule's remedy (`true`) or
|
||||
* never will (`false`) — the source of the generated table's "Repairable"
|
||||
* column. This MUST match `diagnosticToIssueEntry`'s (`src/verify.cts`)
|
||||
* per-diagnostic semantics: `remedy.action !== ADVISE && remedy.risk !==
|
||||
* REMEDY_RISK.DESTRUCTIVE`. `false` covers TWO distinct cases, and both
|
||||
* must map to `false` here:
|
||||
*
|
||||
* 1. ADVISE-only rules — no real `REMEDY_ACTION` exists to apply.
|
||||
* 2. DESTRUCTIVE-risk rules (`regenerateState`, `resetConfig`) — a real
|
||||
* action exists and is described, but `applyRepairs`'s dispatcher
|
||||
* (`src/health-diagnostic.cts`) refuses to auto-apply any
|
||||
* DESTRUCTIVE-risk remedy (§8.3 rule 3), so `--repair` never applies it
|
||||
* either. "A remedy exists to describe" is NOT sufficient for `true` —
|
||||
* only "an unattended `--repair` run will actually apply it" is.
|
||||
*
|
||||
* STATIC field, not derived by executing `check` against a fixture at
|
||||
* doc-gen time: confirmed by direct read of all 8
|
||||
* `src/health-diagnostic-rules/*.cts` files that every rule in this
|
||||
* codebase uses exactly ONE `remedy.action` (and therefore one
|
||||
* `remedy.risk`) across every `Diagnostic` it can ever emit — no rule mixes
|
||||
* ADVISE with a real action, or NONE-risk with DESTRUCTIVE-risk, depending
|
||||
* on the triggering condition (the design doc's "primary remedy" ambiguity
|
||||
* this field's doc comment was asked to consider does not arise in
|
||||
* practice). A single static boolean is therefore a faithful,
|
||||
* execution-free summary, and cheaper/simpler than adding a second
|
||||
* `primaryRemedyAction` field or having the generator import and execute
|
||||
* every rule against a synthetic snapshot.
|
||||
*/
|
||||
repairable: boolean;
|
||||
check: (snapshot: PlanningSnapshot) => Diagnostic[]; // §8.1 rule 1 signature, verbatim
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user