From 14410d78fb40b56f007e8bf4274084209853bdad Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 08:20:02 +0000 Subject: [PATCH] fix(lint): wire test layer into check:test-typecheck, so its 2,700-line rule pin file is type-checked (#14173) Onboarded by wiring to the mechanism #14062 (PR #14420) landed on, per the triage ruling on this card (comment 5502888897): a sibling tsconfig.test.json matching vitest's real module semantics, named by `typecheck`. Measured: 6 residual TS6059 errors over 2 files (rootDir violations from imports outside this package, pre-existing, config-tier), recorded in the new test-typecheck-debt.json as a CONVERSION of the coverage gate's existing 16-error TEST_DEBT entry, which is deleted as the graduation that pairing forces. Co-Authored-By: Claude Code Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV --- .changeset/lint-test-typecheck-wiring.md | 32 +++++++++ packages/lint/package.json | 4 +- packages/lint/test-typecheck-debt.json | 12 ++++ packages/lint/tsconfig.test.json | 87 ++++++++++++++++++++++++ scripts/check-type-check-coverage.mjs | 36 +++------- scripts/regen-artifacts.mjs | 20 ++++++ 6 files changed, 163 insertions(+), 28 deletions(-) create mode 100644 .changeset/lint-test-typecheck-wiring.md create mode 100644 packages/lint/test-typecheck-debt.json create mode 100644 packages/lint/tsconfig.test.json diff --git a/.changeset/lint-test-typecheck-wiring.md b/.changeset/lint-test-typecheck-wiring.md new file mode 100644 index 0000000000..971c3e6ada --- /dev/null +++ b/.changeset/lint-test-typecheck-wiring.md @@ -0,0 +1,32 @@ +--- +"@objectstack/lint": patch +--- + +fix(lint): wire `packages/lint`'s test layer into `check:test-typecheck`, so its 2,700-line rule pin file is actually type-checked (#14173) + +`packages/lint/tsconfig.json` excluded `**/*.test.ts` and `**/*.spec.ts`, and +the package's `typecheck` script was a bare `tsc --noEmit` against that very +config — so no gate anywhere read a lint test file with a type checker. +`src/validate-expressions.test.ts` alone is ~2,700 lines built almost entirely +out of compile-time and meta pins (the #5017 receiver scan, the +`TRACKED_UNDECLARED_READS` shrink-only list, the residual-root table), and none +of it was type-checked by anything: vitest transpiles through esbuild (types +stripped, never resolved), so a wrong key or a signature drift in a pin's own +scaffolding was caught by nobody. + +Onboarded by *wiring* to the mechanism #14062 (PR #14420) landed on +(`scripts/check-test-typecheck.mts`), per the triage ruling on this card: a +sibling `tsconfig.test.json` matching vitest's real module semantics +(`module: esnext`, `moduleResolution: bundler`, `lib: ["ES2022"]`; strictness +and `rootDir` untouched, inherited), named by `typecheck`. Measured (workspace +closure built first): 6 residual errors over 2 files, all TS6059 (imports from +`examples/app-showcase`, outside this package's `rootDir` — pre-existing, +config-tier, not a lint defect), recorded EXACT and shrink-only in the new +`test-typecheck-debt.json`. + +This is a CONVERSION of the coverage gate's existing `@objectstack/lint` +TEST_DEBT entry (`errors: 16`), not a new debt-opening decision: the same +authority that recorded the 16 now holds the residue one level finer, per file +and per signature, and the coverage-gate entry is deleted as the graduation +that pairing forces. No test file is edited — opening the ratchet is not the +same job as paying it down. diff --git a/packages/lint/package.json b/packages/lint/package.json index 45f6b96563..d0c9be5bb0 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -32,7 +32,9 @@ "build": "tsup && node ../../scripts/check-dts-emitted.mjs", "dev": "tsc -w", "test": "vitest run", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && pnpm check:test-typecheck", + "check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/lint --project tsconfig.test.json", + "gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/lint --project tsconfig.test.json", "check:doc-formula-expressions": "node scripts/check-doc-formula-expressions.mjs --self-test && node scripts/check-doc-formula-expressions.mjs", "check:doc-security-posture": "node scripts/check-doc-security-posture.mjs --self-test && node scripts/check-doc-security-posture.mjs" }, diff --git a/packages/lint/test-typecheck-debt.json b/packages/lint/test-typecheck-debt.json new file mode 100644 index 0000000000..c9f9c02cc8 --- /dev/null +++ b/packages/lint/test-typecheck-debt.json @@ -0,0 +1,12 @@ +{ + "_comment": "Per-file tsc error debt of the @objectstack/lint TEST layer (#5286). `tsconfig.test.json` compiles `src/**/*.test.ts` — which `tsconfig.json` excludes and therefore no gate ever read — and every file below still carries errors from before that gate existed. THIS FIELD IS GENERATED: every regeneration rewrites it from scripts/check-test-typecheck.mts, and the EXACT ratchet below requires a regeneration on every repair — so an edit made here is gone by the next one. Anything true of THIS package goes in the sibling `_note` field, which is authored, is preserved verbatim, and is never written by the generator (#12624). This comment states NO cause for the errors, deliberately: the classes differ per package and per file, they move as the debt is paid down, and a cause written here is rewritten verbatim into every ledger by every regeneration — so it outlives its own repair and cannot be corrected in the file where it is read. Measure instead, before repairing anything: `tsc --noEmit --pretty false -p tsconfig.test.json` in the package prints the real classes with their TS codes. Each entry maps a file to its per-SIGNATURE error counts, never to a bare total (#13470): a signature is the TS code plus the diagnostic message with structural type blobs collapsed, and it carries NO line or column — so the pin survives edits that move code around, and only stops matching when the error itself becomes a different error. EXACT ratchet, judged by re-running tsc: a file that gains errors is red, a file that loses them is red until its number is re-recorded, a file that reaches zero is red until its entry is deleted, a signature that ARRIVES or VANISHES is red even when the file total is unchanged, and a file NOT listed here may have no errors at all. Regenerate with: pnpm --filter @objectstack/lint gen:test-typecheck-debt", + "_note": "STARTING LEDGER, opened by #14173 under the triage ruling of 2026-09-02 (comment 5502888897 on that issue: \"onboard by wiring to whichever of the two in-repo routes #14062 lands on\"), which carries the #5286 maintainer authority for it. This is a CONVERSION, not a new debt-opening decision: the coverage gate's own `@objectstack/lint` TEST_DEBT entry (`scripts/check-type-check-coverage.mjs`, errors: 16 — TS7006 x11, TS2835 x5) already recorded this package's untyped test layer at the per-package approximation, and the 16 is the authority this ledger converts. Measured under THIS config's module semantics (`esnext`/`bundler`): the TS7006 x11 and TS2835 x5 that made up the 16 both dissolve — the same config-tier effect `packages/spec`/`packages/rest`/`packages/plugins/**` measured, here from the `.js`-extension relative-import errors resolving cleanly under `bundler` and the downstream implicit-any callbacks resolving with them. What remains is 6 TS6059 (rootDir violation) errors over 2 files, NOT part of the original 16 and explicitly EXCLUDED from it by that entry's own historical note (\"they were never this package's debt ... the generated re-measure project reporting on its own inherited rootDir\"): both files import example-app fixtures from `examples/app-showcase`, outside this package's `rootDir`, which `tsconfig.test.json` inherits untouched. Measured 6 <= 16, so the conversion stays within the triage's authority boundary. Under BUILD semantics (NodeNext, tests included, workspace closure built first) the same two files plus three ledgered-away test files measure 22 errors total (11 TS7006 + 5 TS2835 + 6 TS6059); only the 6 TS6059 survive THIS config, confirming the other 16 were config-tier. Opening the ratchet is not paying it down; paydown follows this ledger's own shrink-only discipline, on its own cards.", + "entries": { + "src/validate-jsx-pages.production-witness.test.ts": { + "TS6059: File '…' is not under 'rootDir' '…'. 'rootDir' is expected to contain all source files.": 3 + }, + "src/validate-translatable-sections.test.ts": { + "TS6059: File '…' is not under 'rootDir' '…'. 'rootDir' is expected to contain all source files.": 3 + } + } +} diff --git a/packages/lint/tsconfig.test.json b/packages/lint/tsconfig.test.json new file mode 100644 index 0000000000..9102f08836 --- /dev/null +++ b/packages/lint/tsconfig.test.json @@ -0,0 +1,87 @@ +// The TEST-layer type-check program (issue #14173 — wired per the triage ruling on +// that card, comment 5502888897, 2026-09-02: "onboard by *wiring* to whichever of +// the two in-repo routes #14062 lands on"). #14062 landed in PR #14420 on the +// `scripts/check-test-typecheck.mts` instrument, so that is the route this file +// follows. `tsconfig.json` beside this one stays exactly as it is: it is the BUILD +// config. This sibling puts the test layer in front of tsc under the module +// semantics vitest really executes it with, and `package.json`'s `typecheck` +// script NAMES it (via `check:test-typecheck --project`), because a config no +// script invokes is exactly the phantom this whole change is about. +// +// This is a CONVERSION, not a new debt-opening decision: the coverage gate +// already carried a `@objectstack/lint` TEST_DEBT entry (`scripts/check-type-check- +// coverage.mjs`, `errors: 16`) recording this package's untyped test layer at the +// per-PACKAGE approximation. That entry's own authority is what this file converts +// to per-file/per-signature form — exactly as PR #14420's graduation docblock +// describes for the plugin family ("None of them was PAID DOWN … the same errors +// are now held one level finer"). +// +// What differs from the build config, and what deliberately does NOT: +// - MODULE SEMANTICS ONLY, plus `lib`. The tests are written and executed as +// ESM by vitest (esbuild/vite). Matching that is FIDELITY, not laxity: it is +// the same subtraction `packages/spec`, `packages/rest` and the `packages/ +// plugins/**` family each made, and it removes a config-tier error pile that +// says nothing about the tests. +// - ⛔ STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`, `noUnusedParameters`, +// `noImplicitReturns`, `noFallthroughCasesInSwitch`, `rootDir`, `paths` and +// `types` (this package's `types: ["node"]` included) are all INHERITED from +// `tsconfig.json` (and through it the root config), and none of them is +// re-declared here. ⚠️ A child that declared its own `paths` would REPLACE the +// parent map rather than merge into it, silently sending a source-resolved +// specifier back to `dist/` — a BUILD ARTIFACT — so this file declares none. +// Nothing here may loosen a type rule; if a test does not compile, that is the +// finding. +// - `lib: ["ES2022"]`, for the same reason `packages/rest` and `plugin-auth` +// state: the root config's `lib` is ES2020 and vitest runs on a Node that has +// ES2022 builtins, so the gap is reported as TS2550 about the CHECK. No `DOM`: +// nothing in this layer touches a browser global. +// +// MEASURED at 6aea1f559, workspace closure built first (`tsc --noEmit --pretty +// false --listFiles -p tsconfig.test.json`, and the same command without +// `--listFiles`): +// +// files in this program 627 +// own `src/**/*.test.ts` in it 93 +// errors under BUILD semantics 22 +// errors under THIS config 6 +// +// The two readings DISAGREE by exactly the recorded coverage-gate debt this +// change converts: under BUILD semantics (NodeNext, tests included, no other +// change) the total is 11 TS7006 + 5 TS2835 + 6 TS6059 = 22. The TS7006 and +// TS2835 — 16 in all, the coverage gate's whole recorded `@objectstack/lint` +// TEST_DEBT entry — dissolve under `bundler` resolution: TS2835 is "relative +// import missing `.js`", which `bundler` does not require, and the TS7006s are +// downstream implicit-`any` parameters whose callback types resolve once the +// import does. Config tier, not the tests, same as `packages/spec` measured. +// The 6 TS6059 that remain are `rootDir` violations from two test files +// importing example-app fixtures (`examples/app-showcase`) outside this +// package's `rootDir`, which this config inherits untouched (see above) — real, +// pre-existing, and explicitly NOT part of the original 16 by that entry's own +// prior note ("never this package's debt ... reporting on its own inherited +// rootDir"). +// +// BEFORE THIS FILE: the build config's `exclude` NAMED `**/*.test.ts` and +// `**/*.spec.ts`, so none of the test files here reached any tsc program the +// `typecheck` script runs (#14173). +// +// The residual error(s), if any, are PRE-EXISTING — this change edits no test +// file, and every one of them would have been reported on `origin/main` had this +// program always existed. They are ledgered per file in `test-typecheck-debt.json` +// beside this config, EXACT and shrink-only: a file that gains an error is red, +// one that loses an error is red until re-recorded, one that reaches zero is red +// until its entry is deleted, a signature that ARRIVES or VANISHES is red even at +// an unchanged file total, and a file NOT listed there may have no errors at all. +// ⛔ The ruling is explicit that this PR does not REPAIR them — it makes the +// ratchet able to see them; paydown follows the ratchet's own shrink-only +// discipline, on its own cards. +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["ES2022"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 30342a2941..8e4d4c6d5e 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -928,6 +928,15 @@ const EXEMPT = { // "the repair is the same one spec took -- put the file in a tsc program", and // the entry goes when the program exists, not when the number reaches zero. // +// `@objectstack/lint` (16) left this ledger under the same shape on 2026-09-02 +// (#14173, wired to this instrument per the triage ruling on that card): the +// recorded 16 (TS7006 x11, TS2835 x5) was entirely a config-tier pile -- it +// dissolves under `tsconfig.test.json`'s `bundler` resolution, the same effect +// measured above -- and the 6 residual TS6059 that replace it in +// `test-typecheck-debt.json` were never part of the 16 at all; this entry's own +// prior note already excluded them ("never this package's debt ... reporting on +// its own inherited rootDir"). +// // So the shrink-only guarantee did not loosen here; it moved to a strictly // sharper instrument, one that also reddens on a wholesale substitution of // error IDENTITY at a constant total, which a per-package integer cannot see. @@ -1036,33 +1045,6 @@ const TEST_DEBT = { + 'ratchet. Re-measured 10 at 2bc187641, and the pristine tree at that commit reports the same 10, ' + "so none of the -33 is this PR's doing.", }, - '@objectstack/lint': { - errors: 16, - note: 'TS7006 x11, TS2835 x5, re-tallied from tsc at the 16 below -- not the older ' - + 'composition rescaled. Per file: src/validate-semantic-roles.test.ts x5, ' - + 'src/validate-dashboard-action-refs.test.ts x4, ' - + 'src/validate-filter-tokens.test.ts x3, src/validate-capability-references.test.ts x3, ' - + 'src/validate-managed-api-methods.test.ts x1. The 5 TS2835 are one per file and all the same ' - + "shape -- the test's own relative import of the module under test, missing its `.js`. " - + 'LOWERED 19 -> 16 in #10779, re-tallied rather than declared stale because the delta is exactly ' - + 'attributable: the 3 that left are the TS6059 this itemisation used to list, all of them in ' - + 'validate-translatable-sections.test.ts, which imports contact.object.ts, contact.view.ts and ' - + 'system/translations/index.ts from examples/app-showcase -- outside this package entirely. They ' - + 'were never this package\'s debt; they were the generated re-measure project reporting on its ' - + 'own inherited `rootDir`, and no author here could have retired them by fixing lint. That file ' - + 'held exactly those 3 and so leaves the per-file list altogether. ' - + 'Measured 26 -> 30 (5ab08428, the +4 being TS6059, a file outside rootDir, a class the pre-#5278 ' - + 'note did not list) -> 32 (e8db1a230), and RECORDED 42 was a bootstrap margin (+10 over that 32). ' - + 'THE MARGIN IS GONE, and has been since #7888 / PR #8225 lowered 42 -> 20 against a measured 20 at ' - + 'b5e09b21 -- that PR deliberately left this note describing the larger pile, because inventing a ' - + 'composition for errors that are gone is the one thing this ledger forbids, so the tally above is ' - + 'the first one taken at the size the entry actually is. Lowered 20 -> 19 at 585edf738 (#8728). ' - + 'The -1 is attributed: #8515 / PR #8610 moved the translation-section-name-missing pins onto the ' - + 'frozen src/showcase-shape.fixtures.ts snapshot and dropped the live `TaskViews` import, which is ' - + 'the TS6059 that left -- the surviving three name exactly the three example files those tests ' - + 'still import. One older claim is now false and is corrected rather than carried: ' - + 'src/validate-visibility-predicates.test.ts held 10 of the 32 and reports none today.', - }, '@objectstack/formula': { errors: 17, note: 'TS2591 x6 (`process`), TS2345 x3, TS2352 x3, TS1470 x2, TS2339 x2, TS2739 x1. Re-measured 17 ' diff --git a/scripts/regen-artifacts.mjs b/scripts/regen-artifacts.mjs index e15667d1b0..81c604c9ad 100644 --- a/scripts/regen-artifacts.mjs +++ b/scripts/regen-artifacts.mjs @@ -522,6 +522,26 @@ export const NOT_DRIVER_MANAGED = Object.freeze([ + 'above; same generator, same per-package ledger, same reason a merge must never recompute ' + 'it.', }, + // ── #14173: `packages/lint`'s starting ledger, wired to the same instrument ── + // + // Onboarded by wiring per the triage ruling on #14173 (comment 5502888897, + // 2026-09-02: "onboard by wiring to whichever of the two in-repo routes #14062 + // lands on"), which landed on `check:test-typecheck`. This is the fifth row + // this manifest adds for the mechanism — `reconcileGenerators`'s population is + // the manifest SCRIPTS, so a package that defines a generator needs a row and + // one that measures zero needs none, same as the plugin family above. + { + path: 'packages/lint/test-typecheck-debt.json', + gen: 'gen:test-typecheck-debt', + owner: '@objectstack/lint', + why: + 'a SHRINK-ONLY ratchet — see `packages/plugins/plugin-approvals/test-typecheck-debt.json` ' + + 'above; same generator, same per-package ledger, same reason a merge must never recompute ' + + 'it: the half-merged tree is not the tree whose type errors this file records, so a file ' + + 'that GAINED errors would enter the ledger as merge noise instead of as red. Its residue is ' + + 'only 6 errors over 2 files, which makes it MORE exposed to the failure this row prevents ' + + 'rather than less.', + }, { path: 'packages/sdui-parser/objectui-lockstep.json', gen: 'gen:sdui-lockstep',