refactor(#3309): add health-diagnostic.cts skeleton (types + evaluator)

Phase 11 of epic #3180 (ADR-3180 §8.2/§8.3/§8.5). New src/health-diagnostic.cts:
SEVERITY/REMEDY_ACTION (7 members: 6 real repair actions + ADVISE)/REMEDY_RISK
(NONE/DESTRUCTIVE) frozen enums, Diagnostic/Remedy/Rule types, an empty RULES
table (rules land in the next commits), evaluateRules (with a duplicate-code
defense-in-depth check ahead of the lint guard), and applyRepairs (the
DESTRUCTIVE-risk-refusal dispatcher — §8.3 rule 3 — with stub handlers; real
repair bodies port in the migration step).

Six-gate .cts ripple: .gitignore, eslint.config.mjs, docs/INVENTORY.md +
manifest, CONTEXT.md glossary entry.
This commit is contained in:
sim
2026-08-13 00:51:12 -04:00
parent 6aa378b261
commit ef10bba707
7 changed files with 446 additions and 0 deletions

1
.gitignore vendored
View File

@@ -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.cjs
/gsd-core/bin/lib/command-roster.cjs
/gsd-core/bin/lib/runtime-artifact-conversion.cjs
/gsd-core/bin/lib/runtime-artifact-layout.cjs

View File

@@ -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 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`.
### Planning Workspace Module
Module owning `.planning` path resolution, active workstream pointer policy (`session-scoped > shared`), pointer self-heal behavior, and planning lock semantics for workstream-aware execution.

View File

@@ -378,6 +378,7 @@
"graphify.cjs",
"gsd2-import.cjs",
"handshake-serialized.cjs",
"health-diagnostic.cjs",
"hook-bus.cjs",
"host-integration-sdk.cjs",
"host-integration.cjs",

View File

@@ -493,6 +493,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.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 |
| `init-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools init` |

View File

@@ -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.cjs',
'gsd-core/bin/lib/shell-command-projection.cjs',
'gsd-core/bin/lib/security.cjs',
'gsd-core/bin/lib/command-aliases.cjs',

213
src/health-diagnostic.cts Normal file
View File

@@ -0,0 +1,213 @@
/**
* Health Diagnostic — frozen rule-table types, enums, and evaluator for
* `validate health` (Phase 11, #3309, ADR-3180 §8.2/§8.3/§8.5).
*
* SKELETON (this phase). Establishes the exact contract every later batch of
* extracted rules builds onto: the frozen `SEVERITY`/`REMEDY_ACTION`/
* `REMEDY_RISK` enums, the `Diagnostic`/`Remedy`/`Rule` shapes, the `RULES`
* container (starts EMPTY — a later migration step appends the 32 rules
* extracted from `cmdValidateHealth`, `src/verify.cts:1616-2577`), the
* `evaluateRules` evaluator, and the `applyRepairs` `--repair`/`--backfill`
* dispatcher. `applyRepairs`'s per-action handlers are stubs in this phase —
* they land alongside the rules that need them.
*
* `PlanningSnapshot` is deliberately NOT re-exported as a type from
* `planning-snapshot.cts` here (see the design doc's "Known limits" and this
* phase's brief): `ReturnType<typeof buildPlanningSnapshot>` is used inline
* instead, via a type-only `import ... = require(...)` that is fully erased
* at compile time — zero changes to the already-shipped, already-tested
* `planning-snapshot.cts`.
*
* 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.cts, compiled to
* gsd-core/bin/lib/health-diagnostic.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
}
// ─── 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[] = [];
// ─── Evaluator ──────────────────────────────────────────────────────────────
/**
* Evaluate an explicit `rules` array against `snapshot`, throwing if any two
* entries share a `code` (defense in depth beside the future static lint
* guard, §8.2 rule 1). Separated from `evaluateRules` so the duplicate-code
* guard is unit-testable against a small, locally-constructed fake rule
* array, independent of whether `RULES` itself has any entries yet (it does
* not, in this skeleton).
*/
function evaluateRuleTable(rules: Rule[], snapshot: PlanningSnapshot): Diagnostic[] {
const seen = new Set<string>();
for (const rule of rules) {
if (seen.has(rule.code)) {
throw new Error(`health-diagnostic: duplicate rule code "${rule.code}" in rule table`);
}
seen.add(rule.code);
}
return rules.flatMap((rule) => rule.check(snapshot));
}
/**
* Evaluate every rule in `RULES` against `snapshot`, flattening each rule's
* `Diagnostic[]` into one array.
*/
function evaluateRules(snapshot: PlanningSnapshot): Diagnostic[] {
return evaluateRuleTable(RULES, snapshot);
}
// ─── Repair dispatcher ──────────────────────────────────────────────────────
/**
* Stub repair handler. Real per-action handlers (`createConfig`,
* `resetConfig`, `regenerateState`, `addNyquistKey`,
* `addAiIntegrationPhaseKey`, `backfillMilestones`) land in a later
* migration batch alongside the rules that need them — see this phase's
* brief. Applying a NONE-risk remedy is a no-op beyond recording it, in this
* skeleton.
*/
function applyStubRepair(_cwd: string, _diagnostic: Diagnostic): void {
/* intentionally empty — real handlers land with the rules that need them */
}
/**
* `--repair`/`--backfill` dispatcher (design doc "`--repair` behavior
* change" section; §8.3 rule 3). For each diagnostic whose remedy is not
* `ADVISE`:
*
* - Not requested — `repair` is false, and for `backfillMilestones`
* specifically `backfill` is also false (mirrors `cmdValidateHealth`'s
* existing `backfillMilestones` gate, `verify.cts:2504`:
* `if (!options['backfill'] && !options['repair']) break;`) — skipped
* entirely, recorded in neither `applied` nor `refused`.
* - Requested and `remedy.risk === DESTRUCTIVE` — pushed onto `refused`,
* handler never invoked. This is the §8.3 rule 3 breaking-change
* enforcement point: a DESTRUCTIVE remedy is describable but is never
* applied by `--repair`.
* - Requested and `remedy.risk === NONE` — stub handler invoked, pushed
* onto `applied`.
*/
function applyRepairs(
cwd: string,
diagnostics: Diagnostic[],
repair: boolean,
backfill: boolean,
): { applied: string[]; refused: string[] } {
const applied: string[] = [];
const refused: string[] = [];
for (const diagnostic of diagnostics) {
const { remedy } = diagnostic;
if (remedy.action === REMEDY_ACTION.ADVISE) continue;
const requested =
remedy.action === REMEDY_ACTION.BACKFILL_MILESTONES ? repair || backfill : repair;
if (!requested) continue;
if (remedy.risk === REMEDY_RISK.DESTRUCTIVE) {
refused.push(diagnostic.code);
continue;
}
applyStubRepair(cwd, diagnostic);
applied.push(diagnostic.code);
}
return { applied, refused };
}
// ─── Exports ────────────────────────────────────────────────────────────────
const healthDiagnostic = {
SEVERITY,
REMEDY_ACTION,
REMEDY_RISK,
RULES,
evaluateRules,
// Additive beyond the phase's required-exports list — exposed so the
// duplicate-code guard (row 13) is directly unit-testable against a fake
// rule array without mutating the real, still-empty `RULES` export.
evaluateRuleTable,
applyRepairs,
};
// 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.cjs')` can reference the
// types as `x.Severity`, `x.RemedyAction`, etc.
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace healthDiagnostic {
export { Severity, RemedyAction, RemedyRisk, Remedy, Diagnostic, Rule };
}
export = healthDiagnostic;

View File

@@ -0,0 +1,226 @@
'use strict';
/**
* Tests for `src/health-diagnostic.cts` (Phase 11, #3309, ADR-3180 §8.2/§8.3/§8.5).
*
* 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
*
* This file covers ONLY the skeleton's own contract — test-matrix section 2,
* rows 9-14. `RULES` starts EMPTY in this phase (later batches append the 32
* extracted rules); rows 15-16 (the DESTRUCTIVE-refusal proof against REAL
* diagnostics emitted by real rules) and section 3 (per-rule fixtures) are
* deferred to the migration step that adds rules. This file DOES prove
* `applyRepairs`'s risk-gating logic directly against hand-constructed fake
* `Diagnostic` objects, independent of whether any real rule produces them
* yet — per this phase's brief.
*
* TDD RED: `src/health-diagnostic.cts` does not exist yet — this file's
* `require('../gsd-core/bin/lib/health-diagnostic.cjs')` throws
* MODULE_NOT_FOUND until this phase's implementation lands. That is the
* intended starting state.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const healthDiagnostic = require('../gsd-core/bin/lib/health-diagnostic.cjs');
const {
SEVERITY,
REMEDY_ACTION,
REMEDY_RISK,
RULES,
evaluateRules,
evaluateRuleTable,
applyRepairs,
} = healthDiagnostic;
// ─── Row 9 — REMEDY_ACTION locks exactly 7 members ─────────────────────────
describe('REMEDY_ACTION', () => {
test('row 9: locks exactly 7 members (6 real repair actions + ADVISE)', () => {
assert.deepEqual(Object.keys(REMEDY_ACTION).sort(), [
'ADD_AI_INTEGRATION_PHASE_KEY',
'ADD_NYQUIST_KEY',
'ADVISE',
'BACKFILL_MILESTONES',
'CREATE_CONFIG',
'REGENERATE_STATE',
'RESET_CONFIG',
]);
assert.deepEqual(
Object.values(REMEDY_ACTION).sort(),
[
'addAiIntegrationPhaseKey',
'addNyquistKey',
'advise',
'backfillMilestones',
'createConfig',
'regenerateState',
'resetConfig',
],
);
});
test('is frozen', () => {
assert.equal(Object.isFrozen(REMEDY_ACTION), true);
});
});
// ─── Row 10 — REMEDY_RISK locks exactly 2 members ──────────────────────────
describe('REMEDY_RISK', () => {
test('row 10: locks exactly 2 members (NONE, DESTRUCTIVE)', () => {
assert.deepEqual(Object.keys(REMEDY_RISK).sort(), ['DESTRUCTIVE', 'NONE']);
assert.deepEqual(Object.values(REMEDY_RISK).sort(), ['destructive', 'none']);
});
test('is frozen', () => {
assert.equal(Object.isFrozen(REMEDY_RISK), true);
});
});
describe('SEVERITY', () => {
test('locks exactly 3 members (ERROR, WARNING, INFO)', () => {
assert.deepEqual(Object.keys(SEVERITY).sort(), ['ERROR', 'INFO', 'WARNING']);
assert.deepEqual(Object.values(SEVERITY).sort(), ['error', 'info', 'warning']);
});
test('is frozen', () => {
assert.equal(Object.isFrozen(SEVERITY), true);
});
});
// ─── Rows 11-12 — applyRepairs risk-gating, hand-constructed diagnostics ───
//
// No real rule exists yet to emit these remedies (RULES is empty in this
// skeleton). These diagnostics are hand-built using the risk harvested from
// health.md's published table (design doc, "Risk assignment" section):
// resetConfig/regenerateState are DESTRUCTIVE; every other real action is
// NONE. This proves applyRepairs's gating logic is correct independent of
// whether any real rule exists to produce these shapes yet.
function fakeDiagnostic(code, action, risk) {
return {
code,
severity: SEVERITY.WARNING,
message: `fake diagnostic for ${code}`,
remedy: { action, risk, args: {} },
};
}
describe('applyRepairs — risk gating (hand-constructed diagnostics)', () => {
test('row 11: resetConfig/regenerateState (DESTRUCTIVE) are refused, never applied, when --repair is requested', () => {
const diagnostics = [
fakeDiagnostic('E005', REMEDY_ACTION.RESET_CONFIG, REMEDY_RISK.DESTRUCTIVE),
fakeDiagnostic('E004', REMEDY_ACTION.REGENERATE_STATE, REMEDY_RISK.DESTRUCTIVE),
];
const result = applyRepairs('/fake/cwd', diagnostics, true, false);
assert.deepEqual(result.applied, []);
assert.deepEqual(result.refused.sort(), ['E004', 'E005']);
});
test('row 12: every other real action (NONE risk) is applied, not refused, when --repair is requested', () => {
const diagnostics = [
fakeDiagnostic('W003', REMEDY_ACTION.CREATE_CONFIG, REMEDY_RISK.NONE),
fakeDiagnostic('W008', REMEDY_ACTION.ADD_NYQUIST_KEY, REMEDY_RISK.NONE),
fakeDiagnostic('W016', REMEDY_ACTION.ADD_AI_INTEGRATION_PHASE_KEY, REMEDY_RISK.NONE),
fakeDiagnostic('W018', REMEDY_ACTION.BACKFILL_MILESTONES, REMEDY_RISK.NONE),
];
const result = applyRepairs('/fake/cwd', diagnostics, true, false);
assert.deepEqual(result.applied.sort(), ['W003', 'W008', 'W016', 'W018']);
assert.deepEqual(result.refused, []);
});
test('ADVISE-action diagnostics are never applied nor refused, regardless of --repair', () => {
const diagnostics = [fakeDiagnostic('W001', REMEDY_ACTION.ADVISE, REMEDY_RISK.NONE)];
const result = applyRepairs('/fake/cwd', diagnostics, true, true);
assert.deepEqual(result.applied, []);
assert.deepEqual(result.refused, []);
});
test('non-backfillMilestones NONE-risk diagnostics are skipped (not applied) when --repair is not requested', () => {
const diagnostics = [fakeDiagnostic('W003', REMEDY_ACTION.CREATE_CONFIG, REMEDY_RISK.NONE)];
const result = applyRepairs('/fake/cwd', diagnostics, false, false);
assert.deepEqual(result.applied, []);
assert.deepEqual(result.refused, []);
});
test('DESTRUCTIVE-risk diagnostics are skipped (not refused) when --repair is not requested — refusal only fires when actually requested', () => {
const diagnostics = [fakeDiagnostic('E005', REMEDY_ACTION.RESET_CONFIG, REMEDY_RISK.DESTRUCTIVE)];
const result = applyRepairs('/fake/cwd', diagnostics, false, false);
assert.deepEqual(result.applied, []);
assert.deepEqual(result.refused, []);
});
test('backfillMilestones applies on --backfill alone, without --repair (mirrors verify.cts:2504 intent)', () => {
const diagnostics = [fakeDiagnostic('W018', REMEDY_ACTION.BACKFILL_MILESTONES, REMEDY_RISK.NONE)];
const result = applyRepairs('/fake/cwd', diagnostics, false, true);
assert.deepEqual(result.applied, ['W018']);
assert.deepEqual(result.refused, []);
});
test('backfillMilestones is skipped when neither --repair nor --backfill is set', () => {
const diagnostics = [fakeDiagnostic('W018', REMEDY_ACTION.BACKFILL_MILESTONES, REMEDY_RISK.NONE)];
const result = applyRepairs('/fake/cwd', diagnostics, false, false);
assert.deepEqual(result.applied, []);
assert.deepEqual(result.refused, []);
});
});
// ─── Row 13 — duplicate-code detection, LOCAL fake rule array ──────────────
//
// `RULES` is still empty in this skeleton, so the duplicate check cannot be
// exercised through the real exported table yet. Proven here instead against
// a small, locally-constructed fake rule array — per this phase's brief.
describe('evaluateRuleTable — duplicate-code guard (row 13)', () => {
test('throws when two rules share the same code', () => {
const fakeRules = [
{ code: 'W999', severity: SEVERITY.WARNING, check: () => [] },
{ code: 'W999', severity: SEVERITY.WARNING, check: () => [] },
];
assert.throws(() => evaluateRuleTable(fakeRules, {}), /W999/);
});
test('does not throw, and flattens all diagnostics, when codes are unique', () => {
const fakeRules = [
{
code: 'W997',
severity: SEVERITY.WARNING,
check: () => [
{ code: 'W997', severity: SEVERITY.WARNING, message: 'a', remedy: { action: REMEDY_ACTION.ADVISE, risk: REMEDY_RISK.NONE, args: {} } },
],
},
{
code: 'W998',
severity: SEVERITY.WARNING,
check: () => [
{ code: 'W998', severity: SEVERITY.WARNING, message: 'b', remedy: { action: REMEDY_ACTION.ADVISE, risk: REMEDY_RISK.NONE, args: {} } },
{ code: 'W998', severity: SEVERITY.WARNING, message: 'c', remedy: { action: REMEDY_ACTION.ADVISE, risk: REMEDY_RISK.NONE, args: {} } },
],
},
];
const diagnostics = evaluateRuleTable(fakeRules, {});
assert.equal(diagnostics.length, 3);
assert.deepEqual(diagnostics.map((d) => d.message), ['a', 'b', 'c']);
});
test('empty rule array never throws and returns []', () => {
assert.deepEqual(evaluateRuleTable([], {}), []);
});
});
// ─── Row 14 — evaluator against an all-clean (here: rule-less) snapshot ───
describe('evaluateRules (row 14)', () => {
test('RULES starts empty in this skeleton', () => {
assert.deepEqual(RULES, []);
assert.equal(Array.isArray(RULES), true);
});
test('returns [] against any snapshot, since RULES is empty', () => {
assert.deepEqual(evaluateRules({}), []);
});
});