Skip to content

Commit 0fb944b

Browse files
os-salesclaude
andauthored
fix(service-cluster): compile the test layer with tsc, and repair the TS2322 it hid (#15032)
* fix(service-cluster): compile the test layer with tsc, repair the TS2322 it hid `packages/services/service-cluster` had no `typecheck` script at all, so no tsc program read the package: turbo/CI typecheck lanes skipped it silently (a zero-matching filter run exits 0), while tsup and vitest both type-STRIP. Its `tsconfig.json` does include the tests and always did — the program existed and was never invoked. That hid a TS2322 in `src/memory/memory.contract.test.ts`, the package's contract witness: a concise arrow body passed as a `PubSubHandler` returns `Array.prototype.push`'s `number` where the contract declares `void | Promise<void>`, and the void-return relaxation does not forgive a UNION target. Fixed with a block body — the handler is side-effect-only by contract. The spec contract is untouched. Wired by the route #14062 settled for `packages/plugins/**`: a sibling `tsconfig.test.json` changing module semantics only (strictness inherited, untouched), named by a new `typecheck` script through `check:test-typecheck`. Measured 1/1 errors before (build semantics / new config — they agree, so no config-tier pile), 0/0 after, over a 410-file program covering all 7 test files. No `test-typecheck-debt.json` is added; its absence is the zero. The package's `DEBT` entry in `scripts/check-type-check-coverage.mjs` is deleted rather than lowered, which is the graduation that gate's own invariant requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8 * fix(service-cluster): re-baseline the type-source-resolution registry for the onboarded test program Onboarding a `tsconfig.test.json` moves this package's tsc PROGRAM SET, which `check:type-source-resolution` judges per program. `service-cluster` had NO `typecheck` script before, so it ran zero counted programs; both deps the gate now reports are reached only through the program this change added. Taken on the onboarding limb the registry's own doc-block opens, on its three stated terms. Provenance measured four ways by varying only what `typecheck` names: absent with no script, absent naming `tsconfig.json` alone, PRESENT naming `tsconfig.test.json`. The build program carries no dist-resolved workspace type import at all, so the exposure is only reachable through the onboarded program rather than merely first seen there. `--list` before 57/78 packages, 118 programs, 288 pairs; after 58/78, 119, 290. +1 package, +1 program, +2 pairs -- this entry and nothing else. `paths` was measured rather than argued and is the wrong tool here: redirecting the two deps to source takes the test layer from 0 errors to 435, all TS6059 and all in `packages/spec/src` and `packages/core/src` -- another package's diagnostics billed to one that cannot pay them down (PR #12570, #8021). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 22b0081 commit 0fb944b

7 files changed

Lines changed: 197 additions & 6 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@objectstack/service-cluster": patch
3+
---
4+
5+
fix(service-cluster): put the test layer in front of tsc, and repair the TS2322 it was hiding (#14181)
6+
7+
`packages/services/service-cluster` had **no `typecheck` script at all** — its
8+
scripts were `build` and `test` — so no tsc program anywhere read this package.
9+
Turbo/CI typecheck lanes skipped it silently, because a zero-matching filter run
10+
exits 0. `tsup` transpiles with esbuild and `vitest` runs through esbuild
11+
type-**stripping**; neither type-checks. The package's own `tsconfig.json` does
12+
include the tests and always did, so the program that would have read them
13+
already existed and was simply never invoked.
14+
15+
What that hid was in the worst possible file. `src/memory/memory.contract.test.ts`
16+
is the package's **contract witness** — type conformance to the `IPubSub` /
17+
`ILock` / `IKV` / `ICounter` contracts is the entire point of its existence — and
18+
it did not compile:
19+
20+
```
21+
src/memory/memory.contract.test.ts(26,46): error TS2322:
22+
Type 'number' is not assignable to type 'void | Promise<void>'.
23+
```
24+
25+
`cluster.pubsub.subscribe('e', (m) => received.push(m.payload))` passes a concise
26+
arrow body as a `PubSubHandler`, whose contract return type is
27+
`void | Promise<void>`. The body returns `Array.prototype.push`'s `number`, and
28+
TypeScript's void-return assignability relaxation does **not** forgive it,
29+
because the target is a UNION rather than a bare `void`. It is repaired with a
30+
block body — the handler is side-effect-only by contract, and the returned length
31+
was an accident of arrow syntax, never intent. The identical shape is what
32+
`@objectstack/metadata` graduated on (20 of them, `(evt) => arr.push(evt)` in a
33+
watcher slot).
34+
35+
⛔ The spec contract is untouched: `PubSubHandler` returning `void | Promise<void>`
36+
is correct and deliberate (the union is what lets a driver `await` an async
37+
handler). The defect was in the test, so the test is where it is fixed — no
38+
consumer-side widening, no source signature change.
39+
40+
Wired by the route the `packages/plugins/**` family settled on in #14062: a
41+
sibling `tsconfig.test.json` that changes **module semantics only** (`esnext` /
42+
`bundler` / `lib: ES2022`, matching how vitest actually executes these files)
43+
with **strictness inherited and untouched**, named by a new `typecheck` script
44+
through the shared `check:test-typecheck` gate. Measured before the repair: 1
45+
error under build semantics, 1 under the new config — the two readings agree, so
46+
this package carried no config-tier pile. After: 0 and 0, across a 410-file
47+
program covering all 7 of its `src/**/*.test.ts`.
48+
49+
No `test-typecheck-debt.json` is added, and its **absence is the zero**: the gate
50+
reads a missing ledger as `{ entries: {} }`, under which any error in any file
51+
here is red immediately. The package's `DEBT` entry in
52+
`scripts/check-type-check-coverage.mjs` (`errors: 1`) is deleted in this PR
53+
rather than lowered — that is the graduation the ratchet's own invariant
54+
requires, and it is why the error was fixed rather than ledgered.
55+
56+
No runtime code changes: `src/**` (excluding tests) is byte-identical, so no
57+
shipped behaviour moves. The `patch` level reflects the published `package.json`
58+
gaining `typecheck` / `check:test-typecheck` scripts and a `tsx` devDependency.

packages/services/service-cluster/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@
2424
},
2525
"scripts": {
2626
"build": "rm -rf dist && tsup && node ../../../scripts/check-dts-emitted.mjs",
27-
"test": "vitest run"
27+
"test": "vitest run",
28+
"typecheck": "tsc --noEmit && pnpm check:test-typecheck",
29+
"check:test-typecheck": "tsx ../../../scripts/check-test-typecheck.mts --self-test && tsx ../../../scripts/check-test-typecheck.mts --package packages/services/service-cluster --project tsconfig.test.json"
2830
},
2931
"dependencies": {
3032
"@objectstack/core": "workspace:*",
3133
"@objectstack/spec": "workspace:*"
3234
},
3335
"devDependencies": {
3436
"@types/node": "^26.2.0",
37+
"tsx": "^4.23.12",
3538
"typescript": "^6.0.3",
3639
"vitest": "^4.1.10"
3740
},

packages/services/service-cluster/src/memory/memory.contract.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('defineCluster(memory) smoke', () => {
2323

2424
// Round-trip through all four.
2525
const received: unknown[] = [];
26-
cluster.pubsub.subscribe('e', (m) => received.push(m.payload));
26+
cluster.pubsub.subscribe('e', (m) => { received.push(m.payload); });
2727
await cluster.pubsub.publish('e', 'hi');
2828
expect(received).toEqual(['hi']);
2929

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// The TEST-layer type-check program (#14181 — the `packages/services/**`
2+
// instance of the class #14062 settled for `packages/plugins/**`, itself
3+
// adopting the mechanism #5286 set for `packages/spec`, #5449 generalised,
4+
// #12542 carried to `packages/rest` and #13176 to `packages/plugins/
5+
// plugin-security`). `tsconfig.json` beside this one stays exactly as it is: it
6+
// is the BUILD config. This sibling puts the test layer in front of tsc under
7+
// the module semantics vitest really executes it with, and `package.json`'s
8+
// `typecheck` script NAMES it (via `check:test-typecheck --project`), because a
9+
// config no script invokes is exactly the phantom this whole change is about.
10+
//
11+
// ⚠️ WHAT WAS DIFFERENT HERE, and why this package was the worst case in the
12+
// family rather than one more of it: `service-cluster` had NO `typecheck`
13+
// script at all — its scripts were `build` and `test`. The other members hid
14+
// their tests behind an `exclude` in a config some script still ran; this one
15+
// ran no tsc anywhere. Its build config does NOT exclude tests and never did,
16+
// so the program that would have read them already existed and simply was
17+
// never invoked, while turbo/CI typecheck lanes skipped the package silently (a
18+
// zero-matching filter run exits 0). `tsup` type-strips, `vitest` type-strips.
19+
//
20+
// What differs from the build config, and what deliberately does NOT:
21+
// - MODULE SEMANTICS ONLY, plus `lib`. The tests are written and executed as
22+
// ESM by vitest (esbuild/vite). Matching that is FIDELITY, not laxity: it is
23+
// the same subtraction `packages/spec`, `packages/rest` and the
24+
// `packages/plugins/**` family each made.
25+
// - ⛔ STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`,
26+
// `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`,
27+
// `rootDir`, `paths` and `types` are all INHERITED from `tsconfig.json`
28+
// (and through it the root config), and none of them is re-declared here.
29+
// ⚠️ A child that declared its own `paths` would REPLACE the parent map
30+
// rather than merge into it, silently sending a source-resolved specifier
31+
// back to `dist/` — a BUILD ARTIFACT — so this file declares none.
32+
// Nothing here may loosen a type rule; if a test does not compile, that is
33+
// the finding.
34+
// - `lib: ["ES2022"]`, for the same reason `packages/rest` states: the root
35+
// config's `lib` is ES2020 and vitest runs on a Node that has es2022
36+
// builtins, so the gap is reported as TS2550 about the CHECK. No `DOM`:
37+
// nothing in this layer touches a browser global.
38+
//
39+
// MEASURED at 44ffa2103, workspace closure built first (`tsc --noEmit --pretty
40+
// false --listFiles -p tsconfig.test.json`, and the same command without
41+
// `--listFiles`):
42+
//
43+
// files in this program 410
44+
// own `src/**/*.test.ts` in it 7
45+
// errors under BUILD semantics 1
46+
// errors under THIS config 1
47+
//
48+
// The two readings agree, so this package carried no config-tier pile at all —
49+
// the single error is code-tier, and it is REPAIRED in the same PR rather than
50+
// ledgered. It was a TS2322 at `src/memory/memory.contract.test.ts:26`:
51+
// `(m) => received.push(m.payload)` passed as a `PubSubHandler`, whose contract
52+
// return type is `void | Promise<void>`. A concise arrow body returns
53+
// `Array.prototype.push`'s `number`, and the void-return assignability
54+
// relaxation does NOT forgive it because the target is a UNION rather than bare
55+
// `void`. That is the same shape `@objectstack/metadata` graduated on (#14342),
56+
// and the fix is a block body — the handler is side-effect-only by contract.
57+
// Triage was explicit that this one is to be fixed, not ledgered.
58+
//
59+
// There is NO `test-typecheck-debt.json` beside this config, and its ABSENCE is
60+
// the zero: `check:test-typecheck` reads a missing ledger as `{ entries: {} }`,
61+
// under which ANY error in ANY file here is red immediately, with no entry to be
62+
// added to. That is strictly stronger than a ledger holding nothing, and it is
63+
// the same call `plugin-webhooks` and `plugin-security` (#13176) each recorded
64+
// for themselves. If this package ever acquires residue that cannot be fixed in
65+
// the PR that causes it, THAT is when a ledger and a `gen:test-typecheck-debt`
66+
// script are owed — and adding one is maintainer-only (#5286), exactly as the
67+
// gate says when it refuses.
68+
{
69+
"extends": "./tsconfig.json",
70+
"compilerOptions": {
71+
"noEmit": true,
72+
"module": "esnext",
73+
"moduleResolution": "bundler",
74+
"lib": ["ES2022"]
75+
},
76+
"include": ["src/**/*"],
77+
"exclude": ["node_modules", "dist"]
78+
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/check-type-check-coverage.mjs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,16 @@ const ROOT_PROGRAM_COUPLED_SCRIPT = 'scripts/check-test-typecheck.mts';
663663
// already itemised its own tiers with confidence: a tier split read off an
664664
// unrepaired config is a guess about what is UNDER it, and the only honest way
665665
// to size the code tier is to fix the config and look.
666+
//
667+
// `@objectstack/service-cluster` GRADUATED from this ledger (#14181; entry: 1
668+
// raw, repaired to 0). Its single TS2322 was the very shape the paragraph above
669+
// itemises for `metadata` -- `(m) => received.push(m.payload)` in a slot typed
670+
// `void | Promise<void>` -- caught here in the package's own CONTRACT witness.
671+
// It is worth a line because this package reached the ledger by a different road
672+
// than the rest: it had NO `typecheck` script at all, so its build config never
673+
// ran even though that config DOES include the tests. Repaired by the #5286
674+
// route -- a `tsconfig.test.json` over the test layer, named by a new `typecheck`
675+
// script -- so the entry is deleted rather than lowered.
666676
const DEBT = {
667677
'@objectstack/cloud-connection': {
668678
errors: 13,
@@ -690,10 +700,6 @@ const DEBT = {
690700
+ 'by acquiring a second file, then 5 -> 3 by graduating the first -- so re-read what the pile is '
691701
+ 'made of before sizing it, never just the number.',
692702
},
693-
'@objectstack/service-cluster': {
694-
errors: 1,
695-
note: 'code-tier 1 (TS2322).',
696-
},
697703
'@objectstack/service-knowledge': {
698704
errors: 10,
699705
note: 'code-tier 3 (TS2339/TS2352/TS2493); config-tier 3 (TS2835); noise 4 (TS7006). Re-measured 10 at '

scripts/check-type-source-resolution.mjs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,49 @@ const KNOWN_DIST_RESOLVED_TYPE_IMPORTS = {
586586
'@objectstack/lint', '@objectstack/mcp', '@objectstack/platform-objects',
587587
'@objectstack/plugin-auth', '@objectstack/spec', '@objectstack/types',
588588
],
589+
// #14181 re-baseline (the onboarding limb above): a NEW entry, reached ONLY
590+
// through `tsconfig.test.json` -- a program this card ADDED. This is the
591+
// limb's cleanest case rather than a borderline one: `service-cluster` had NO
592+
// `typecheck` script AT ALL before (its scripts were `build` and `test`), so
593+
// it ran ZERO counted programs and there is no pre-existing program for a dep
594+
// to be laundered through. Both deps here are annotated `via
595+
// tsconfig.test.json` by this gate's own failure text.
596+
//
597+
// Provenance measured four ways on one checkout, by varying only what the
598+
// `typecheck` script NAMES (`--list`, totals as printed):
599+
//
600+
// no `typecheck` script (origin/main) absent 118 programs / 288 pairs
601+
// names `tsconfig.json` only absent 118 programs / 288 pairs
602+
// names `tsconfig.test.json` only PRESENT 119 programs / 290 pairs
603+
// names both (this card) PRESENT 119 programs / 290 pairs
604+
//
605+
// Row 2 is the load-bearing one: the BUILD program carries no dist-resolved
606+
// workspace type import at all, so the exposure is not merely first SEEN
607+
// through the onboarded program, it is only REACHABLE through it. (The two
608+
// programs put the same files in -- this package's `tsconfig.json` has never
609+
// excluded tests -- so module semantics, NodeNext vs bundler, is the only
610+
// axis that differs.)
611+
//
612+
// Numbers, `--list` before/after on the same checkout (before at 44ffa2103,
613+
// after with this card applied):
614+
//
615+
// before 57 of 78 packages, 118 programs, 288 pairs, 21 clean
616+
// after 58 of 78 packages, 119 programs, 290 pairs, 20 clean
617+
//
618+
// so +1 package, +1 program, +2 pairs -- this entry and nothing else.
619+
//
620+
// Why the entry and not `paths`, which is what this gate's failure text asks
621+
// for: MEASURED both ways on the same checkout, and `paths` is decisively the
622+
// wrong tool here. Redirecting these two deps to source takes this package's
623+
// test layer from 0 errors to 435, ALL of them TS6059 (`not under rootDir`)
624+
// and every one of them in ANOTHER package's source -- `packages/spec/src/**`
625+
// and `packages/core/src/**` -- billed to a package that cannot pay them down.
626+
// That is the PR #12570 finding (+5 TS6133 for `rest`) and the #8021 one (247
627+
// TS6059) reproduced at a much larger scale, on a package whose entire point
628+
// in this card was to reach ZERO test-layer errors. Note the direction: the
629+
// #5286 route it took makes its OWN test files compile clean, and `paths`
630+
// would immediately re-bury that result under other packages' diagnostics.
631+
'@objectstack/service-cluster': ['@objectstack/core', '@objectstack/spec'],
589632
// #14386 re-baseline (the onboarding limb above): a NEW entry, reached ONLY
590633
// through `tsconfig.typecheck.json` -- a program that card ADDED (this
591634
// package's `typecheck` was a bare `tsc --noEmit` before it, with no sibling

0 commit comments

Comments
 (0)