Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changeset/lint-test-typecheck-wiring.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion packages/lint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
12 changes: 12 additions & 0 deletions packages/lint/test-typecheck-debt.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
87 changes: 87 additions & 0 deletions packages/lint/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -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"]
}
36 changes: 9 additions & 27 deletions scripts/check-type-check-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 '
Expand Down
20 changes: 20 additions & 0 deletions scripts/regen-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading