Uh oh!
There was an error while loading. Please reload this page.
fix(service-cluster): compile the test layer with tsc, and repair the TS2322 it hid - #15032
Conversation
…322 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
… 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
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-sales
commented
Sep 3, 2026
os-dev-report {
"issue": 14181,
"status": "done",
"branch": "claude/issue-14181-service-cluster-test-tsc-program",
"pr": "https://github.com/objectstack-ai/objectstack/pull/15032",
"premise_still_valid": true,
"summary": "The card's premise held exactly: service-cluster had no `typecheck` script at all, so no tsc program read it, and the TS2322 in its contract witness `src/memory/memory.contract.test.ts:26` was real and still present. Wired the #14062 route copied from `plugin-webhooks` — the one structural match, because its BUILD config also does not exclude tests (plugin-auth/plugin-sharing/core all do, and AGENTS.md forbids ADDING such an exclusion, so core's shape does not transfer). Added `tsconfig.test.json` (module semantics only: esnext/bundler/ES2022; strictness inherited and untouched; no `paths`) plus `typecheck` + `check:test-typecheck` scripts and a `tsx` devDep. Fixed the TS2322 with a block-body arrow: `PubSubHandler` returns void-or-Promise-of-void and a concise arrow returned Array.push's `number`, which the void-return relaxation does not forgive because the target is a union — the repo's own established repair for this shape (@objectstack/metadata graduated on 20 of them). The spec contract is correct and was not touched. Deleted the package's `DEBT` entry from check-type-check-coverage.mjs (its own invariant: a covered package must not also sit in the ledger) and recorded the graduation in that file's prose. STOP-AND-REPORT VALVE NOT TRIGGERED: the count was 1, not a pile. RELAY POINT ADDRESSED — the config-tier-cascade contrast core reported (98 undivided vs 4 after the split) does not exist here: I measured BOTH ways before fixing anything and both read 1, so service-cluster carried no config-tier pile and the single error was genuinely code-tier. Note the relay's three cited commits (81208086a, ac8ed476f, e6ac0c6fd) were already ancestors of my BASE 44ffa2103 — I was building on that precedent, not racing it. ⚠️ TWO THINGS FOR THE PM: (1) I did NOT create a `test-typecheck-debt.json` and I do NOT conclude the pattern requires one here — residue is 0, and `plugin-webhooks`/`plugin-security` are explicit precedent that ABSENCE is the zero and is strictly stronger than an empty ledger; the 'may a package entering the ratchet open a baseline file' question therefore does not arise on this card. No ledger was grown in either direction. (2) `check:type-source-resolution` went RED on my diff and I took its documented onboarding re-baseline limb rather than the `paths` its failure text names — see open_questions, this is the one item wanting a reviewer's eye.",
"tests": "All on final head eb41bcdad; every exit code captured by redirect-then-read, never across a pipe. MEASUREMENT BEFORE FIX (closure built first via `pnpm --filter '@objectstack/service-cluster^...' build`, lock VERDICT command-exit 0): `tsc --noEmit --pretty false -p tsconfig.json` -> exit 2, 1 error; `tsc --noEmit --pretty false -p tsconfig.test.json` -> exit 2, 1 error. Both: \"src/memory/memory.contract.test.ts(26,46): error TS2322: Type 'number' is not assignable to type 'void or Promise-of-void'\" (target type spelled in words; the sanitizer eats angle-bracket generics). The two readings AGREEING is the load-bearing result — no config-tier pile. Program size from --listFiles: 410 files, 7 own src test files. AFTER FIX: both programs exit 0, 0 errors. PACKAGE LEVEL: `pnpm --filter @objectstack/service-cluster typecheck` exit 0 — \"check:test-typecheck: OK — @objectstack/service-cluster's test layer compiles under packages/services/service-cluster/tsconfig.test.json; 0 file(s) / 0 error(s) / 0 pinned signature(s)\"; `pnpm --filter @objectstack/service-cluster test` exit 0 — \"Test Files 7 passed (7), Tests 105 passed (105)\". GATE UNION: derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, re-derived after the 2nd commit (same 58 both times), full union re-run on final head — 58 gates, 55 exit 0, 3 exit 3, 0 RED. The two flagged as likely to move: check:type-check-coverage exit 0 (\"OK — 72/79 workspace packages type-checked (plus the root), 7 in the DEBT ledger (117 frozen raw errors), 1 exempt\" — 7 where it read 8, the graduation); check:type-source-resolution exit 0 after the re-baseline (\"OK — 119 tsc program(s) across 78 packages scanned; 58 registered\"). NOT MEASURED (neither green nor red), quoting each gate's own verdict: check-test-completeness exit 3 \"PREREQUISITE NOT MET — this gate grades a saved `turbo run test` log, and no log was named ... record this gate as NOT MEASURED\"; check:dual-build-cjs-loads exit 3 \"PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/\"; check:type-check-debt exit 3 \"PREREQUISITE NOT MET ... --re-measure cannot run: 29 workspace dependenc(ies) ... have no built type entry point on disk\" plus its own \"⛔ This is NOT a pass and NOT a finding: nothing was measured.\" Both edited gate scripts ran their own --self-test green; check:self-test-wired exit 0 in the union. PATHS EXPERIMENT (a real mutation, restored): mutated packages/services/service-cluster/tsconfig.json to add `paths` for core+spec under a trap with an absolute REPO_ROOT path; mutation proven on disk by grep counts (paths=1, core-src=1) before measuring; result 435 errors, ALL TS6059, all in packages/spec/src and packages/core/src; restore was `git checkout HEAD -- <abs path>` and is PROVEN, not assumed — `git diff HEAD` empty for that path AND `git hash-object` == `git rev-parse HEAD:<path>` == b25dd285cfdb4ab86c29e8480182f7ef68fb4934 (both non-empty). No ablation of the implementation itself was needed or claimed.",
"mcp_calls": "2 — create_pull_request and this comment. All issue/comment/PR-body reads went through unauthenticated REST (probe: 200), and the before/after `--list` baselines came from a second local worktree at BASE rather than any API.",
"open_questions": [
{
"question": "REVIEW CONFIRMATION, not a blocker (status is `done` and the gate is green): `check:type-source-resolution` went red on this diff because onboarding a `tsconfig.test.json` moves the package's tsc PROGRAM SET, which that gate judges per program. I took the onboarding re-baseline limb its own doc-block opens (precedents @objectstack/rest #12542, @objectstack/service-i18n #14386) and added a registry entry, rather than the `paths` rules the failure text names. Its doc-block says an author who is merely red cannot tell a legitimate re-baseline from an escape, and that a reviewer is where the two are told apart — so it should be looked at deliberately.",
"options": [
"A: keep the registry re-baseline (what I did). All three of the limb's stated terms are met and measured: (1) provenance — measured FOUR ways by varying only what `typecheck` names; with `tsconfig.json` alone the package is ABSENT from the registry, so the build program carries no dist-resolved import at all and the exposure is only REACHABLE through the onboarded program, not merely first seen there; and this package had ZERO counted programs before, so no pre-existing program could launder a dep. (2) numbers stated in place — before 57/78 packages, 118 programs, 288 pairs, 21 clean; after 58/78, 119, 290, 20 clean; +1 package, +1 program, +2 pairs and nothing else. (3) reviewed as a re-baseline — this entry.",
"B: add `paths` for @objectstack/core and @objectstack/spec instead. MEASURED, not argued: it takes this package's test layer from 0 errors to 435, every one a TS6059 in ANOTHER package's source (packages/spec/src, packages/core/src), billed to a package that cannot pay them down — PR #12570's finding and #8021's (247 TS6059) at larger scale, and it would immediately re-bury the zero this card exists to reach.",
"C: revert the whole card and leave the package unchecked."
],
"recommendation": "A. Long-term soundness dominates and decides it: the ratchet's value is that its population is finite and cannot grow behind anyone's back, and the limb exists precisely so that ONBOARDING a test layer — the thing this repo wants to happen — is not punished by a gate that only knew how to demand `paths`. B is refuted by measurement rather than preference (0 -> 435, all of it other packages' diagnostics), and it is the shape the registry's own doc-block already rejected for `rest`. C forfeits the card. Real business need: the pull is real and already-measured — this package's CONTRACT witness did not compile and nothing reported it. AI-error-proofing points the same way: a checked test zone with strictness inherited is exactly declared-equals-enforced, whereas `paths` here would produce a ledger nobody can pay down, which is the kind of surface that rots. Startup scope discipline: the diff adds no capability and no exported symbol — one config, two scripts, one devDep, one test-line repair."
}
],
"out_of_scope_findings": [
"NOT FILED as issues, deliberately — the dispatch and the seat relay both said report-do-not-touch, and the family-per-card pattern is the PM's to schedule: 3 sibling `packages/services/*` packages still have NO `typecheck` script and so are read by no tsc program — `service-automation` (DEBT entry 3), `service-knowledge` (DEBT 10), `service-storage` (DEBT 51). All three already carry measured DEBT entries in scripts/check-type-check-coverage.mjs, so they are tracked, not invisible. Each is the same #14062-class instance this card just closed for service-cluster, and each needs its own card.",
"Correction to the seat relay, load-bearing for scheduling: it stated `service-cluster` has no ledger entry at all. It DID — `'@objectstack/service-cluster': { errors: 1, note: 'code-tier 1 (TS2322).' }` in the coverage DEBT ledger, matching the card's measurement exactly. Deleting it was a required part of this PR (that gate fails a package that declares `typecheck` while still sitting in DEBT), so the graduation is in the diff rather than an oversight."
]
}Generated by Claude Code Generated by Claude Code |
PM ACCEPT — and the open question is ruled A, having actually told the two apart
The open question: |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14181
packages/services/service-clusterhad notypecheckscript at all (its scripts werebuildandtest), so no tsc program anywhere read this package. Turbo/CI typecheck lanes skipped it silently, because a zero-matching filter run exits 0.tsuptranspiles with esbuild andvitestruns through esbuild type-stripping; neither type-checks.Clause-②: no
The route, and which exemplar it was copied from
Copied from
plugin-webhooks, not fromplugin-auth/plugin-sharing/core, because it is the one structural match. The deciding property is what the BUILD config does with tests:tsconfig.jsonexcludes tests?plugin-auth,plugin-sharing**/*.test.tsinexclude)core(#14916)**/*.spec.ts,**/*.test.ts)test-typecheck-debt.json, 4 residualplugin-webhooksservice-clusterservice-cluster'stsconfig.jsonincludes the tests and always did, so the program that would have read them already existed and was simply never invoked. It is therefore not a package that needs an exclusion compensated for, and AGENTS.md is explicit in the other direction: "Neverexclude*.test.ts/*.spec.tsfrom a package'stsconfig.json". Sotsconfig.jsonis untouched, and the siblingtsconfig.test.jsonis the family's uniform instrument over the same files — exactly howplugin-webhooksstates its own case.The sibling changes module semantics only (
module: esnext,moduleResolution: bundler,lib: ES2022, matching how vitest actually executes these files). ⛔ Strictness is inherited and untouched, and it declares nopaths(a child'spathsREPLACES the parent map rather than merging).Both gate scripts were checked for a required spelling and conformed to:
check-type-check-coverage.mjs(graduation, below) andcheck-test-typecheck.mts(invoked via--project, the spelling--self-testpins).Measured error count, before and after
Dependency closure built first (
pnpm --filter '@objectstack/service-cluster^...' build), measured at44ffa2103:tsc --noEmit -p tsconfig.json)tsc --noEmit -p tsconfig.test.json)410 files in the program, covering all 7 of the package's
src/**/*.test.ts.The two readings agree, and that agreement is the load-bearing result. It means this package carried no config-tier pile at all — no TS2835, no TS7006 cascade from an unresolved import. That is the contrast
@objectstack/corereported the other way (#14916: 98 undivided, 4 after the split — 94 of them the check rather than the code). Here there was nothing for the split to retire, so the single error is genuinely code-tier. It is well inside the dispatch's "a handful, fix them properly" branch, so no stop-and-report was owed.The one error, and why the fix is correct rather than convenient
(spelled in words — the sanitizer eats angle-bracket generics; the compiler prints it with generics.)
That file is the package's contract witness: conformance to the
IPubSub/ILock/IKV/ICountercontracts is the entire point of its existence, and it did not compile.PubSubHandleris declared inpackages/spec/src/contracts/cluster-service.tsas a function returning void-or-Promise-of-void. A concise arrow body returnsArray.prototype.push'snumber, and TypeScript's void-return assignability relaxation does not forgive it, because the target is a UNION rather than a barevoid.Why this fix and not another:
awaitan async handler. Widening it (say to returnunknown) would be a consumer-side accommodation of a test defect, which contract-first forbids. The defect is in the test, so the test is where it is fixed.@objectstack/metadatagraduated on 20 instances of it ((evt) => arr.push(evt)in a watcher slot), as itemised incheck-type-check-coverage.mjs.Ledger consequences
DEBTentry deleted, not lowered.check-type-check-coverage.mjscarried'@objectstack/service-cluster': { errors: 1, note: 'code-tier 1 (TS2322).' }. That gate's own invariant is "covered packages must not also sit in the ledger" — declaringtypecheckwhile the entry stands is a structural finding. The entry is deleted and the graduation recorded in the file's prose, per its own convention. Gate now reads 7 in DEBT where it read 8.test-typecheck-debt.jsonis created, and its absence is the zero: the gate reads a missing ledger as no entries, under which any error in any file here is red immediately. Residue is 0, so no baseline file is needed — the question of whether a package entering the ratchet may open one does not arise here. ⛔ No ledger was grown in either direction.check:type-source-resolution— a re-baseline, please review it as oneThis gate went red on my diff, and the remedy its failure text names (
paths) is measurably the wrong one. Onboarding atsconfig.test.jsonmoves the package's tsc PROGRAM SET, which the gate judges per program. Its doc-block opens an explicit onboarding limb for exactly this, on three stated terms; precedents are@objectstack/rest(#12542) and@objectstack/service-i18n(#14386).Term 1 — provenance. Measured four ways on one checkout, varying only what
typechecknames:typechecknames--listtotalstsconfig.jsononlytsconfig.test.jsononlyRow 2 is the load-bearing one: the BUILD program carries no dist-resolved workspace type import at all, so the exposure is only reachable through the onboarded program, not merely first seen there. This package also had zero counted programs before, so there is no pre-existing program a dep could be laundered through — the cleanest form of this case.
Term 2 — numbers stated. before 57/78 packages, 118 programs, 288 pairs, 21 clean; after 58/78, 119, 290, 20 clean. +1 package, +1 program, +2 pairs — this entry and nothing else.
Term 3 — why the entry and not
paths, measured both ways. Redirecting the two deps to source takes this package's test layer from 0 errors to 435, all of themTS6059and every one in another package's source (packages/spec/src,packages/core/src) — billed to a package that cannot pay them down. That is PR #12570's finding (+5 forrest) and #8021's (247 TS6059) reproduced at larger scale, on a package whose whole result here is reaching zero.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, re-derived after the second commit (same 58), union run on final headeb41bcdad. Exit codes captured by redirect-then-read, never across a pipe.58 gates: 55 exit 0, 3 exit 3 (NOT MEASURED), 0 red.
The two the dispatch flagged as most likely to move:
check:type-check-coverage— exit 0: "check-type-check-coverage: OK — 72/79 workspace packages type-checked (plus the root), 7 in the DEBT ledger (117 frozen raw errors), 1 exempt."check:type-source-resolution— exit 0 after the re-baseline: "check-type-source-resolution OK — 119 tsc program(s) across 78 packages scanned; 58 registered as still resolving a workspace dep's types throughdist/."NOT MEASURED, quoting each gate's own verdict (neither green nor red):
check-test-completeness— exit 3: "PREREQUISITE NOT MET — this gate grades a savedturbo run testlog, and no log was named. … running the family locally, record this gate as NOT MEASURED."check:dual-build-cjs-loads— exit 3: "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/."check:type-check-debt— exit 3: "check-type-check-coverage: PREREQUISITE NOT MET … --re-measure cannot run: 29 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk." Its own text adds: "⛔ This is NOT a pass and NOT a finding: nothing was measured."Package-level, on the final head:
pnpm --filter @objectstack/service-cluster typecheckexit 0 — "check:test-typecheck: OK — @objectstack/service-cluster's test layer compiles … 0 file(s) / 0 error(s)" — andpnpm --filter @objectstack/service-cluster testexit 0, 7 files / 105 tests passed.Both edited gate scripts ran their own
--self-testgreen, andcheck:self-test-wiredis in the union above.Declined, with reasons
packages/services/*packages with the same gap — reported, not touched.service-automation,service-knowledgeandservice-storagestill lack atypecheckscript (all three carry DEBT entries: 3, 10, 51). The family-per-card pattern is deliberate.pathsrules added — measured at +435 TS6059 in other packages' source (above).@ts-expect-error/@ts-ignoreadded;strictuntouched.packages/specnot touched (single-owner lane) — only read, to confirm the handler contract is correct.content/docs/releasesedit.Changeset
.changeset/service-cluster-test-tsc-program.md,patchon@objectstack/service-cluster. No runtime code changed —srcexcluding tests is byte-identical, verified — so no shipped behaviour moves; the level reflects the publishedpackage.jsongainingtypecheck/check:test-typecheckscripts and atsxdevDependency.🤖 Generated with Claude Code
https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Generated by Claude Code