diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c6187fcb..b2517a028 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -252,11 +252,13 @@ jobs: run: cd server && npm run check:db-types - name: Lint - # Type-aware rules build the src + tests program, which outgrows - # Node's default ~4 GB heap on the runner. + # Errors fail, and the warnings per rule may not pass + # scripts/eslint-baseline.json (src, tests and eslint-disable'd + # messages counted apart). Type-aware rules build the src + tests + # program, which outgrows Node's default ~4 GB heap on the runner. env: NODE_OPTIONS: --max-old-space-size=8192 - run: cd server && npm run lint:check + run: cd server && npm run lint:warnings - name: DB call-graph gate (no sync DB-touching methods, no raw transactions) run: cd server && npm run db:call-graph -- --sync --tx diff --git a/CLAUDE.md b/CLAUDE.md index caf453e01..3ecc1fafd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,6 +39,7 @@ cd client && npm run lint:dup # lines in copied code may on cd client && npm run lint:pairs # desktop and phone views of a feature import its shared hook; every phone screen is listed (CI gate) cd client && npm run lint:size # no source file past 1000 lines, longer ones only shrink (CI gate; server has the same) cd client && npm run lint:warnings # eslint errors fail, warnings per rule may only shrink (CI gate) +cd server && npm run lint:warnings # the same for the server, against server/scripts/eslint-baseline.json (CI gate) cd client && npm run lint:format # Prettier outside the baseline list (CI gate) cd client && npm run lint:layers # components never import pages/ or mobile/ (CI gate) cd client && npm run lint:offline # views may not import src/api/ beyond the baseline (CI gate) @@ -123,7 +124,7 @@ These principles come out of a full-repo audit and shape all new code: ## Quality gates in CI -`.github/workflows/test.yml` runs, per package: typecheck (server also `typecheck:tests`), lint (client through `lint:warnings`, which also holds the warning count per rule), `lint:pages`, `lint:rtl`, `lint:size` (client and server), the server's `lint:boundaries`, `lint:tx`, `lint:dialect` and `lint:test-sql`, the client's `lint:format`, `lint:layers`, `lint:offline`, `lint:dup`, `lint:pairs`, `lint:skips` and `lint:i18n-keys`, shared's `contracts:open`, `check:plugin-facts`, `i18n:parity:strict` (with the untranslated-strings ratchet), the S3 contract suite, the Postgres probe (`postgres-probe`: every dialect helper on SQLite and Postgres, and the statements every repository method reaches with sample arguments against a Postgres service; the SQL errors per method and the list of methods it cannot measure are held to a baseline that only shrinks and fails while it is still unmeasured; see `server/CLAUDE.md`), the browser end-to-end flows (Chromium and WebKit), a client build with `check:gl-split`, coverage for all four packages, then a SonarCloud scan. It also builds the Docker image and boots it until `/api/health/ready` answers (`docker-image`), and lints and renders the Helm chart against the Kubernetes schemas and runs `docker compose config` on every compose file (`deploy-manifests`). `lint-prettier.yml` additionally runs `lint` + `format:check` on `shared/`. `plugin-sdk` has no lint script and is outside the eslint gates. **Not in CI**: `theme:lint` and the screenshot and help-media Playwright projects; run them locally. There are no git hooks; nothing runs before a commit except you. Other workflows build Docker images, scan them, and publish `trek-plugin-sdk` on `plugin-sdk-v*` tags. +`.github/workflows/test.yml` runs, per package: typecheck (server also `typecheck:tests`), lint (client and server through `lint:warnings`, which also holds the warning count per rule), `lint:pages`, `lint:rtl`, `lint:size` (client and server), the server's `lint:boundaries`, `lint:tx`, `lint:dialect` and `lint:test-sql`, the client's `lint:format`, `lint:layers`, `lint:offline`, `lint:dup`, `lint:pairs`, `lint:skips` and `lint:i18n-keys`, shared's `contracts:open`, `check:plugin-facts`, `i18n:parity:strict` (with the untranslated-strings ratchet), the S3 contract suite, the Postgres probe (`postgres-probe`: every dialect helper on SQLite and Postgres, and the statements every repository method reaches with sample arguments against a Postgres service; the SQL errors per method and the list of methods it cannot measure are held to a baseline that only shrinks and fails while it is still unmeasured; see `server/CLAUDE.md`), the browser end-to-end flows (Chromium and WebKit), a client build with `check:gl-split`, coverage for all four packages, then a SonarCloud scan. It also builds the Docker image and boots it until `/api/health/ready` answers (`docker-image`), and lints and renders the Helm chart against the Kubernetes schemas and runs `docker compose config` on every compose file (`deploy-manifests`). `lint-prettier.yml` additionally runs `lint` + `format:check` on `shared/`. `plugin-sdk` has no lint script and is outside the eslint gates. **Not in CI**: `theme:lint` and the screenshot and help-media Playwright projects; run them locally. There are no git hooks; nothing runs before a commit except you. Other workflows build Docker images, scan them, and publish `trek-plugin-sdk` on `plugin-sdk-v*` tags. - **`ci-ok` is the required check.** The branch ruleset requires `ci-ok` (plus `lint` and `check-target` from their own workflows), and `ci-ok` needs every job above except the scan. It fails if any of them failed or was cancelled and passes if they passed or were skipped. A new gate job counts only once it is listed in `ci-ok`'s `needs:`. - **Guides name paths that exist**: the `changes` job runs `scripts/ci/doc-paths.mjs`, which resolves every backtick path in the CLAUDE.md files, `README.md`, `server/src/nest/README.md` and the wiki against the tracked files; the missing ones are listed in `scripts/ci/doc-paths-baseline.json`, which only shrinks (`--update` after a fix). diff --git a/server/CLAUDE.md b/server/CLAUDE.md index f125ab8c6..19d26782a 100644 --- a/server/CLAUDE.md +++ b/server/CLAUDE.md @@ -9,8 +9,9 @@ npm run dev # scripts/dev.mjs: tsc -w then node --watch dist/index npm run build # scripts/build.mjs — emits even on type errors (see below) npm run typecheck # tsc --noEmit — the real type gate npm run typecheck:tests # tsc over tests/ — CI runs this too; vitest green does NOT mean typed mocks compile -npm run lint # eslint --fix -npm run lint:check # eslint, no fix (CI) +npm run lint # eslint, no fix +npm run lint:fix # eslint --fix +npm run lint:warnings # eslint errors fail, warnings per rule may only shrink (CI gate, scripts/eslint-baseline.json) npm run test # vitest run; also test:unit / test:integration / test:ws / test:e2e npm run test:coverage # istanbul coverage; per-domain ratchet over src/nest/** npm run gen:plugin-facts # regenerate the plugin-protocol tables into plugin-sdk/ + shared/ diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs index c31d68ded..f0e93632d 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -201,6 +201,13 @@ export default tseslint.config( 'no-empty': 'warn', 'no-useless-escape': 'warn', 'prefer-const': 'warn', + // Held per rule by lint:warnings (scripts/eslint-baseline.json), so their + // count may only fall: sequential awaits in a loop (N round trips where + // one batch would do), `!` assertions that strict null checks would have + // to prove instead, and console output that bypasses the audit logger. + 'no-await-in-loop': 'warn', + '@typescript-eslint/no-non-null-assertion': 'warn', + 'no-console': 'warn', }, }, { diff --git a/server/package.json b/server/package.json index f1414a7ab..35f8f7998 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,7 @@ "typecheck:scripts": "tsc -p tsconfig.scripts.json --noEmit", "db:call-graph": "node scripts/db-call-graph.mjs", "lint:size": "node scripts/size-lint.mjs", + "lint:warnings": "node scripts/eslint-ratchet.mjs", "lint:boundaries": "node scripts/import-boundaries.mjs", "lint:dialect": "node scripts/dialect-lint.mjs", "lint:tx": "node scripts/tx-writes.mjs", @@ -30,7 +31,8 @@ "probe:pg": "node --import tsx scripts/pg-probe.ts", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "lint:check": "eslint .", "test": "vitest run", "test:watch": "vitest", diff --git a/server/scripts/eslint-baseline.json b/server/scripts/eslint-baseline.json new file mode 100644 index 000000000..e5208fa84 --- /dev/null +++ b/server/scripts/eslint-baseline.json @@ -0,0 +1,30 @@ +{ + "src": { + "@typescript-eslint/no-explicit-any": 93, + "@typescript-eslint/no-non-null-assertion": 297, + "@typescript-eslint/no-require-imports": 18, + "@typescript-eslint/no-unsafe-function-type": 1, + "@typescript-eslint/no-unused-vars": 43, + "no-await-in-loop": 566, + "no-console": 237, + "no-empty": 6 + }, + "suppressed": { + "@typescript-eslint/no-explicit-any": 128, + "@typescript-eslint/no-require-imports": 20, + "no-control-regex": 7, + "no-misleading-character-class": 1 + }, + "tests": { + "@typescript-eslint/no-explicit-any": 1571, + "@typescript-eslint/no-non-null-assertion": 1757, + "@typescript-eslint/no-require-imports": 25, + "@typescript-eslint/no-unsafe-function-type": 7, + "@typescript-eslint/no-unused-vars": 101, + "no-await-in-loop": 459, + "no-console": 18, + "no-empty": 2, + "no-useless-escape": 1, + "prefer-const": 3 + } +} diff --git a/server/scripts/eslint-ratchet.mjs b/server/scripts/eslint-ratchet.mjs new file mode 100644 index 000000000..0542fc28e --- /dev/null +++ b/server/scripts/eslint-ratchet.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * lint:warnings: ESLint errors fail, warnings may only go down. + * + * Several rules in eslint.config.mjs are warnings (no-explicit-any, + * no-unused-vars, no-require-imports, no-await-in-loop, no-non-null-assertion, + * no-console and a few js.recommended ones), set that way because the code + * had hits when they came in, and a warning never fails a build. So this runs + * ESLint over the server with its own config, as `eslint .` does, and counts + * the warnings per rule, separately for src/ and tests/, plus the messages an + * eslint-disable comment silenced as a third group. Each count may not pass + * its entry in scripts/eslint-baseline.json, and an entry above its count + * fails as well until --update lowers it, so a fixed warning cannot come back + * unseen. Any error fails, so the check stands in for `npm run lint:check`, + * and so does a block comment that configures a rule inline: it leaves no + * message to count, so it would otherwise be the free way past the ratchet. + * + * npm run lint:warnings check against the baseline (CI) + * npm run lint:warnings -- --update lower the baseline to today's counts; + * it never raises an entry + * + * The tally lives in scripts/lib/eslint-warnings.mjs at the repository root; + * ESLint comes from this workspace, so the server lints with the major its + * config was written for. + */ +import { ESLint } from 'eslint'; +import { fileURLToPath } from 'node:url'; +import { check, lintWith } from '../../scripts/lib/eslint-warnings.mjs'; +import { runCli } from '../../scripts/lib/ratchet.mjs'; + +const root = fileURLToPath(new URL('..', import.meta.url)); +await runCli('lint:warnings', (args) => check({ root, update: args.includes('--update'), lint: lintWith(ESLint) })); diff --git a/wiki/Development-environment.md b/wiki/Development-environment.md index 5a9788411..d2f3894b9 100644 --- a/wiki/Development-environment.md +++ b/wiki/Development-environment.md @@ -171,7 +171,8 @@ The `@trek/shared` package is the single source of truth for code shared between | `npm run test:watch` | Run tests in watch mode | | `npm run test:coverage` | Run tests with coverage report | | `npm run lint` | Lint source | -| `npm run lint:check` | Lint everything, no `--fix` (CI gate) | +| `npm run lint:check` | Lint everything, no `--fix` | +| `npm run lint:warnings` | ESLint, failing on any error and on warnings above the per-rule baseline (CI gate, replaces `lint:check`) | | `npm run check:plugin-facts` | Verify generated plugin facts (CI gate) | | `npm run lint:size` | No source file grows past its line limit or its baseline entry (CI gate) | | `npm run lint:boundaries` | Import cycles and domain boundaries may not grow past their baseline (CI gate) |