From e0021a2fed7035e3618ee37d10a74530e006a103 Mon Sep 17 00:00:00 2001 From: sim Date: Thu, 13 Aug 2026 01:46:20 -0400 Subject: [PATCH] refactor(#3309): extract health-diagnostic-types leaf module, wire RULES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .gitignore | 1 + CONTEXT.md | 3 + docs/INVENTORY-MANIFEST.json | 1 + docs/INVENTORY.md | 1 + eslint.config.mjs | 1 + src/health-diagnostic-rules/agent-install.cts | 4 +- .../config-validation.cts | 4 +- .../milestone-archive-hygiene.cts | 2 +- .../phase-structure.cts | 2 +- .../roadmap-disk-consistency.cts | 2 +- .../root-existence.cts | 2 +- .../state-consistency.cts | 2 +- .../worktree-health.cts | 2 +- src/health-diagnostic-types.cts | 104 +++++++++++++++++ src/health-diagnostic.cts | 105 ++++++++---------- 15 files changed, 167 insertions(+), 69 deletions(-) create mode 100644 src/health-diagnostic-types.cts diff --git a/.gitignore b/.gitignore index 0cb4f6a17..348012479 100644 --- a/.gitignore +++ b/.gitignore @@ -196,6 +196,7 @@ build/ /gsd-core/bin/lib/planning-workspace.cjs /gsd-core/bin/lib/planning-scope.cjs /gsd-core/bin/lib/planning-snapshot.cjs +/gsd-core/bin/lib/health-diagnostic-types.cjs /gsd-core/bin/lib/health-diagnostic.cjs /gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs /gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs diff --git a/CONTEXT.md b/CONTEXT.md index fd2ccf6a4..3ac7e02ff 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -106,6 +106,9 @@ Leaf module owning the frozen `SCOPE` discriminator (`COMPLETE` / `TRUNCATED` / ### Planning Snapshot Module Module owning the parsed projection of `.planning/` that a diagnostic rule may read, per ADR-3180 §8.1 (Decision 8, Phase 10, #3308). `buildPlanningSnapshot(cwd) → PlanningSnapshot` is composed EXCLUSIVELY from the already-consolidated §7 owners — `getMilestoneInfo` (Roadmap Parser Module), `listMilestonePhaseDirs` (Phase Locator Module), `isPhaseComplete` (Verification Module), `scanPhasePlans` (Plan Scan Module), `stateFieldValue`/`stateCurrentPositionSlice` (STATE.md Document Module), `planningPaths` (Planning Workspace Module) — and introduces no new semantic derivation of its own. `PlanningSnapshot` exposes `milestone`/`phaseDirs`/`phases`/`currentPhaseLabel`, each a `{value, scope}` pair per the Planning Scope Module's frozen `SCOPE` enum; `phases` additionally carries a `PhaseSnapshot[]` (`dir`, `complete`, `verificationStatus`, `planCount`, `summaryCount`, `scope`). The one new piece of logic this module adds is `worstScope(...scopes) → Scope`, a pure severity-ordered combinator (`UNREADABLE` > `UNSCOPED` > `TRUNCATED` > `COMPLETE`) that folds several independently-scoped owner answers about the same phase directory into one composite signal — NOT a re-derivation of any owner (each owner's own algorithm is untouched; only their already-computed `scope` verdicts are combined), but new coordination logic no single owner has the visibility to express. Every exposed field carries PARSED values only, never raw document text — this is structural, not advisory: a diagnostic rule given only the parsed value cannot re-derive a field's location the way `#3162`'s three inert `Current Phase` literal-search predicates did. Read failures on STATE.md (exists-but-unreadable, distinct from absent) are reported via the Unusable Input Diagnostic Module's `warnUnusableInput(UNUSABLE_REASON.STATE_UNREADABLE)`. Guarded by `scripts/lint-planning-snapshot-bypass-drift.cjs` (ratcheted per Decision 4(e), scoped to `DIAGNOSTIC_RULE_FUNCTIONS` — currently `cmdValidateHealth` in `src/verify.cts` only, acknowledging its existing raw `.planning/` reads as debt owned by Phase 11, #3309, which migrates it onto this snapshot). Source of truth: `gsd-core/bin/lib/planning-snapshot.cjs` (generated from `src/planning-snapshot.cts`). Design: `.gsd/phase/refactor-3308-planning-snapshot-parsed-projection/40-design.md`. +### Health Diagnostic Types Module +Leaf module owning the `SEVERITY`/`REMEDY_ACTION`/`REMEDY_RISK` enums and `Diagnostic`/`Remedy`/`Rule` types shared between the Health Diagnostic Module (the evaluator) and the Health Diagnostic Rule Groups (the eight rule-group files it concatenates). Split out of `src/health-diagnostic.cts` (Phase 11, #3309, ADR-3180 §8.2/§8.3/§8.5) to break a CJS circular dependency: the evaluator must `require()` every rule-group file to populate `RULES`, and every rule-group file needs these enums/types — if the rule-group files required the evaluator back, the require cycle would resolve `module.exports` before it is assigned. This leaf has no runtime dependency on either side of that cycle. Source of truth: `gsd-core/bin/lib/health-diagnostic-types.cjs` (generated from `src/health-diagnostic-types.cts`). + ### Health Diagnostic Module Module owning the frozen rule-table contract for `validate health`, per ADR-3180 §8.2/§8.3/§8.5 (Phase 11, #3309). Exposes three frozen enums — `SEVERITY` (`error`/`warning`/`info`), `REMEDY_ACTION` (the six real repair actions harvested from `cmdValidateHealth`'s existing `--repair` implementation — `createConfig`, `resetConfig`, `regenerateState`, `addNyquistKey`, `addAiIntegrationPhaseKey`, `backfillMilestones` — plus `advise`, the non-repairable payload every non-actionable finding's fix text becomes), and `REMEDY_RISK` (`none`/`destructive`) — plus the `Diagnostic`/`Remedy`/`Rule` shapes every rule's `check(snapshot: PlanningSnapshot) → Diagnostic[]` signature and every finding's `remedy` conform to. `RULES: Rule[]` is the rule table a later migration batch appends the 32 rules extracted from `cmdValidateHealth` (`src/verify.cts:1616-2577`) onto; this phase ships it EMPTY, establishing only the container and its type. `evaluateRules(snapshot) → Diagnostic[]` runs every rule in `RULES` against one `PlanningSnapshot` and flattens the results, throwing on any two rules sharing a `code` — defense in depth beside the future static 1:1 lint guard (§8.2 rule 1). `applyRepairs(cwd, diagnostics, repair, backfill) → {applied, refused}` is the `--repair`/`--backfill` dispatcher: a `DESTRUCTIVE` remedy (`resetConfig`/`regenerateState` — health.md's own published table: "loses custom settings" / "loses session history") is reported but never executed by `--repair`, a deliberate, disclosed breaking change (§8.3 rule 3) from `cmdValidateHealth`'s current unconditional application; `backfillMilestones` alone among the `NONE`-risk actions is requested by `--backfill` without `--repair`, mirroring `cmdValidateHealth`'s existing gate (`src/verify.cts:2504`). Per-action repair handlers are stubs in this phase — they land alongside the rules that need them. Source of truth: `gsd-core/bin/lib/health-diagnostic.cjs` (generated from `src/health-diagnostic.cts`). Design: `.gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md`. diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index 03e1b26dc..42a7a7289 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -378,6 +378,7 @@ "graphify.cjs", "gsd2-import.cjs", "handshake-serialized.cjs", + "health-diagnostic-types.cjs", "health-diagnostic.cjs", "hook-bus.cjs", "host-integration-sdk.cjs", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index ed9f2bac3..6ed348057 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -495,6 +495,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`. | `graphify.cjs` | Knowledge-graph build/query/status/diff for `/gsd-graphify` | | `graphify-command-router.cjs` | ADR-959 capability command router for `gsd-tools graphify` — dispatches build/query/status/diff subcommands; first real capability command cutover (phase 4d-impl-2) | | `gsd2-import.cjs` | External-plan ingest for `/gsd-import --from-gsd2` | +| `health-diagnostic-types.cjs` | Shared, dependency-free `SEVERITY`/`REMEDY_ACTION`/`REMEDY_RISK` enums and `Diagnostic`/`Remedy`/`Rule` types for `validate health` — split out of `health-diagnostic.cjs` so its rule-group files can depend on the enums/types without a CJS circular require back into the evaluator (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) | | `health-diagnostic.cjs` | Frozen rule-table contract for `validate health` — `SEVERITY`/`REMEDY_ACTION`/`REMEDY_RISK` enums, `Diagnostic`/`Remedy`/`Rule` shapes, the `RULES` table (empty in this phase; a later migration batch appends the 32 rules extracted from `cmdValidateHealth`), `evaluateRules` (throws on duplicate rule codes), and `applyRepairs` (the `--repair`/`--backfill` dispatcher — refuses `DESTRUCTIVE`-risk remedies) (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) | | `host-integration.cjs` | Host-Integration Interface (ADR-1239 Phase A) — negotiated capability contract over the six host-integration points; `negotiateHostCapabilities` fail-closes on undeclared/unknown/`undocumented` values, typed degradation ladder, host-capability profiles; the 8 `runtime.hostIntegration` axes are validated in `capability-validator.cjs` and sourced per-CLI in `docs/reference/host-integration-capability-matrix.md` | | `host-runtime-detection.cjs` | Host Runtime Detection Module (ADR-2313 Phase 5, #3245) — the detection rung beneath `GSD_RUNTIME` and `.planning/config.json` `runtime` that lets `init` report `agent_runtime: codex` inside a Codex session instead of the hardcoded `claude` default; `detectHostRuntime` returns the typed `{runtime, source, signal}` from citation-backed Codex signals (`CODEX_SANDBOX`/`CODEX_SANDBOX_NETWORK_DISABLED`, else `CODEX_HOME` + `config.toml`), `resolveReportedRuntime` composes the full ladder. Pure, injectable, never writes, never shells out | diff --git a/eslint.config.mjs b/eslint.config.mjs index 5b24e16ab..21a073cb8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -135,6 +135,7 @@ export default tseslint.config( 'gsd-core/bin/lib/configuration.cjs', 'gsd-core/bin/lib/state-document.cjs', 'gsd-core/bin/lib/planning-snapshot.cjs', + 'gsd-core/bin/lib/health-diagnostic-types.cjs', 'gsd-core/bin/lib/health-diagnostic.cjs', 'gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs', 'gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs', diff --git a/src/health-diagnostic-rules/agent-install.cts b/src/health-diagnostic-rules/agent-install.cts index 986b495f3..0e27aa758 100644 --- a/src/health-diagnostic-rules/agent-install.cts +++ b/src/health-diagnostic-rules/agent-install.cts @@ -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; diff --git a/src/health-diagnostic-rules/config-validation.cts b/src/health-diagnostic-rules/config-validation.cts index 3c1e2424a..f4bad1f2f 100644 --- a/src/health-diagnostic-rules/config-validation.cts +++ b/src/health-diagnostic-rules/config-validation.cts @@ -54,8 +54,8 @@ import type planningSnapshotMod = require('../planning-snapshot.cjs'); type PlanningSnapshot = ReturnType; -// 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; diff --git a/src/health-diagnostic-rules/milestone-archive-hygiene.cts b/src/health-diagnostic-rules/milestone-archive-hygiene.cts index 58f617296..84658425b 100644 --- a/src/health-diagnostic-rules/milestone-archive-hygiene.cts +++ b/src/health-diagnostic-rules/milestone-archive-hygiene.cts @@ -22,7 +22,7 @@ import type planningSnapshotMod = require('../planning-snapshot.cjs'); type PlanningSnapshot = ReturnType; // 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; diff --git a/src/health-diagnostic-rules/phase-structure.cts b/src/health-diagnostic-rules/phase-structure.cts index 8d8667e31..43937141d 100644 --- a/src/health-diagnostic-rules/phase-structure.cts +++ b/src/health-diagnostic-rules/phase-structure.cts @@ -44,7 +44,7 @@ import type planningSnapshotMod = require('../planning-snapshot.cjs'); type PlanningSnapshot = ReturnType; // 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; diff --git a/src/health-diagnostic-rules/roadmap-disk-consistency.cts b/src/health-diagnostic-rules/roadmap-disk-consistency.cts index 76028977e..937c7ae36 100644 --- a/src/health-diagnostic-rules/roadmap-disk-consistency.cts +++ b/src/health-diagnostic-rules/roadmap-disk-consistency.cts @@ -87,7 +87,7 @@ import type planningSnapshotMod = require('../planning-snapshot.cjs'); type PlanningSnapshot = ReturnType; // 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; diff --git a/src/health-diagnostic-rules/root-existence.cts b/src/health-diagnostic-rules/root-existence.cts index 3e991cf53..5e6f93dbe 100644 --- a/src/health-diagnostic-rules/root-existence.cts +++ b/src/health-diagnostic-rules/root-existence.cts @@ -22,7 +22,7 @@ import type planningSnapshotMod = require('../planning-snapshot.cjs'); type PlanningSnapshot = ReturnType; // 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; diff --git a/src/health-diagnostic-rules/state-consistency.cts b/src/health-diagnostic-rules/state-consistency.cts index 240e40895..830805dc6 100644 --- a/src/health-diagnostic-rules/state-consistency.cts +++ b/src/health-diagnostic-rules/state-consistency.cts @@ -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; diff --git a/src/health-diagnostic-rules/worktree-health.cts b/src/health-diagnostic-rules/worktree-health.cts index 05665915b..d96e3d66c 100644 --- a/src/health-diagnostic-rules/worktree-health.cts +++ b/src/health-diagnostic-rules/worktree-health.cts @@ -64,7 +64,7 @@ import type planningSnapshotMod = require('../planning-snapshot.cjs'); type PlanningSnapshot = ReturnType; // 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; diff --git a/src/health-diagnostic-types.cts b/src/health-diagnostic-types.cts new file mode 100644 index 000000000..e54d90a4d --- /dev/null +++ b/src/health-diagnostic-types.cts @@ -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; + +// ─── 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; +} + +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; diff --git a/src/health-diagnostic.cts b/src/health-diagnostic.cts index aac125121..430d42b8d 100644 --- a/src/health-diagnostic.cts +++ b/src/health-diagnostic.cts @@ -30,68 +30,55 @@ import type planningSnapshotMod = require('./planning-snapshot.cjs'); type PlanningSnapshot = ReturnType; -// ─── 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; -} - -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 ──────────────────────────────────────────────────────────────