diff --git a/.changeset/service-storage-test-tsc-program.md b/.changeset/service-storage-test-tsc-program.md new file mode 100644 index 0000000000..0879cbebd1 --- /dev/null +++ b/.changeset/service-storage-test-tsc-program.md @@ -0,0 +1,50 @@ +--- +"@objectstack/service-storage": patch +--- + +fix(service-storage): put the test layer in front of tsc, and repair what it was hiding (#15050) + +`packages/services/service-storage` had **no `typecheck` script at all** — its +scripts were `build` and `test` — so no tsc program anywhere read this +package's test layer, and its errors were carried instead as a 51-error DEBT +entry in `scripts/check-type-check-coverage.mjs`. Gives it the #14062 / +#14181 "checked test zone" shape: a sibling `tsconfig.test.json` (module +semantics only — `esnext` / `bundler` / `lib: ES2022` — matching how vitest +actually executes these files; strictness inherited and untouched) plus a +`tsconfig.scripts.json` for `scripts/i18n-extract.config.ts` (the ninth +instance of #11351, previously excluded from that ledger only because this +package had no `typecheck` script to hang it on), both named by a new +`typecheck` script. + +Measured before repair: 51 errors under BUILD semantics (`tsc --noEmit -p +tsconfig.json`, which already includes the tests — matching the DEBT entry's +recorded number exactly), 10 under the split. Unlike `service-cluster` +(#14181), this package's BUILD reading was *not* already clean, so both +programs needed genuine repair, not just the test-only split: 23 `TS2835` +(relative imports missing their `.js` extension, required under BUILD's +NodeNext resolution) were fixed by *adding* the extension — which resolves +correctly under both NodeNext and the split's bundler mode — and clearing +that also cleared all 15 `TS7006` "implicitly any" as a downstream cascade +from the same unresolved imports (the shape `@objectstack/core` reported at +98 → 4). The remaining 3 `TS2550` (`Array.prototype.at` needing `lib` +es2022) are rewritten to indexed access rather than widening the shared +BUILD `tsconfig.json`. The 8 code-tier errors (`TS2339` × 4 — a test +helper's object-spread dropped its `Record` index +signature, fixed with an explicit return-shape annotation; `TS2347` × 4 — a +fake `ctx: any`'s `getService(...)` calls converted to `getService(...) +as T`, the pattern one call site in the same file had already adopted for +exactly this reason) are genuine test-file fixes. Both readings now agree at +0/0 — the same result `service-cluster` reported, reached by a longer road. + +The package's DEBT entry (51 errors) is **deleted**, not lowered — the +graduation this ratchet's invariant requires. No `test-typecheck-debt.json` +is added: residue is 0, so none is owed (#5286, maintainer-only to open). +`check:type-source-resolution` went red from onboarding the two new +programs (the documented onboarding-limb case): a registry entry is added +rather than `paths`, measured both ways — `paths` takes this package's test +layer from 0 errors to 306, all in other packages' source. + +No runtime code changes: `src/**` excluding tests is byte-identical, so no +shipped behaviour moves. The `patch` level reflects the published +`package.json` gaining `typecheck` / `check:test-typecheck` scripts and a +`tsx` devDependency. diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index 17a630e497..40773aecae 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -20,7 +20,9 @@ }, "scripts": { "build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json && pnpm check:test-typecheck", + "check:test-typecheck": "tsx ../../../scripts/check-test-typecheck.mts --self-test && tsx ../../../scripts/check-test-typecheck.mts --package packages/services/service-storage --project tsconfig.test.json" }, "dependencies": { "@objectstack/core": "workspace:*", @@ -45,6 +47,7 @@ "@objectstack/objectql": "workspace:*", "@objectstack/driver-sql": "workspace:*", "@types/node": "^26.2.0", + "tsx": "^4.23.12", "typescript": "^6.0.3", "vitest": "^4.1.10" }, diff --git a/packages/services/service-storage/src/error-envelope.conformance.test.ts b/packages/services/service-storage/src/error-envelope.conformance.test.ts index 5f962afda2..9a21bc6932 100644 --- a/packages/services/service-storage/src/error-envelope.conformance.test.ts +++ b/packages/services/service-storage/src/error-envelope.conformance.test.ts @@ -40,9 +40,9 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { BaseResponseSchema, envelopeViolations } from '@objectstack/spec/api'; import type { IHttpRequest, IHttpResponse, RouteHandler } from '@objectstack/spec/contracts'; -import { LocalStorageAdapter } from './local-storage-adapter'; -import { StorageMetadataStore } from './metadata-store'; -import { registerStorageRoutes } from './storage-routes'; +import { LocalStorageAdapter } from './local-storage-adapter.js'; +import { StorageMetadataStore } from './metadata-store.js'; +import { registerStorageRoutes } from './storage-routes.js'; const BASE = '/api/v1/storage'; diff --git a/packages/services/service-storage/src/file-reference-lifecycle.test.ts b/packages/services/service-storage/src/file-reference-lifecycle.test.ts index b10a3ddea0..e90a9ed31d 100644 --- a/packages/services/service-storage/src/file-reference-lifecycle.test.ts +++ b/packages/services/service-storage/src/file-reference-lifecycle.test.ts @@ -127,7 +127,13 @@ function recordDispatch(scope: Record = {}) { async function driveInsert(engine: Engine, object: string, data: Record, id: string) { const ctx: any = { object, event: 'beforeInsert', input: { data }, dispatch: recordDispatch() }; await engine.trigger('beforeInsert', ctx); - const row = { ...(ctx.input.data as Record), id }; + // Explicitly typed rather than left to spread-inference: TS drops a spread + // source's index signature when it has no KNOWN properties, so an + // un-annotated `row` here infers as bare `{ id: string }` and every + // `row.` read below is an error — the shape IS + // `Record` at runtime (the caller's `data` plus `id`), this + // just states it so the checker agrees. + const row: Record & { id: string } = { ...(ctx.input.data as Record), id }; (engine.tables[object] ??= []).push(row); ctx.event = 'afterInsert'; ctx.result = row; diff --git a/packages/services/service-storage/src/local-storage-adapter.metrics.test.ts b/packages/services/service-storage/src/local-storage-adapter.metrics.test.ts index 15cae46d03..87446ed5f2 100644 --- a/packages/services/service-storage/src/local-storage-adapter.metrics.test.ts +++ b/packages/services/service-storage/src/local-storage-adapter.metrics.test.ts @@ -5,7 +5,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { InMemoryMetricsRegistry, SEMCONV } from '@objectstack/observability'; -import { LocalStorageAdapter } from './local-storage-adapter'; +import { LocalStorageAdapter } from './local-storage-adapter.js'; describe('LocalStorageAdapter instrumentation', () => { let rootDir: string; diff --git a/packages/services/service-storage/src/local-storage-adapter.test.ts b/packages/services/service-storage/src/local-storage-adapter.test.ts index bf00830d05..1b3a605ce5 100644 --- a/packages/services/service-storage/src/local-storage-adapter.test.ts +++ b/packages/services/service-storage/src/local-storage-adapter.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, afterEach } from 'vitest'; import { promises as fs } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -import { LocalStorageAdapter } from './local-storage-adapter'; +import { LocalStorageAdapter } from './local-storage-adapter.js'; import type { IStorageService } from '@objectstack/spec/contracts'; describe('LocalStorageAdapter', () => { diff --git a/packages/services/service-storage/src/storage-adapter-list-contract.test.ts b/packages/services/service-storage/src/storage-adapter-list-contract.test.ts index bdd7edbe19..7284dc0688 100644 --- a/packages/services/service-storage/src/storage-adapter-list-contract.test.ts +++ b/packages/services/service-storage/src/storage-adapter-list-contract.test.ts @@ -30,8 +30,8 @@ */ import { describe, it, expect } from 'vitest'; -import { LocalStorageAdapter } from './local-storage-adapter'; -import { S3StorageAdapter } from './s3-storage-adapter'; +import { LocalStorageAdapter } from './local-storage-adapter.js'; +import { S3StorageAdapter } from './s3-storage-adapter.js'; /** Every method name reachable on an instance, own + prototype chain. */ function reachableMethodNames(instance: object): string[] { diff --git a/packages/services/service-storage/src/storage-adapter-list.conformance.test.ts b/packages/services/service-storage/src/storage-adapter-list.conformance.test.ts index 1ec64e9796..f4787df6b5 100644 --- a/packages/services/service-storage/src/storage-adapter-list.conformance.test.ts +++ b/packages/services/service-storage/src/storage-adapter-list.conformance.test.ts @@ -155,8 +155,8 @@ vi.mock('@aws-sdk/client-s3', () => { // `vi.mock` is hoisted above every import in this file, so the lazy // `await import('@aws-sdk/client-s3')` inside `S3StorageAdapter` resolves to the // fake above no matter where these two sit. -import { LocalStorageAdapter } from './local-storage-adapter'; -import { S3StorageAdapter } from './s3-storage-adapter'; +import { LocalStorageAdapter } from './local-storage-adapter.js'; +import { S3StorageAdapter } from './s3-storage-adapter.js'; // --------------------------------------------------------------------------- // Backend harness @@ -365,8 +365,8 @@ describe.each(BACKENDS)('$name adapter — list() conformance', ({ make }) => { expect(page.keys).toHaveLength(400); expect(page.nextCursor).toBeDefined(); } - expect(pages.at(-1)!.keys).toHaveLength(BULK_SET.length % 400); - expect(pages.at(-1)!.nextCursor).toBeUndefined(); + expect(pages[pages.length - 1]!.keys).toHaveLength(BULK_SET.length % 400); + expect(pages[pages.length - 1]!.nextCursor).toBeUndefined(); }, 60_000); it('fills one page past the backend\'s own page size (>1000 in a single call)', async () => { @@ -434,7 +434,7 @@ describe.each(BACKENDS)('$name adapter — list() conformance', ({ make }) => { it('issues a cursor the contract codec can read back', async () => { const page = await backend.adapter.list!('bulk/', { limit: 10 }); - expect(page.nextCursor).toBe(encodeStorageListCursor(page.items.at(-1)!.key)); + expect(page.nextCursor).toBe(encodeStorageListCursor(page.items[page.items.length - 1]!.key)); }); }); diff --git a/packages/services/service-storage/src/storage-route-ledger.conformance.test.ts b/packages/services/service-storage/src/storage-route-ledger.conformance.test.ts index f4357d5933..60e0058d9e 100644 --- a/packages/services/service-storage/src/storage-route-ledger.conformance.test.ts +++ b/packages/services/service-storage/src/storage-route-ledger.conformance.test.ts @@ -25,8 +25,8 @@ */ import { describe, it, expect, vi } from 'vitest'; -import { registerStorageRoutes } from './storage-routes'; -import { STORAGE_ROUTE_LEDGER } from './storage-route-ledger'; +import { registerStorageRoutes } from './storage-routes.js'; +import { STORAGE_ROUTE_LEDGER } from './storage-route-ledger.js'; /** Minimal IHttpServer mock that records registrations. */ function createMockServer() { diff --git a/packages/services/service-storage/src/storage-routes.test.ts b/packages/services/service-storage/src/storage-routes.test.ts index cdd4a93e42..070af4125f 100644 --- a/packages/services/service-storage/src/storage-routes.test.ts +++ b/packages/services/service-storage/src/storage-routes.test.ts @@ -2,9 +2,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { IHttpRequest, IHttpResponse, RouteHandler } from '@objectstack/spec/contracts'; -import { LocalStorageAdapter } from './local-storage-adapter'; -import { StorageMetadataStore } from './metadata-store'; -import { registerStorageRoutes } from './storage-routes'; +import { LocalStorageAdapter } from './local-storage-adapter.js'; +import { StorageMetadataStore } from './metadata-store.js'; +import { registerStorageRoutes } from './storage-routes.js'; import { promises as fs } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; @@ -409,7 +409,7 @@ describe('Storage REST Routes', () => { }); describe('attachments download gate (#2970 item 2)', () => { - const commit = async (s: StorageMetadataStore, rec: Partial) => + const commit = async (s: StorageMetadataStore, rec: Partial) => s.createFile({ id: rec.id ?? 'f-dl', key: rec.key ?? `attachments/${rec.id ?? 'f-dl'}.bin`, @@ -421,7 +421,7 @@ describe('Storage REST Routes', () => { ...rec, } as any); - function serverWith(verdict: import('./storage-routes').FileReadVerdict | 'skip', extra: any = {}) { + function serverWith(verdict: import('./storage-routes.js').FileReadVerdict | 'skip', extra: any = {}) { const server = createMockHttpServer(); const s = new StorageMetadataStore(null); const authorizeFileRead = diff --git a/packages/services/service-storage/src/storage-service-plugin.metrics.test.ts b/packages/services/service-storage/src/storage-service-plugin.metrics.test.ts index c5546c406a..e43651ca6a 100644 --- a/packages/services/service-storage/src/storage-service-plugin.metrics.test.ts +++ b/packages/services/service-storage/src/storage-service-plugin.metrics.test.ts @@ -9,7 +9,7 @@ import { OBSERVABILITY_METRICS_SERVICE, SEMCONV, } from '@objectstack/observability'; -import { StorageServicePlugin } from './storage-service-plugin'; +import { StorageServicePlugin } from './storage-service-plugin.js'; /** * Mirror of `cache-service-plugin.metrics.test.ts` — verifies the diff --git a/packages/services/service-storage/src/storage-service-plugin.test.ts b/packages/services/service-storage/src/storage-service-plugin.test.ts index 3ac4c45cd8..8ba1a836e0 100644 --- a/packages/services/service-storage/src/storage-service-plugin.test.ts +++ b/packages/services/service-storage/src/storage-service-plugin.test.ts @@ -4,9 +4,9 @@ import { describe, it, expect } from 'vitest'; import { promises as fs } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -import type { IStorageService, StorageFileInfo } from '@objectstack/spec/contracts'; -import { StorageServicePlugin } from './storage-service-plugin'; -import { SwappableStorageService } from './swappable-storage-service'; +import type { IStorageService } from '@objectstack/spec/contracts'; +import { StorageServicePlugin } from './storage-service-plugin.js'; +import { SwappableStorageService } from './swappable-storage-service.js'; /** * Plugin-level integration test exercising the settings live-wire. @@ -89,7 +89,10 @@ describe('StorageServicePlugin: settings live-wire', () => { }); const ctx = makeCtx(); await plugin.init(ctx); - const svc = ctx.getService('storage'); + // Plain call with a cast, not `getService(...)` — the fake ctx's + // getService is untyped, and each type-argument call adds a frozen-debt + // TS2347 to this package's shrink-only type-check ledger. + const svc = ctx.getService('storage') as IStorageService; expect(svc).toBeInstanceOf(SwappableStorageService); }); @@ -132,7 +135,7 @@ describe('StorageServicePlugin: settings live-wire', () => { await plugin.init(ctx); await plugin.start(ctx); - const proxy = ctx.getService('storage'); + const proxy = ctx.getService('storage') as SwappableStorageService; const innerBefore = proxy.getInner(); await ctx._flushReady(); @@ -160,7 +163,7 @@ describe('StorageServicePlugin: settings live-wire', () => { await plugin.init(ctx); await plugin.start(ctx); - const proxy = ctx.getService('storage'); + const proxy = ctx.getService('storage') as SwappableStorageService; const before = proxy.getInner(); await ctx._flushReady(); expect(proxy.getInner()).toBe(before); @@ -414,7 +417,7 @@ describe('StorageServicePlugin: settings live-wire', () => { await plugin.init(ctx); await plugin.start(ctx); - const proxy = ctx.getService('storage'); + const proxy = ctx.getService('storage') as SwappableStorageService; const before = proxy.getInner(); await ctx._flushReady(); expect(proxy.getInner()).toBe(before); // no swap diff --git a/packages/services/service-storage/src/success-envelope.conformance.test.ts b/packages/services/service-storage/src/success-envelope.conformance.test.ts index 7cf16455d0..3e18c8e246 100644 --- a/packages/services/service-storage/src/success-envelope.conformance.test.ts +++ b/packages/services/service-storage/src/success-envelope.conformance.test.ts @@ -67,9 +67,9 @@ import { RawUploadResponseSchema, } from '@objectstack/spec/api'; import type { IHttpRequest, IHttpResponse, RouteHandler } from '@objectstack/spec/contracts'; -import { LocalStorageAdapter } from './local-storage-adapter'; -import { StorageMetadataStore } from './metadata-store'; -import { registerStorageRoutes } from './storage-routes'; +import { LocalStorageAdapter } from './local-storage-adapter.js'; +import { StorageMetadataStore } from './metadata-store.js'; +import { registerStorageRoutes } from './storage-routes.js'; const BASE = '/api/v1/storage'; diff --git a/packages/services/service-storage/src/swappable-storage-service.test.ts b/packages/services/service-storage/src/swappable-storage-service.test.ts index 84be489d8a..9b0de9cbab 100644 --- a/packages/services/service-storage/src/swappable-storage-service.test.ts +++ b/packages/services/service-storage/src/swappable-storage-service.test.ts @@ -12,7 +12,7 @@ import { encodeStorageListCursor, resolveStorageListLimit, } from '@objectstack/spec/contracts'; -import { SwappableStorageService } from './swappable-storage-service'; +import { SwappableStorageService } from './swappable-storage-service.js'; class FakeAdapter implements IStorageService { public name: string; diff --git a/packages/services/service-storage/src/verify-file-references.test.ts b/packages/services/service-storage/src/verify-file-references.test.ts index e39b5c8ec8..ac4c9a4f40 100644 --- a/packages/services/service-storage/src/verify-file-references.test.ts +++ b/packages/services/service-storage/src/verify-file-references.test.ts @@ -216,7 +216,7 @@ describe('verifyFileReferences (ADR-0104 D3 wave 2 — R4 gate)', () => { const many = Array.from({ length: 1200 }, (_, i) => ({ id: `p${i}`, image: `file_${i}` })); const engine = fakeEngine({ product: many, - sys_file: many.map((r, i) => file(`file_${i}`, { object: 'product', recordId: `p${i}`, field: 'image' })), + sys_file: many.map((_r, i) => file(`file_${i}`, { object: 'product', recordId: `p${i}`, field: 'image' })), }); const report = await verifyFileReferences(engine); diff --git a/packages/services/service-storage/tsconfig.scripts.json b/packages/services/service-storage/tsconfig.scripts.json new file mode 100644 index 0000000000..bc3095edde --- /dev/null +++ b/packages/services/service-storage/tsconfig.scripts.json @@ -0,0 +1,42 @@ +// The SCRIPTS-layer type-check program for this package (#11351, the ninth +// instance — see `scripts/check-type-check-coverage.mjs`'s +// `UNCHECKED_SOURCE_DEBT` docblock for why this package was deliberately left +// out of that ledger's eight: it had NO `typecheck` script at all, so +// SOURCES_COVERED's invariant — which only asks its question of a package +// that DECLARES one — never fired on it, and the directory below was covered +// instead by the (now-deleted) `DEBT['@objectstack/service-storage']` entry. +// #15050 gives this package a `typecheck` script, so this file completes the +// family the same PR that makes SOURCES_COVERED start asking here. +// +// `scripts/i18n-extract.config.ts` is the input to this package's i18n +// extraction: it composes the package's own objects and translation bundles +// into an `ObjectStackDefinition`. It is real source, and until this file +// existed no tsc program read a line of it -- `tsconfig.json` selects only +// `src`, so the package would have passed `check:type-check-coverage` as +// COVERED with the directory unread the moment `typecheck` existed. +// +// A SIBLING rather than a wider `include` on `tsconfig.json`, the +// distinction #5475 drew for `packages/spec` and #10756 for +// `packages/objectql/scripts`: that config emits, so widening it to reach +// `scripts/` would put the directory in front of the emit. This program +// emits nothing. +// +// 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. Module semantics are inherited +// too (NodeNext): this config already spells its relative imports with `.js` +// extensions (#10868 annotated it), so nothing here needs `packages/spec`'s +// bundler resolution or `allowImportingTsExtensions`. +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + // `.` rather than the inherited `src`, because the file this program + // checks is the one outside `src`. Safe precisely because nothing is + // emitted from here -- see the header. + "rootDir": "." + }, + "include": ["scripts/**/*"] +} diff --git a/packages/services/service-storage/tsconfig.test.json b/packages/services/service-storage/tsconfig.test.json new file mode 100644 index 0000000000..8804177ce2 --- /dev/null +++ b/packages/services/service-storage/tsconfig.test.json @@ -0,0 +1,83 @@ +// The TEST-layer type-check program (#15050 — the `packages/services/**` +// instance of the class #14062 settled for `packages/plugins/**`, itself +// adopting the mechanism #5286 set for `packages/spec`, #5449 generalised, +// #12542 carried to `packages/rest`, #13176 to `packages/plugins/ +// plugin-security`, and #14181 to `packages/services/service-cluster`. +// `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. +// +// STRUCTURAL MATCH, checked before copying (the card's own warning): this +// package's `tsconfig.json` does NOT exclude tests — `include: ["src"]` with +// no test exclusion — so the program that would have read them already +// existed and was simply never invoked, the same shape as `plugin-webhooks` +// and `service-cluster`. It is therefore not one of the packages +// (`plugin-auth`, `plugin-sharing`, `core`) whose BUILD config excludes tests +// and needs its own compensation; AGENTS.md forbids adding such an exclusion. +// +// 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/**` / `service-cluster` family each made. +// - ⛔ STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`, +// `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`, +// `rootDir`, `paths` and `types` 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` states: 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 with the dependency closure built first +// (`pnpm --filter '@objectstack/service-storage^...' build`, and +// `pnpm --filter '@objectstack/objectql' build` re-run after its DTS worker +// was truncated by an unrelated 10-minute foreground kill — see the PR body +// for that finding), `tsc --noEmit --pretty false --listFiles -p ` +// and the same command without `--listFiles`: +// +// files in this program 1102 (BUILD) / 1068 (THIS config) +// own `src/**/*.test.ts` in it 35 +// errors under BUILD semantics 51 raw -> 0 after repair (matches the +// DEBT ledger's 51 exactly, before repair) +// errors under THIS config 10 raw -> 0 after repair +// +// The two readings AGREE at 0/0 after repair — same result `service-cluster` +// reported, reached by a longer road: unlike that package, this one's BUILD +// program was NOT already clean. 51 raw split as code-tier 8 (TS2339 x4, +// TS2347 x4) / config-tier 26 (TS2835 x23, TS2550 x3) / noise 17 (TS7006 x15, +// TS6196, TS6133) — matching the ledger's own composition note exactly. +// config-tier and noise were REPAIRED, not routed around: the 23 TS2835 +// (missing `.js` on relative imports, required under BUILD's NodeNext +// resolution but optional under THIS config's bundler resolution) are fixed +// by ADDING the extension, which resolves correctly under BOTH module modes +// -- and doing so also cleared all 15 TS7006 "implicitly any" as a +// downstream cascade (the same shape `@objectstack/core` reported at 98 -> 4, +// here 41 -> 3 after the extensions alone). The remaining 3 TS2550 +// (`Array.prototype.at`, needs `lib` es2022) are rewritten to indexed access +// (`arr[arr.length - 1]`) rather than widening the shared BUILD config's +// `lib` -- the narrower fix, and it leaves `tsconfig.json` untouched. The 8 +// code-tier (TS2339 x4 `driveInsert`'s spread dropped its index signature; +// TS2347 x4 a fake `ctx: any`'s `getService()` calls, the identical shape +// this file's OWN prior fix at line ~92 already documented for one call site +// but left three siblings unconverted) are fixed in the test files -- see the +// PR body for the full per-file breakdown. No `test-typecheck-debt.json`: +// residue is 0, so none is owed (#5286 route, maintainer-only to open one). +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["ES2022"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c521ae930..000bb7dfbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -377,7 +377,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(jsdom@30.0.1(@noble/hashes@2.3.0))(msw@2.14.6(@types/node@26.2.0)(typescript@6.0.3))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(jsdom@30.0.1(@noble/hashes@2.3.0))(msw@2.14.6(@types/node@26.2.0)(typescript@6.0.3))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) packages/apps/setup: dependencies: @@ -2731,6 +2731,9 @@ importers: '@types/node': specifier: ^26.2.0 version: 26.2.0 + tsx: + specifier: ^4.23.12 + version: 4.23.12 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -11646,15 +11649,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(msw@2.14.6(@types/node@26.2.0)(typescript@6.0.3))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - msw: 2.14.6(@types/node@26.2.0)(typescript@6.0.3) - vite: 8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) - '@vitest/mocker@4.1.10(msw@2.14.6(@types/node@26.2.0)(typescript@6.0.3))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 @@ -15729,21 +15723,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.26 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 26.2.0 - esbuild: 0.28.1 - fsevents: 2.3.3 - jiti: 2.7.0 - tsx: 4.23.12 - yaml: 2.9.0 - vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 @@ -15759,37 +15738,6 @@ snapshots: tsx: 4.23.12 yaml: 2.9.0 - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(jsdom@30.0.1(@noble/hashes@2.3.0))(msw@2.14.6(@types/node@26.2.0)(typescript@6.0.3))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(msw@2.14.6(@types/node@26.2.0)(typescript@6.0.3))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 - expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@types/node': 26.2.0 - '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) - happy-dom: 20.10.2 - jsdom: 30.0.1(@noble/hashes@2.3.0) - transitivePeerDependencies: - - msw - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(jsdom@30.0.1(@noble/hashes@2.3.0))(msw@2.14.6(@types/node@26.2.0)(typescript@6.0.3))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 0be82dc27e..c204b7e7d7 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -680,6 +680,28 @@ const ROOT_PROGRAM_COUPLED_SCRIPT = 'scripts/check-test-typecheck.mts'; // route -- a `tsconfig.test.json` over the test layer, named by a new `typecheck` // script -- so the entry is deleted rather than lowered. // +// `@objectstack/service-storage` GRADUATED from this ledger (#15050, the +// `packages/services/**` sibling of #14181; entry: 51 raw, repaired to 0 -- +// re-measured at exactly 51, confirming the entry's own composition note). +// Same road as `service-cluster` (no `typecheck` script at all; BUILD config +// already includes tests) but a longer repair: unlike that package, THIS +// one's BUILD reading was not already clean, so both programs needed fixing, +// not just the test-only split. code-tier 8 (TS2339 x4 a `driveInsert` test +// helper's spread dropped its `Record` index signature -- +// annotated the return type; TS2347 x4 a fake `ctx: any`'s +// `getService(...)` calls -- converted to `getService(...) as T`, the +// pattern one call site in the same file had already adopted for exactly this +// reason). config-tier 26 (TS2835 x23 relative imports missing `.js` under +// BUILD's NodeNext resolution -- ADDED the extension rather than routed +// around it, since that resolves under both BUILD's NodeNext and the split's +// bundler mode; TS2550 x3 `Array.prototype.at` needing `lib` es2022 -- +// rewritten to indexed access rather than widening the shared BUILD +// `tsconfig.json`). noise 17 (TS7006 x15, all a downstream CASCADE from the +// same unresolved imports -- cleared as a side effect of the TS2835 repair, +// the same shape `@objectstack/core` reported at 98 -> 4; TS6196 x1 dead +// import, TS6133 x1 unused param, both one-line). The two readings AGREE at +// 0/0 after repair, the same result `service-cluster` reported. +// // `@objectstack/service-knowledge` GRADUATED from this ledger too (#15049, // PR #15032's sibling for `packages/services/**`; entry: 10 raw, repaired to // 0 under BOTH the build config and the new `tsconfig.test.json` split). This @@ -733,37 +755,6 @@ const DEBT = { errors: 11, note: 'all code-tier (TS2554 wrong arity x10, TS2552).', }, - '@objectstack/service-storage': { - errors: 51, - note: 'code-tier 8 (TS2339 x4, TS2347 x4); config-tier 26 (TS2835 x23, TS2550 x3); noise 17 ' - + '(TS7006 x15, TS6196, TS6133). RE-TALLIED from tsc at the 51 below (62b2655d8), not the older ' - + '42-composition rescaled -- the previous tally summed to 42 and was never restated when this ' - + 'entry was lowered onto 51. The code tier is the half that did NOT move: the same 8, and all 8 ' - + 'sit in two files (src/file-reference-lifecycle.test.ts x4, src/storage-service-plugin.test.ts ' - + 'x4). Everything in the 42 -> 51 delta is config-tier and noise -- TS2835 21 -> 23, TS7006 ' - + '11 -> 15, plus TS2550 x3 (`Array.prototype.at` against a `lib` older than es2022, in ' - + 'src/storage-adapter-list.conformance.test.ts), a class the old tally did not list at all. Which ' - + 'PRs contributed the +9 is NOT attributed: the pre-#8225 per-file counts were not retained, and ' - + 'an invented attribution is worse than an admitted gap. ' - + 'This entry WAS the fourth bootstrap margin, and it earned the label the hard way inside ' - + 'one flight: 42 -> 41 at e8db1a230 (the spec half of the `IStorageService.list(prefix)` ' - + 'retirement, #5540 / PR #5983, removed one error, and it was lowered rather than left standing) ' - + '-> 42 again at 77c7c884b an hour later, when the adapter half (#5541 / PR #6061) deleted the ' - + 'old list tests (-1 TS7006) and added storage-adapter-list-retirement.test.ts (+2 TS2835). A ' - + 'two-PR retirement moves a count twice, and an exact number recorded between the halves is stale ' - + 'before it is pushed -- so this one took the same documented margin as the three proven-hot ' - + 'packages instead of a sixth calibration lap. (The `storage-adapter-list-retirement.test.ts` ' - + 'that history names no longer exists under that name; the adapter-list coverage is now ' - + 'src/storage-adapter-list.conformance.test.ts and src/storage-adapter-list-contract.test.ts.) ' - + 'The two concentrations the old note gave for the 42 both re-verify at 51: 11 in ' - + 'storage-route-ledger.conformance.test.ts and 7 in storage-service-plugin.test.ts, with ' - + 'swappable-storage-service.test.ts x7 and storage-routes.test.ts x5 next. ' - + 'THE MARGIN IS GONE. RECORDED 52 was a bootstrap margin (+10 over 42 measured at 77c7c884b), ' - + 'and it was spending itself the whole time it stood: the real count climbed 42 -> 51 underneath ' - + 'it, which is why the composition above had to be re-tallied rather than adjusted. #7888 / ' - + 'PR #8225 then lowered 52 -> 51 onto the exact measurement, re-confirmed at 51 at 62b2655d8, so ' - + 'the next new error here goes red on arrival (#5278 option A).', - }, '@objectstack/spec-monorepo': { errors: 26, compositionAt: 80, @@ -1275,18 +1266,22 @@ const PHANTOM_PIN_DEBT = {}; // The shape these eight copy is the minimal one -- `packages/objectql` // (#10756) and `packages/plugins/plugin-auth` (#10869). // -// THE NINTH CONFIG IS NOT HERE, deliberately. -// `packages/services/service-storage/scripts/i18n-extract.config.ts` is the -// ninth instance #10868 annotated, and that package appears in no line of this -// ledger for a reason SOURCES_COVERED makes structural: the invariant only asks -// its question of a package that DECLARES a `typecheck` script, and -// service-storage declares none. It is covered instead by -// DEBT['@objectstack/service-storage'], which records 51 errors -- so giving it -// a `typecheck` script is not a one-line graduation, it is a 51-error -// burn-down, and wiring one that ran ONLY `tsconfig.scripts.json` would be -// worse than leaving it: COVERED would start passing on a script that never -// reads `src`, and RECONCILED would then force out a 51-error DEBT entry whose -// errors are all still there. It graduates with that entry, not before it. +// THE NINTH CONFIG JOINED THE EIGHT (#15050), completing what this section +// used to say was deliberately deferred. `packages/services/ +// service-storage/scripts/i18n-extract.config.ts` is the ninth instance +// #10868 annotated, and this ledger carried no line for it because +// SOURCES_COVERED's invariant only asks its question of a package that +// DECLARES a `typecheck` script, and service-storage declared none -- it was +// covered instead by `DEBT['@objectstack/service-storage']` (51 errors), so +// wiring a `typecheck` that ran ONLY `tsconfig.scripts.json` would have been +// worse than leaving it: COVERED would have started passing on a script that +// never read `src`, while the 51 real errors stood. #15050 repairs the DEBT +// entry to 0 (deleted, not lowered -- see the graduation note above `DEBT`) +// in the SAME change that adds `tsconfig.scripts.json` here, so the ordering +// this paragraph used to insist on -- "it graduates with that entry, not +// before it" -- is satisfied by construction rather than deferred again. The +// directory type-checks clean (0 errors, matching all eight siblings), so no +// entry is added here either. // // ── #14710: `packages/cli/test` GRADUATED, exactly as its own entry foretold ── // diff --git a/scripts/check-type-source-resolution.mjs b/scripts/check-type-source-resolution.mjs index d895d3e321..6cad51d984 100644 --- a/scripts/check-type-source-resolution.mjs +++ b/scripts/check-type-source-resolution.mjs @@ -796,6 +796,56 @@ const KNOWN_DIST_RESOLVED_TYPE_IMPORTS = { '@objectstack/service-messaging': ['@objectstack/spec'], '@objectstack/service-realtime': ['@objectstack/spec'], '@objectstack/service-sms': ['@objectstack/core', '@objectstack/plugin-auth', '@objectstack/spec'], + // #15050 re-baseline (the onboarding limb above): a NEW entry, reached ONLY + // through `tsconfig.test.json` (all 7 deps) and `tsconfig.scripts.json` + // (`@objectstack/spec` again, no new pairs). Same shape as `service-cluster` + // (#14181, above): this package had NO `typecheck` script AT ALL before (its + // scripts were `build` and `test`), so it ran ZERO counted programs and + // there is no pre-existing program a dep could be laundered through. + // + // Provenance measured four ways on one checkout, by varying only what the + // `typecheck` script NAMES (`--list`, totals as printed). RE-MEASURED on the + // merge of `origin/main` @ 460134af8, which had landed BOTH sibling + // onboardings of this family since this card's first reading + // (`service-knowledge` #15049 and `service-automation` #15048, above): that + // merge moved every ABSOLUTE here (+2 programs, +12 pairs, +2 packages + // before this entry exists) and moved none of the DELTAS, which are what + // this block claims. + // + // no `typecheck` script (origin/main) absent 121 programs / 302 pairs + // names `tsconfig.json` only absent 121 programs / 302 pairs + // names `tsconfig.test.json` only PRESENT 122 programs / 309 pairs + // names all three (this card) PRESENT 123 programs / 309 pairs + // + // Row 2 is the load-bearing one, exactly as it was for `service-cluster`: + // the BUILD program carries no dist-resolved workspace type import at all, + // so the exposure is only REACHABLE through the onboarded programs, not + // merely first seen there. (`tsconfig.json` has never excluded tests, so + // module semantics — NodeNext vs bundler — is the axis that differs for the + // test program; `tsconfig.scripts.json` reads a directory BUILD's `include` + // never reached at all.) + // + // Numbers: +1 package (60 -> 61 of 78), +2 programs (121 -> 123, one per + // onboarded config), +7 pairs (302 -> 309) -- this entry and nothing else. + // + // Why the entry and not `paths`, which is what this gate's failure text + // asks for: MEASURED on this checkout (temporary `paths` added to + // `tsconfig.test.json`, `tsc --noEmit -p` run, then removed — never + // committed), and `paths` is decisively the wrong tool here, same as + // `service-cluster` found. Redirecting all 7 deps to source takes this + // package's test layer from 0 errors to 306 (305 x TS6059 "not under + // rootDir" + 1 x TS6133), every TS6059 in ANOTHER package's source + // (`packages/types/src/**`, `packages/spec/src/**`, `packages/objectql/ + // src/**`, `packages/observability/src/**`, `packages/drivers/ + // driver-sql/src/**`) -- billed to a package that cannot pay them down. The + // #5286 route this card took makes its OWN test files compile clean (0/0, + // both readings agree), and `paths` would immediately re-bury that result + // under other packages' diagnostics. + '@objectstack/service-storage': [ + '@objectstack/core', '@objectstack/driver-sql', '@objectstack/objectql', + '@objectstack/observability', '@objectstack/platform-objects', '@objectstack/spec', + '@objectstack/types', + ], '@objectstack/setup': ['@objectstack/platform-objects', '@objectstack/spec'], '@objectstack/studio': ['@objectstack/platform-objects', '@objectstack/spec'], '@objectstack/trigger-api': ['@objectstack/core', '@objectstack/spec'],