From 27ecaa29ec113ac0a183d8856942f30427fa96bc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 09:38:21 +0000 Subject: [PATCH 1/2] feat(tooling): @objectstack/core declares a typecheck script; test and examples layers enter the ratchet (#14613) `packages/core/package.json` declared exactly `build`, `test`, `test:watch`. `turbo run typecheck` selects only packages that declare the task, so the lint workflow's typecheck job could not reach this package and `pnpm --filter @objectstack/core typecheck` failed with ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT for anyone who tried it. Measured at 84b8190ae, dependency closure built first: - `tsc --noEmit -p tsconfig.json` (tests included, as the DEBT entry measured it): 98 errors over 12 files, all 12 of them `.test.ts` - the same program over only the 63 non-test source files: ZERO - the same 48 test files under vitest's module semantics: 4 So 94 of the 98 were the CHECK, not the code, and NO TEST FILE IS EDITED here. `tsconfig.json` keeps the build layer and excludes tests; a new `tsconfig.test.json` compiles them under `module: esnext` / `moduleResolution: bundler`, retiring 22 x TS2835, the TS2347 beside them and the TS7006 cascade they cause. Strictness is inherited and untouched. The residue of 4 is held per file and per signature in `test-typecheck-debt.json`, EXACT and shrink-only. Declaring the script flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, so `check:type-check-coverage`'s SOURCES_COVERED invariant then reported `packages/core/examples` -- 2 non-test source files in NO tsc program. Neither had ever compiled (`../index.js` resolves above the package root; `@objectstack/core` is this package self-referencing by a name it declares in no dependency block). Collapsing that cascade EXPOSED errors, 12 -> 29: 20 reads of ObjectKernel's private `logger`, four scan-result members that do not exist, and two config literals passing unparsed shapes where the methods are declared over the `Parsed` ones -- drift from two retirements that edited this file while no tsc program could check it. Every correction is pinned to this package's own signatures; `packages/spec` was not touched. `@objectstack/core` therefore leaves the DEBT ledger: the coverage gate reads 70/79 type-checked with 9 ledgered, where it read 68/78 with 10. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 --- .changeset/core-typecheck-script.md | 58 ++++++++++++ .../core/examples/kernel-features-example.ts | 4 +- packages/core/examples/phase2-integration.ts | 91 +++++++++++-------- packages/core/package.json | 3 + packages/core/test-typecheck-debt.json | 18 ++++ packages/core/tsconfig.examples.json | 64 +++++++++++++ packages/core/tsconfig.json | 15 ++- packages/core/tsconfig.test.json | 64 +++++++++++++ scripts/check-type-check-coverage.mjs | 7 -- 9 files changed, 276 insertions(+), 48 deletions(-) create mode 100644 .changeset/core-typecheck-script.md create mode 100644 packages/core/test-typecheck-debt.json create mode 100644 packages/core/tsconfig.examples.json create mode 100644 packages/core/tsconfig.test.json diff --git a/.changeset/core-typecheck-script.md b/.changeset/core-typecheck-script.md new file mode 100644 index 0000000000..3ae00b9107 --- /dev/null +++ b/.changeset/core-typecheck-script.md @@ -0,0 +1,58 @@ +--- +"@objectstack/core": patch +--- + +feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613) + +`packages/core/package.json` declared exactly `build`, `test` and `test:watch`. +Around twenty sibling packages declare `typecheck`, and `turbo run typecheck` +selects only packages that declare the task — so the lint workflow's typecheck +job had no way to reach this package, and `pnpm --filter @objectstack/core +typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried +it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`, +and rest, runtime, mcp, services and plugins all import it. + +The state was tracked but not runnable. `check:type-check-coverage` carried +`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once +(91 to 98), so nothing was invisible — but a ledger only the gate can read is +not something a contributor working in the package can run, which is how a +dispatched task came to assume the script existed. + +**Measured at `84b8190ae`, dependency closure built first.** The undivided +program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it) +reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The +same program restricted to the 63 non-test source files reports **zero**. So the +build layer graduated as it stood, and the 98 did not have to be repaired before +the script could exist. + +**94 of the 98 were the check, not the code.** The repair is the split this +repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json` +stays the build config and excludes the test layer; a new `tsconfig.test.json` +compiles that layer under the module semantics vitest actually executes it with +(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the +TS2347 beside them and the share of 71 x TS7006 they cascade into — an import +that does not resolve makes every symbol it names `any`. **No test file was +edited.** Strictness is inherited and untouched. The residue is 4 errors over 4 +files, held per file and per signature in `test-typecheck-debt.json`, EXACT and +shrink-only. + +**The `examples/` half was found by the new script, not by the card.** Declaring +`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and +`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported +`packages/core/examples` — 2 non-test source files in no tsc program at all. +Neither had ever compiled: `kernel-features-example.ts` imported `../index.js` +(above the package root, never existed) and `phase2-integration.ts` imported +`@objectstack/core`, i.e. this package self-referencing by a name it declares in +no dependency block. Collapsing that cascade exposed rather than removed errors, +12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s +**private** `logger`; four members of the security scan result that do not exist +(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries +`status` and per-severity counts); and two config literals passing the unparsed +shapes where `PluginHealthMonitor.registerPlugin` and +`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last +pair is retirement drift — this file was edited by two retirements (restart keys, +`watchPatterns`) while no tsc program could check the result. Every correction is +pinned to this package's own signatures; `packages/spec` was not touched. + +`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads +70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10. diff --git a/packages/core/examples/kernel-features-example.ts b/packages/core/examples/kernel-features-example.ts index bd3e9d65a6..a6466bf58d 100644 --- a/packages/core/examples/kernel-features-example.ts +++ b/packages/core/examples/kernel-features-example.ts @@ -21,7 +21,7 @@ import { PluginMetadata, ServiceLifecycle, PluginContext -} from '../index.js'; +} from '../src/index.js'; // ============================================================================ // Example 1: Database Plugin with Health Checks @@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = { ctx.logger.info('Disconnecting from database...'); this.connected = false; }, - async query(sql: string) { + async query(_sql: string) { if (!this.connected) { throw new Error('Database not connected'); } diff --git a/packages/core/examples/phase2-integration.ts b/packages/core/examples/phase2-integration.ts index f5657b6e9d..853566e993 100644 --- a/packages/core/examples/phase2-integration.ts +++ b/packages/core/examples/phase2-integration.ts @@ -14,28 +14,34 @@ import { ObjectKernel, PluginHealthMonitor, HotReloadManager, - DependencyResolver, PluginPermissionManager, PluginSandboxRuntime, - PluginSecurityScanner -} from '@objectstack/core'; - -import type { Plugin } from '@objectstack/core'; + PluginSecurityScanner, + createLogger +} from '../src/index.js'; + +import type { Plugin, ObjectLogger } from '../src/index.js'; +// [#14613] The PARSED variants, because that is what the methods below take: +// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed` +// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed` +// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every +// key optional, so passing them was a real type error this file carried while no +// tsc program read it. import type { - PluginHealthCheck, - HotReloadConfig, - PermissionSet, + PluginHealthCheckParsed, + HotReloadConfigParsed, + PluginPermissionSet, SandboxConfig -} from '@objectstack/spec/system'; +} from '@objectstack/spec/kernel'; /** * Example: Enterprise Plugin Platform with Phase 2 Features */ export class EnterprisePluginPlatform { private kernel: ObjectKernel; + private logger: ObjectLogger; private healthMonitor: PluginHealthMonitor; private hotReload: HotReloadManager; - private depResolver: DependencyResolver; private permManager: PluginPermissionManager; private sandbox: PluginSandboxRuntime; private scanner: PluginSecurityScanner; @@ -49,13 +55,18 @@ export class EnterprisePluginPlatform { }, }); + // [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so + // every one of the 20 reads this file made of it was a type error -- + // invisible until this package declared a `typecheck` script, because no + // tsc program had ever compiled this directory. + this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' }); + // Initialize Phase 2 components - this.healthMonitor = new PluginHealthMonitor(this.kernel.logger); - this.hotReload = new HotReloadManager(this.kernel.logger); - this.depResolver = new DependencyResolver(this.kernel.logger); - this.permManager = new PluginPermissionManager(this.kernel.logger); - this.sandbox = new PluginSandboxRuntime(this.kernel.logger); - this.scanner = new PluginSecurityScanner(this.kernel.logger); + this.healthMonitor = new PluginHealthMonitor(this.logger); + this.hotReload = new HotReloadManager(this.logger); + this.permManager = new PluginPermissionManager(this.logger); + this.sandbox = new PluginSandboxRuntime(this.logger); + this.scanner = new PluginSecurityScanner(this.logger); } /** @@ -64,9 +75,9 @@ export class EnterprisePluginPlatform { async installPlugin( plugin: Plugin, config: { - health?: PluginHealthCheck; - hotReload?: HotReloadConfig; - permissions?: PermissionSet; + health?: PluginHealthCheckParsed; + hotReload?: HotReloadConfigParsed; + permissions?: PluginPermissionSet; sandbox?: SandboxConfig; securityScan?: boolean; } @@ -74,11 +85,11 @@ export class EnterprisePluginPlatform { const pluginName = plugin.name; const pluginVersion = plugin.version || '1.0.0'; - this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`); + this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`); // Step 1: Security Scan if (config.securityScan !== false) { - this.kernel.logger.info('Running security scan...'); + this.logger.info('Running security scan...'); const scanResult = await this.scanner.scan({ pluginId: pluginName, @@ -86,16 +97,20 @@ export class EnterprisePluginPlatform { // In real implementation, would provide actual files and dependencies }); - if (!scanResult.passed) { + // [#14613] `KernelSecurityScanResult` carries `status` and per-severity + // COUNTS; it has never had `passed`, `score`, `summary.critical` or + // `summary.high`. This block read four members that do not exist. + if (scanResult.status !== 'passed') { throw new Error( - `Security scan failed: Score ${scanResult.score}/100, ` + - `Critical: ${scanResult.summary.critical}, ` + - `High: ${scanResult.summary.high}` + `Security scan ${scanResult.status}: ` + + `${scanResult.summary.totalVulnerabilities} vulnerability(ies), ` + + `Critical: ${scanResult.summary.criticalCount}, ` + + `High: ${scanResult.summary.highCount}` ); } - this.kernel.logger.info( - `Security scan passed: ${scanResult.score}/100` + this.logger.info( + `Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)` ); } @@ -106,7 +121,7 @@ export class EnterprisePluginPlatform { // Auto-grant all permissions (in production, would prompt user) this.permManager.grantAllPermissions(pluginName, 'system'); - this.kernel.logger.info( + this.logger.info( `Permissions registered: ${config.permissions.permissions.length} permissions` ); } @@ -114,13 +129,13 @@ export class EnterprisePluginPlatform { // Step 3: Create Sandbox if (config.sandbox) { this.sandbox.createSandbox(pluginName, config.sandbox); - this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`); + this.logger.info(`Sandbox created: ${config.sandbox.level} level`); } // Step 4: Register for Health Monitoring if (config.health) { this.healthMonitor.registerPlugin(pluginName, config.health); - this.kernel.logger.info( + this.logger.info( `Health monitoring configured: ${config.health.interval}ms interval` ); } @@ -128,7 +143,7 @@ export class EnterprisePluginPlatform { // Step 5: Register for Hot Reload if (config.hotReload) { this.hotReload.registerPlugin(pluginName, config.hotReload); - this.kernel.logger.info( + this.logger.info( `Hot reload enabled: ${config.hotReload.stateStrategy} state strategy` ); } @@ -136,7 +151,7 @@ export class EnterprisePluginPlatform { // Step 6: Register with Kernel this.kernel.use(plugin); - this.kernel.logger.info(`Plugin ${pluginName} installed successfully`); + this.logger.info(`Plugin ${pluginName} installed successfully`); } /** @@ -153,14 +168,14 @@ export class EnterprisePluginPlatform { } } - this.kernel.logger.info('Platform started successfully'); + this.logger.info('Platform started successfully'); } /** * Shutdown the platform */ async shutdown(): Promise { - this.kernel.logger.info('Shutting down platform...'); + this.logger.info('Shutting down platform...'); // Stop health monitoring this.healthMonitor.shutdown(); @@ -171,7 +186,7 @@ export class EnterprisePluginPlatform { // Shutdown kernel await this.kernel.shutdown(); - this.kernel.logger.info('Platform shutdown complete'); + this.logger.info('Platform shutdown complete'); } /** @@ -203,7 +218,7 @@ export class EnterprisePluginPlatform { * Perform hot reload of a plugin */ async reloadPlugin(pluginName: string): Promise { - this.kernel.logger.info(`Hot reloading plugin: ${pluginName}`); + this.logger.info(`Hot reloading plugin: ${pluginName}`); const plugin = this.kernel['plugins'].get(pluginName); if (!plugin) { @@ -218,7 +233,7 @@ export class EnterprisePluginPlatform { // Restore state (simplified - would need plugin cooperation) const restoreState = (state: Record) => { - this.kernel.logger.info(`Restoring state from ${new Date(state.timestamp)}`); + this.logger.info(`Restoring state from ${new Date(state.timestamp)}`); // ... restore plugin state }; @@ -230,7 +245,7 @@ export class EnterprisePluginPlatform { restoreState ); - this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`); + this.logger.info(`Plugin ${pluginName} reloaded successfully`); } } diff --git a/packages/core/package.json b/packages/core/package.json index 6f7b6247ac..38b595e866 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,6 +20,9 @@ }, "scripts": { "build": "tsup && node ../../scripts/check-dts-emitted.mjs", + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck", + "check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json", + "gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json", "test": "vitest run", "test:watch": "vitest" }, diff --git a/packages/core/test-typecheck-debt.json b/packages/core/test-typecheck-debt.json new file mode 100644 index 0000000000..71697be33e --- /dev/null +++ b/packages/core/test-typecheck-debt.json @@ -0,0 +1,18 @@ +{ + "_comment": "Per-file tsc error debt of the @objectstack/core 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/core gen:test-typecheck-debt", + "_note": "OPENED AT 4, NOT AT 98, and the difference is the CHECK rather than any repair to a test file (#14613). This package declared NO typecheck script at all until this ledger existed, so `turbo run typecheck` -- which selects only packages declaring the task -- could not reach it; the state was tracked, as a `check:type-check-coverage` DEBT entry of 98 re-measured once from 91, but nothing a contributor could RUN reported it, which is how a dispatched task came to assume `pnpm --filter @objectstack/core typecheck` existed. Measured at 84b8190ae with the dependency closure built: the undivided program (`tsc --noEmit -p tsconfig.json`, tests included, as the DEBT entry measured it) reports 98 errors over 12 files, all 12 of them `.test.ts`; the same program over only the 63 non-test source files reports ZERO; and THIS program -- the same 48 test files under vitest's own module semantics -- reports 4. So 94 of the 98 were the build config's NodeNext judging vitest-executed ESM (TS2835 x22 and the TS2347 beside them, plus the share of TS7006 x71 they cause: an import that does not resolve makes every symbol it names `any`). Not one test file was edited to retire them. That is the `check-type-check-coverage` header's own discipline applied literally -- fix the config first, then read the residue -- and it is why the 98 in that ledger was an upper bound on nothing. WHAT THE 4 ACTUALLY ARE, deliberately left ledgered rather than repaired here. Two are ONE defect twice over: `src/plugin-loader.test.ts` (TS2352) and `src/security/plugin-permission-enforcer.test.ts` (TS2739) each build a mock PluginContext literal missing `registerServiceFactory`, `replaceService` and `getServiceScoped`. ⚠️ That is the SAME shape as the 30 x TS2345 the DEBT entry for `@objectstack/metadata` records ('every one the same mock PluginContext literal missing registerServiceFactory and getServiceScoped'), and that package's repair is in flight on its own card -- so the shared fixture those two want should be authored ONCE, by whoever closes that, rather than twice in parallel. Repairing them here would have raced it. The third, `src/utils/filter-tokens.test.ts` (TS2352), is a genuine question about a signature and not a fixture typo: a `$and` array of single-key literals is asserted into `Record[]`, and the union's absent keys are `undefined`, which no index signature of `string` admits -- repairing it means deciding whether the test's intent or the parameter's type is the wrong one. Only the fourth, `src/utils/migration-journal.test.ts` (TS6133, an unread `rows`), is mechanical, and a lone mechanical fix beside three judgement calls buys nothing while making the diff that opens this gate harder to read. ⛔ None of the 4 is a reason to widen `exclude` in `tsconfig.test.json`: the whole point of the split beside it is that the strictness flags are INHERITED and untouched. Each is red on the PR that changes it, and the ratchet is EXACT in both directions -- a file that loses its error is red until re-recorded, and reaching zero here means deleting the entry, not lowering a number.", + "entries": { + "src/plugin-loader.test.ts": { + "TS2352: Conversion of type '…' to type 'PluginContext' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.": 1 + }, + "src/security/plugin-permission-enforcer.test.ts": { + "TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1 + }, + "src/utils/filter-tokens.test.ts": { + "TS2352: Conversion of type '…' to type '…' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.": 1 + }, + "src/utils/migration-journal.test.ts": { + "TS6133: 'rows' is declared but its value is never read.": 1 + } + } +} diff --git a/packages/core/tsconfig.examples.json b/packages/core/tsconfig.examples.json new file mode 100644 index 0000000000..16cda9cbaf --- /dev/null +++ b/packages/core/tsconfig.examples.json @@ -0,0 +1,64 @@ +// The EXAMPLES-layer type-check program (#14613), the sibling-config pattern +// `packages/plugins/plugin-auth/tsconfig.examples.json` (#10869 / #14386) and +// `packages/spec/tsconfig.scripts.json` (#5475) already run. +// +// WHY IT ARRIVED WITH THE `typecheck` SCRIPT AND NOT BEFORE. Until that script +// existed this package was COVERED by a `check:type-check-coverage` DEBT entry, +// and `isUncheckedSourceCandidate` does not ask a ledgered package where its +// source sits. Declaring `typecheck` flips the package to COVERED-BY-SCRIPT and +// the gate immediately reported what that ledger had been standing in front of: +// `packages/core/examples` — 2 non-test source files in NO tsc program at all. +// So this file is not scope creep beside the script; it is the other half of +// the same invariant, and the gate refuses the tree without it. +// +// WHAT THE DIRECTORY TURNED OUT TO CONTAIN, measured at 84b8190ae: 12 errors +// over the 2 files, and NEITHER FILE COMPILED AT ALL. Both had a broken module +// specifier at the top — `kernel-features-example.ts` imported `../index.js`, +// which resolves above the package root and has never existed, and +// `phase2-integration.ts` imported `@objectstack/core`, i.e. this package +// self-referencing by a name it declares in no dependency block. An unresolved +// import makes every symbol it names `any`, which is where 3 of the 12 TS7006 +// came from; the pile was one third module resolution and two thirds its +// cascade. That is the same shape #14386 found in plugin-auth's example (a +// published example that could not resolve, compile or run for anyone who +// copied it) and the reason the repo's answer to an examples directory is a +// sibling program rather than a debt row — there is no ledger here, on purpose. +// +// The 4 remaining were the wrong SPEC SUBPATH, not a missing spec surface: +// `PluginHealthCheck`, `HotReloadConfig` and `SandboxConfig` are exported from +// `@objectstack/spec/kernel`, never from `@objectstack/spec/system`, and this +// package's own source already imports them from `kernel` +// (`src/health-monitor.test.ts`, `src/hot-reload.test.ts`). The fourth was a +// name that exists nowhere: `PermissionSet`. The type the demonstrated API +// actually consumes is `PluginPermissionSet` — read off +// `src/security/permission-manager.ts`, whose `registerPermissions` takes it — +// so the example was corrected against this package's own consumer rather than +// against a guess. ⛔ NOTHING IN `packages/spec` WAS TOUCHED, and nothing here +// needed it to be. +// +// ⚠️ `rootDir` IS DELIBERATELY NOT RE-DECLARED, and this is where this file +// departs from the plugin-auth precedent it otherwise copies. That one sets +// `rootDir: "."` because it redirects no specifier; `tsconfig.json` here +// redirects `@objectstack/types` to its SOURCE (read its #11663 comment), so +// `packages/types/src/**` enters any program that reaches this package's `src` +// — which an example importing `../src/index.js` does. Measured: with +// `rootDir: "."` this program reports a TS6059 pile naming +// `packages/types/src/*`; inheriting `..` (= `packages/`, the directory that +// genuinely contains every file in the program) reports ZERO. Emit is +// unaffected either way — this program sets `noEmit` and the package builds +// with tsup. +// +// STRICTNESS IS INHERITED and deliberately not relaxed: `strict`, +// `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns` and the rest come +// from the root config through `tsconfig.json`. The directory type-checks clean +// under them — it enters with ZERO recorded debt and there is no ledger here to +// record any in. A published example that does not compile is the finding, not +// a line to write down. +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["examples/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index c94397c632..48b1642702 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -26,5 +26,18 @@ } }, "include": ["src/**/*"], - "exclude": [] + // [#14613] The TEST layer moved to the `tsconfig.test.json` sibling, which + // `package.json`'s `typecheck` NAMES via `check:test-typecheck --project`. + // This stays the BUILD config. Splitting the two is what let a `typecheck` + // script exist here at all: this package carried a `check:type-check-coverage` + // DEBT entry of 98, and all 98 sat in `src/**/*.test.ts` while the 63 + // non-test source files measured ZERO — so the build program graduates as it + // stands, and the test layer keeps a per-file, shrink-only ledger instead of + // being repaired in the PR that opens the gate. + // + // ⚠️ 22 of the 98 were TS2835 ("dynamic import needs .js") and are an + // artifact of checking vitest-executed ESM under this config's NodeNext, not + // a fact about the tests. The sibling config answers that with vitest's own + // module semantics; ⛔ it loosens no strictness flag. + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] } diff --git a/packages/core/tsconfig.test.json b/packages/core/tsconfig.test.json new file mode 100644 index 0000000000..372dc88bd0 --- /dev/null +++ b/packages/core/tsconfig.test.json @@ -0,0 +1,64 @@ +// The TEST-layer type-check program (#14613), adopting the mechanism #5286 set +// for `packages/spec`, #5449 generalised, and `packages/rest` / `packages/objectql` +// already run. `tsconfig.json` beside it stays the BUILD config; this sibling +// puts the layer that config excludes back in front of tsc, and `package.json`'s +// `typecheck` script NAMES it (via `check:test-typecheck --project`), because a +// config no script invokes is exactly the phantom this mechanism exists to close. +// +// BEFORE THIS FILE, NO PACKAGE SCRIPT COMPILED ANY FILE IN THIS PACKAGE. There +// was no `typecheck` script at all — the ~20 sibling packages declare one and +// `@objectstack/core` did not — so `turbo run typecheck`, which selects only +// packages declaring the task, could not reach it. The package was not invisible: +// `check:type-check-coverage` carried it as a DEBT entry of 98 and had already +// re-measured it once (91 -> 98). What was missing was a script anyone working +// in the package could RUN, which is what cost a dispatched task a step. +// +// WHY THE SPLIT RATHER THAN ONE PROGRAM. Measured at 84b8190ae with the +// dependency closure built: `tsc --noEmit -p tsconfig.json` over the undivided +// program reports 98 errors across 12 files, and EVERY ONE of the 12 is a +// `.test.ts`. The same program restricted to the 63 non-test source files +// reports ZERO. So the build layer graduates exactly as it stands, and adding +// the script does not require repairing 98 errors first — the test layer is +// ledgered per file in `test-typecheck-debt.json` beside this config instead, +// EXACT and shrink-only. +// +// What differs from the build config, and what deliberately does NOT: +// - module semantics ONLY, plus `lib`. These tests are written and executed as +// ESM by vitest (esbuild/vite), and this package IS `"type": "module"`, so +// the build config's NodeNext compiles them as ESM too — and then demands +// explicit `.js` extensions on relative imports, which vitest does not. +// That single mismatch is 22 of the 98 (TS2835) plus the TS2347 beside them, +// and a share of the 71 TS7006 they cause: an import that does not resolve +// makes every symbol it names `any`. Those diagnostics are about the CHECK, +// never about the code. Matching vitest is fidelity, not laxity. No `DOM` in +// `lib`: nothing in this layer touches a browser global. +// - ⛔ STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`, +// `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch` +// are inherited from the root config and `types: ["node"]` from +// `tsconfig.json`. Nothing here may loosen a type rule; if a test does not +// compile, that is the finding. +// +// ⚠️ WHAT THIS PROGRAM INHERITS FROM `tsconfig.json`, both load-bearing and +// neither re-declared here (read that file's #11663 comment first): +// - `rootDir: ".."`. Already widened to `packages/` there as a CONSEQUENCE of +// the `paths` rule, not a preference. Re-declaring it as `"."` — as +// `packages/objectql`'s sibling config can, because it redirects nothing — +// would put `packages/types/src/**` outside the program's root and reopen +// the TS6059 pile that comment was written to close. +// - `paths: { "@objectstack/types": ["../types/src/index.ts"] }`. A child that +// declared its own `paths` would REPLACE this map rather than merge into it, +// silently sending that specifier back to `dist/` — i.e. making this layer's +// verdict a function of build state rather than of the source in the +// checkout, which `check:type-source-resolution` refuses. This file declares +// no `paths` at all, so the rule stands. +{ + "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 12fdaecc18..f2cb7596b4 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -647,13 +647,6 @@ const DEBT = { errors: 13, note: 'code-tier 11 (TS2493 tuple indexing) + 2 config-tier.', }, - '@objectstack/core': { - errors: 98, - note: 'code-tier 3 (TS18046/TS2739/TS2352); the rest is config-tier 23 (TS2835 x22 / TS2347 module ' - + 'resolution) and noise 72 (TS7006 x71, TS6133). Re-measured 98 at 5ab08428, up from 91: the ' - + 'code-tier count is UNCHANGED at 3, so the whole +7 landed in the NodeNext/implicit-any residue ' - + '-- which is the tier the note at the top of this ledger says to fix first, not last.', - }, '@objectstack/hono': { errors: 3, note: 'all code-tier (TS2769/TS18046).', From bb93f56c56f135aefc5ea95310ffa35492088540 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 15:11:32 +0000 Subject: [PATCH 2/2] fix(pm): record packages/core's test-typecheck-debt.json as NOT_DRIVER_MANAGED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check:merge-driver red on PR #14916: gen:test-typecheck-debt (added to @objectstack/core by #14613) had no recorded merge disposition in either ledger in scripts/regen-artifacts.mjs. packages/core/test-typecheck-debt.json is the same shrink-only, per-signature EXACT ratchet as its six siblings already routed to NOT_DRIVER_MANAGED (spec, client, rest, objectql, the plugin family, lint, runtime) — a mid-merge "discard both sides and regenerate" would record whatever the half-merged tree happens to compile to, turning a real GAINED-error red into merge noise. It also carries the family's clearest example of the second hazard: a hand-authored `_note` (never written by `--update`, only ever preserved verbatim from an existing file) recording a human's triage of all 4 entries — 2 deferred to a shared-fixture repair in flight on @objectstack/metadata, 1 a genuine open design question, 1 left unfixed on purpose for diff cleanliness — plus why the ledger opens at 4 rather than the 98 the retired check:type-check-coverage DEBT entry once measured (94 of those 98 were a NodeNext config artifact this PR's tsconfig.test.json fixes, not test debt). None of that triage is reconstructable from source. Verified: only one new generator (gen:test-typecheck-debt) was added by #14613's diff; check:test-typecheck and typecheck are non-generator scripts and need no disposition. `pnpm check:merge-driver` is green, both legs. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 --- scripts/regen-artifacts.mjs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scripts/regen-artifacts.mjs b/scripts/regen-artifacts.mjs index ab3b19303f..71a7c7967f 100644 --- a/scripts/regen-artifacts.mjs +++ b/scripts/regen-artifacts.mjs @@ -564,6 +564,35 @@ export const NOT_DRIVER_MANAGED = Object.freeze([ + 'prevents both more likely and harder to see: at that size a recomputed number reads as ' + 'plausible, and the per-signature keys underneath it would be silently rewritten with it.', }, + // ── #14613: `packages/core`'s starting ledger, same instrument ───────────── + // + // The package declared NO `typecheck` script at all until this PR, so this row + // both opens the ledger and is the reason `pnpm --filter @objectstack/core + // typecheck` now exists. + { + path: 'packages/core/test-typecheck-debt.json', + gen: 'gen:test-typecheck-debt', + owner: '@objectstack/core', + 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. This file also ' + + 'carries the family\'s clearest instance of the second thing at stake, in its hand-authored ' + + '`_note` (never written by `gen:test-typecheck-debt` — `--update` only ever preserves an ' + + 'EXISTING `_note` verbatim, it never invents or edits one): a human triaged all 4 entries ' + + 'and decided to fix NONE of them in the PR that opened this gate — 2 are one defect twice ' + + 'over (a mock `PluginContext` literal missing three methods) deliberately left for a ' + + "shared-fixture repair already in flight on `@objectstack/metadata`'s own ledger, 1 is a " + + "genuine open question about whether a test's assertion or a parameter's type is wrong, and " + + 'the 4th — though mechanical — was left beside them on purpose so the diff that opens this ' + + 'gate stays readable. It also records WHY the ledger opens at 4 and not the 98 the retired ' + + '`check:type-check-coverage` DEBT entry once measured: 94 of those 98 were `tsconfig.json`\'s ' + + 'NodeNext judging vitest-executed ESM, a config-tier artifact this PR\'s `tsconfig.test.json` ' + + 'fixes, not a test repair. None of that triage is reconstructable from source — a mid-merge ' + + 'regeneration can only recompute COUNTS from whatever the half-merged tree compiles to, ' + + 'never re-derive which of those counts a human already chose to defer, or why.', + }, { path: 'packages/sdui-parser/objectui-lockstep.json', gen: 'gen:sdui-lockstep',