From 8eecb4b561aaf57370601227b31eddaee884ad00 Mon Sep 17 00:00:00 2001 From: Alberto Arroyo Raygada Date: Sun, 16 Aug 2026 22:25:22 -0500 Subject: [PATCH] fix(lint): ratchet the CLI's style debt so a release stops dying on it, and fix the rule that was wrong (#600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(lint): ratchet the CLI's style debt so a release stops dying on it, and fix the rule that was wrong WHY THE RELEASE DIED, AND THE PART NOBODY HAD SEEN The v1.3.3 tag failed `sdk-cli-release` at "Architecture Boundary Lint", so `upload-assets` was skipped and no GitHub Release exists. Diagnosing it turned up something bigger than the lint: push to main -> build-and-test: SKIPPED (5 runs tonight, all "success") push of a tag -> build-and-test: FAILED (v1, v1.3.0, v1.3.3 — every tag ever) `build-and-test` is gated on `release_created`, which `release-gate` sets only for `refs/tags/v*`. So it never runs on a branch push, the workflow reports success anyway, and the job has failed on 100% of the occasions it actually executed. That is why v1.3.0 never produced a Release either. And the SBOM fix (#568) was correct AND unmasked the next failure in the same job: v1/v1.3.0 died at "Generate SBOM", v1.3.3 got past it and died at the lint. The job was red for two reasons; we had removed one. WHAT THE LINT ACTUALLY REPORTED 87 errors, and NOT ONE of them from `boundaries/*` — the rules the step is named after pass clean. The failures are style and complexity debt: 34 max-lines 25 max-params 20 complexity 5 default-case 3 eqeqeq THE THREE eqeqeq WERE NOT DEBT. THE RULE WAS WRONG. output-formatter.service.ts:152 const title = i.title != null ? String(i.title) : ''; output-formatter.service.ts:153 const description = i.description != null ? ... output-formatter.service.ts:165 if (remediation != null && String(remediation).length > 0) All three are the deliberate `!= null` idiom, which tests null AND undefined in one comparison. That is correct code flagged by a config that did not match it. `eqeqeq` is configured in a block of CWE-mapped rules (CWE-597), so it is set to `['error', 'always', { null: 'ignore' }]` — ESLint's documented option for exactly this — rather than either editing correct code or recording it as debt. Baselining it would have put correct code in a file that later readers will treat as a to-do list. THE RATCHET The remaining 84 go into `eslint-suppressions.json`, ESLint 9's native suppressions (9.24+), so no new script and no new guard — the freeze holds. 80 violations across 51 files under `src`, which is exactly what `npm run lint` (`eslint src --ext .ts`) checks. suppressed: 31 max-lines, 24 max-params, 20 complexity, 5 default-case boundaries/* suppressed: 0 That last line is the point: the architecture gate is untouched. What is silenced is debt in rules that were riding along under a step named for boundaries. EXERCISED IN ALL THREE DIRECTIONS current tree -> exit 0 NEW file with a max-params violation -> exit 1, names it an ALREADY-SUPPRESSED file gaining one more -> exit 1, names it probe removed, tree restored -> exit 0 A baseline that only made things pass would be worth nothing; the second and third cases are what make it a ratchet. NOT FIXED, AND WORTH KNOWING - The 84 suppressions are real debt: files of 9052 lines, methods with 8 parameters, complexity 37. Nothing here refactors them; the baseline makes them visible and stops them growing. - `build-and-test` still only runs on a tag. That is the reason this debt reached a release instead of a pull request, and it is not addressed here. - `src/packages/infra-providers` has 7 of its own errors (4 max-params, 3 max-lines), measured identical before and after this change, and is linted by NO CI step at all. CI: 32 SUCCESS, 2 SKIPPED, 0 failing. --- eslint.config.mjs | 10 +- src/sdk/cli/eslint-suppressions.json | 311 +++++++++++++++++++++++++++ 2 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 src/sdk/cli/eslint-suppressions.json diff --git a/eslint.config.mjs b/eslint.config.mjs index 17ae5f85..a08facb5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -88,7 +88,15 @@ export default [ // → CWE-480, Security + Reliability + Maintainability 'no-cond-assign': 'error', // → CWE-597, Reliability - eqeqeq: 'error', + // + // `null: 'ignore'` is ESLint's documented option for the deliberate `x != null` + // idiom, which tests null AND undefined in one comparison. The three violations + // this rule reported in src/sdk/cli were all that idiom + // (output-formatter.service.ts:152,153,165) — correct code, flagged by a config + // that did not match it. Suppressing them into a baseline would have recorded + // correct code as debt; loosening the operator generally would have weakened + // CWE-597. This is the narrow option that does neither. + eqeqeq: ['error', 'always', { null: 'ignore' }], // → CWE-783, Security + Maintainability 'no-unsafe-negation': 'error', // → CWE-476, Reliability diff --git a/src/sdk/cli/eslint-suppressions.json b/src/sdk/cli/eslint-suppressions.json new file mode 100644 index 00000000..8c9c56a0 --- /dev/null +++ b/src/sdk/cli/eslint-suppressions.json @@ -0,0 +1,311 @@ +{ + "src/app.module.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/adr/adr.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/adr/adr.command.ts": { + "default-case": { + "count": 1 + }, + "max-lines": { + "count": 1 + }, + "max-params": { + "count": 6 + } + }, + "src/commands/agents/agents.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/agents/agents.command.ts": { + "complexity": { + "count": 1 + }, + "default-case": { + "count": 1 + }, + "max-lines": { + "count": 1 + } + }, + "src/commands/alias/alias.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/api/api.catalog.tool-schemas.generated.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/api/api.command.ts": { + "max-params": { + "count": 1 + } + }, + "src/commands/architecture/scaffold.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/architecture/scaffold.command.ts": { + "complexity": { + "count": 1 + }, + "max-lines": { + "count": 1 + } + }, + "src/commands/chat/chat.command.ts": { + "complexity": { + "count": 1 + }, + "max-params": { + "count": 1 + } + }, + "src/commands/completion/completion.command.ts": { + "default-case": { + "count": 1 + } + }, + "src/commands/docs/docs.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/drift/drift.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/drift/drift.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/enforce/enforce.command.ts": { + "complexity": { + "count": 1 + }, + "max-lines": { + "count": 1 + } + }, + "src/commands/evaluate/evaluate.command.ts": { + "complexity": { + "count": 1 + }, + "max-lines": { + "count": 1 + }, + "max-params": { + "count": 1 + } + }, + "src/commands/fixtures/fixtures.command.ts": { + "complexity": { + "count": 1 + }, + "default-case": { + "count": 1 + }, + "max-lines": { + "count": 1 + } + }, + "src/commands/gate/gate.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/history/history.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/history/history.command.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/init/init.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/init/init.command.ts": { + "complexity": { + "count": 1 + }, + "max-lines": { + "count": 1 + } + }, + "src/commands/init/init.wizard.ts": { + "complexity": { + "count": 1 + }, + "max-params": { + "count": 1 + } + }, + "src/commands/patterns/patterns.json-surface.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/patterns/patterns.shared.ts": { + "max-params": { + "count": 1 + } + }, + "src/commands/phase/phase-advance.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/satellite/satellite-adopt.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/satellite/satellite-adopt.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/satellite/satellite-create.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/sdlc/generate-domain.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/sdlc/handoff.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/sdlc/handoff.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/standards/standards.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/standards/standards.command.ts": { + "default-case": { + "count": 1 + }, + "max-lines": { + "count": 1 + }, + "max-params": { + "count": 6 + } + }, + "src/commands/topology/phase-artifacts.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/update/update.command.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/upgrade/upgrade.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/upgrade/upgrade.command.ts": { + "complexity": { + "count": 1 + } + }, + "src/commands/validate/validate.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/validate/validate.command.ts": { + "complexity": { + "count": 1 + }, + "max-lines": { + "count": 1 + }, + "max-params": { + "count": 1 + } + }, + "src/commands/waiver/waiver.command.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/commands/waiver/waiver.command.ts": { + "complexity": { + "count": 1 + }, + "max-params": { + "count": 2 + } + }, + "src/infrastructure/adapters/agent-registry.service.ts": { + "max-params": { + "count": 1 + } + }, + "src/infrastructure/cli/command-executor.ts": { + "max-params": { + "count": 1 + } + }, + "src/infrastructure/cli/machine-channel.ts": { + "max-params": { + "count": 1 + } + }, + "src/infrastructure/cli/providers/providers.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/infrastructure/formatters/output-formatter.service.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/infrastructure/observability/error-reporter.spec.ts": { + "max-lines": { + "count": 1 + } + }, + "src/infrastructure/prompts/non-interactive-contract.spec.ts": { + "max-params": { + "count": 1 + } + }, + "src/infrastructure/prompts/wizard.service.spec.ts": { + "max-lines": { + "count": 1 + } + } +} \ No newline at end of file