refactor(#3309): extract health-diagnostic-types leaf module, wire RULES

Wiring all 8 rule-group files into health-diagnostic.cts's RULES array
created a genuine CJS circular dependency: each group file required
health-diagnostic.cjs back for the shared enums, and health-diagnostic.cjs
now required the group files forward, so the enums were undefined
mid-load (destructuring health-diagnostic.cjs's still-unassigned
exports).

Fixes it by splitting the enums/types (SEVERITY, REMEDY_ACTION,
REMEDY_RISK, Remedy, Diagnostic, Rule) into a dependency-free leaf
module, health-diagnostic-types.cts, that both sides import instead of
each other. health-diagnostic.cts re-exports the enums for existing
consumers. RULES is now the real concatenation of all 8 groups (31
codes — E001 intentionally stays a pre-check outside the table).
This commit is contained in:
sim
2026-08-13 01:46:20 -04:00
parent c469aeeacd
commit e0021a2fed
15 changed files with 167 additions and 69 deletions

View File

@@ -24,8 +24,8 @@
* ("Rule table organization" — Agent installation group)
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports -- health-diagnostic.cjs is an export= CommonJS module
import healthDiagnosticMod = require('../health-diagnostic.cjs');
// 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;
type Rule = healthDiagnosticMod.Rule;
type Diagnostic = healthDiagnosticMod.Diagnostic;

View File

@@ -54,8 +54,8 @@ import type planningSnapshotMod = require('../planning-snapshot.cjs');
type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnapshot>;
// eslint-disable-next-line @typescript-eslint/no-require-imports -- health-diagnostic.cjs is an export= CommonJS module
import healthDiagnosticMod = require('../health-diagnostic.cjs');
// 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;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Remedy = healthDiagnosticMod.Remedy;

View File

@@ -22,7 +22,7 @@ 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');
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Rule = healthDiagnosticMod.Rule;

View File

@@ -44,7 +44,7 @@ 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');
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Rule = healthDiagnosticMod.Rule;

View File

@@ -87,7 +87,7 @@ 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');
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Rule = healthDiagnosticMod.Rule;

View File

@@ -22,7 +22,7 @@ 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');
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Rule = healthDiagnosticMod.Rule;

View File

@@ -42,7 +42,7 @@
// — unlike `health-diagnostic.cts`'s own type-only import of
// `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.cjs');
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
type Rule = healthDiagnosticMod.Rule;
type Diagnostic = healthDiagnosticMod.Diagnostic;

View File

@@ -64,7 +64,7 @@ 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');
import healthDiagnosticMod = require('../health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
type Diagnostic = healthDiagnosticMod.Diagnostic;
type Rule = healthDiagnosticMod.Rule;

View File

@@ -0,0 +1,104 @@
/**
* Health Diagnostic Types — shared, dependency-free rule-table types (Phase
* 11, #3309, ADR-3180 §8.2/§8.3/§8.5).
*
* Split out from `src/health-diagnostic.cts` to break a CJS circular
* dependency between the evaluator and its own rule-group files
* (`src/health-diagnostic-rules/*.cts`): those files need the frozen
* `SEVERITY`/`REMEDY_ACTION`/`REMEDY_RISK` enums and the `Diagnostic`/
* `Remedy`/`Rule` shapes, but the evaluator (`health-diagnostic.cts`) also
* needs to `require()` every rule-group file to populate its `RULES` array —
* a rule-group file requiring `health-diagnostic.cjs` back, mid-load, reads
* `module.exports` before it is assigned, so the destructured enums come
* back `undefined`. This leaf has NO runtime dependency on anything in that
* cycle, so both sides can depend on it directly.
*
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
* Test matrix: .gsd/phase/refactor-3309-health-diagnostic-rule-table/50-test-matrix.md
*
* ADR-457 build-at-publish: source in src/health-diagnostic-types.cts,
* compiled to gsd-core/bin/lib/health-diagnostic-types.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>;
// ─── Severity ───────────────────────────────────────────────────────────────
const SEVERITY = Object.freeze({
ERROR: 'error',
WARNING: 'warning',
INFO: 'info',
});
type Severity = (typeof SEVERITY)[keyof typeof SEVERITY];
// ─── Remedy action / risk ───────────────────────────────────────────────────
// Harvested from health.md's published table + the corrected 6-action
// implementation (`src/verify.cts:2405-2553`) — not 5; `addAiIntegrationPhaseKey`
// (verify.cts:1860/2481-2502) was live in code, missing from docs (design
// doc, "Ground truth vs. issue #3309's claims" section).
const REMEDY_ACTION = Object.freeze({
CREATE_CONFIG: 'createConfig',
RESET_CONFIG: 'resetConfig',
REGENERATE_STATE: 'regenerateState',
ADD_NYQUIST_KEY: 'addNyquistKey',
ADD_AI_INTEGRATION_PHASE_KEY: 'addAiIntegrationPhaseKey',
BACKFILL_MILESTONES: 'backfillMilestones',
// §8.3 rule 5 — every non-repairable finding's `fix` string becomes an
// ADVISE payload; ADVISE never acts, only describes.
ADVISE: 'advise',
});
type RemedyAction = (typeof REMEDY_ACTION)[keyof typeof REMEDY_ACTION];
const REMEDY_RISK = Object.freeze({
NONE: 'none',
DESTRUCTIVE: 'destructive',
});
type RemedyRisk = (typeof REMEDY_RISK)[keyof typeof REMEDY_RISK];
// ─── Diagnostic / Rule shapes ───────────────────────────────────────────────
interface Remedy {
action: RemedyAction;
risk: RemedyRisk;
args: Record<string, unknown>;
}
interface Diagnostic {
code: string; // e.g. 'W010' — append-only, never renumbered (§8.2 rule 2)
severity: Severity; // property of the RULE, never the emit call (§8.2 rule 3)
message: string;
remedy: Remedy;
}
interface Rule {
code: string;
severity: Severity;
check: (snapshot: PlanningSnapshot) => Diagnostic[]; // §8.1 rule 1 signature, verbatim
}
// ─── Exports ────────────────────────────────────────────────────────────────
const healthDiagnosticTypes = {
SEVERITY,
REMEDY_ACTION,
REMEDY_RISK,
};
// Namespace merge (same binding name as the value above) is how a CommonJS
// `export =` module exposes a type alongside its runtime export — `export
// type` is rejected by TS2309 ("An export assignment cannot be used in a
// module with other exported elements") when combined with `export =`, so
// these types ride along on the exported object via declaration merging
// instead. Mirrors `src/planning-scope.cts`'s exact mechanism. Consumers
// doing `import x = require('./health-diagnostic-types.cjs')` can reference
// the types as `x.Severity`, `x.RemedyAction`, etc.
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace healthDiagnosticTypes {
export { Severity, RemedyAction, RemedyRisk, Remedy, Diagnostic, Rule };
}
export = healthDiagnosticTypes;

View File

@@ -30,68 +30,55 @@ import type planningSnapshotMod = require('./planning-snapshot.cjs');
type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnapshot>;
// ─── Severity ───────────────────────────────────────────────────────────────
const SEVERITY = Object.freeze({
ERROR: 'error',
WARNING: 'warning',
INFO: 'info',
});
type Severity = (typeof SEVERITY)[keyof typeof SEVERITY];
// ─── Remedy action / risk ───────────────────────────────────────────────────
// Harvested from health.md's published table + the corrected 6-action
// implementation (`src/verify.cts:2405-2553`) — not 5; `addAiIntegrationPhaseKey`
// (verify.cts:1860/2481-2502) was live in code, missing from docs (design
// doc, "Ground truth vs. issue #3309's claims" section).
const REMEDY_ACTION = Object.freeze({
CREATE_CONFIG: 'createConfig',
RESET_CONFIG: 'resetConfig',
REGENERATE_STATE: 'regenerateState',
ADD_NYQUIST_KEY: 'addNyquistKey',
ADD_AI_INTEGRATION_PHASE_KEY: 'addAiIntegrationPhaseKey',
BACKFILL_MILESTONES: 'backfillMilestones',
// §8.3 rule 5 — every non-repairable finding's `fix` string becomes an
// ADVISE payload; ADVISE never acts, only describes.
ADVISE: 'advise',
});
type RemedyAction = (typeof REMEDY_ACTION)[keyof typeof REMEDY_ACTION];
const REMEDY_RISK = Object.freeze({
NONE: 'none',
DESTRUCTIVE: 'destructive',
});
type RemedyRisk = (typeof REMEDY_RISK)[keyof typeof REMEDY_RISK];
// ─── Diagnostic / Rule shapes ───────────────────────────────────────────────
interface Remedy {
action: RemedyAction;
risk: RemedyRisk;
args: Record<string, unknown>;
}
interface Diagnostic {
code: string; // e.g. 'W010' — append-only, never renumbered (§8.2 rule 2)
severity: Severity; // property of the RULE, never the emit call (§8.2 rule 3)
message: string;
remedy: Remedy;
}
interface Rule {
code: string;
severity: Severity;
check: (snapshot: PlanningSnapshot) => Diagnostic[]; // §8.1 rule 1 signature, verbatim
}
// Runtime values (SEVERITY/REMEDY_ACTION/REMEDY_RISK) are needed here — not
// just types — for `applyRepairs`'s comparisons, so this is a normal
// (non type-only) `import ... = require(...)`. `health-diagnostic-types.cjs`
// is the leaf module these enums/types were extracted to, so that this file
// can `require()` every rule-group file below without a circular dependency
// (see that module's file-level comment for the full explanation).
// eslint-disable-next-line @typescript-eslint/no-require-imports
import healthDiagnosticTypesMod = require('./health-diagnostic-types.cjs');
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticTypesMod;
type Severity = healthDiagnosticTypesMod.Severity;
type RemedyAction = healthDiagnosticTypesMod.RemedyAction;
type RemedyRisk = healthDiagnosticTypesMod.RemedyRisk;
type Remedy = healthDiagnosticTypesMod.Remedy;
type Diagnostic = healthDiagnosticTypesMod.Diagnostic;
type Rule = healthDiagnosticTypesMod.Rule;
// ─── Rule table ─────────────────────────────────────────────────────────────
// Starts EMPTY. A later migration batch appends each of the 32 rule
// functions extracted from `cmdValidateHealth` (design doc, "Rule table
// organization" section) — this phase establishes only the container and its
// type.
const RULES: Rule[] = [];
// Populated by concatenating each rule group's exported `RULES` array (design
// doc, "Rule table organization" section) — the 32 rule functions extracted
// from `cmdValidateHealth`, `src/verify.cts:1616-2577`.
// eslint-disable-next-line @typescript-eslint/no-require-imports
import rootExistenceMod = require('./health-diagnostic-rules/root-existence.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import stateConsistencyMod = require('./health-diagnostic-rules/state-consistency.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import configValidationMod = require('./health-diagnostic-rules/config-validation.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import phaseStructureMod = require('./health-diagnostic-rules/phase-structure.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import agentInstallMod = require('./health-diagnostic-rules/agent-install.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import roadmapDiskConsistencyMod = require('./health-diagnostic-rules/roadmap-disk-consistency.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import worktreeHealthMod = require('./health-diagnostic-rules/worktree-health.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import milestoneArchiveHygieneMod = require('./health-diagnostic-rules/milestone-archive-hygiene.cjs');
const RULES: Rule[] = [
...rootExistenceMod.RULES,
...stateConsistencyMod.RULES,
...configValidationMod.RULES,
...phaseStructureMod.RULES,
...agentInstallMod.RULES,
...roadmapDiskConsistencyMod.RULES,
...worktreeHealthMod.RULES,
...milestoneArchiveHygieneMod.RULES,
];
// ─── Evaluator ──────────────────────────────────────────────────────────────