From 044552910f9e174539f7a3e5904a4ca94c521b95 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 03:01:37 +0000 Subject: [PATCH 1/3] wip: onboard service-automation typecheck, fix TS2341 residue --- .../services/service-automation/package.json | 5 +- .../src/nested-region-parity.test.ts | 10 +-- .../service-automation/tsconfig.test.json | 88 +++++++++++++++++++ pnpm-lock.yaml | 3 + scripts/check-type-check-coverage.mjs | 34 ++++--- 5 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 packages/services/service-automation/tsconfig.test.json diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index 3cec97a038..cf8fe1cba0 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/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 && 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-automation --project tsconfig.test.json" }, "dependencies": { "@objectstack/core": "workspace:*", @@ -38,6 +40,7 @@ "@objectstack/service-job": "workspace:*", "@objectstack/service-messaging": "workspace:*", "@types/node": "^26.2.0", + "tsx": "^4.23.12", "typescript": "^6.0.3", "vitest": "^4.1.10" }, diff --git a/packages/services/service-automation/src/nested-region-parity.test.ts b/packages/services/service-automation/src/nested-region-parity.test.ts index 4236bf9b6b..645ac935b2 100644 --- a/packages/services/service-automation/src/nested-region-parity.test.ts +++ b/packages/services/service-automation/src/nested-region-parity.test.ts @@ -90,9 +90,9 @@ describe('#4347 — a loop-body predicate is canonicalized like a top-level one' it.each([ ['a bare string', CONDITION], ['an explicit CEL envelope', ENVELOPE], - ])('stores %s as the canonical envelope on BOTH edges', (_label, condition) => { + ])('stores %s as the canonical envelope on BOTH edges', async (_label, condition) => { engine.registerFlow('repro', reproFlow(condition)); - const flow = engine.flows.get('repro')!; + const flow = (await engine.getFlow('repro'))!; const topEdge = flow.edges.find(e => e.id === 'e2')!.condition; const bodyEdge = (flow.nodes.find(n => n.id === 'loop')!.config as any).body.edges[0].condition; @@ -148,13 +148,13 @@ describe('#4347 — the conversion table reaches a node inside a region', () => edges: [{ id: 'e1', source: 'start', target: 'loop', type: 'default' }], }); - expect((engine.flows.get('callout')!.nodes[1]!.config as any).body.nodes[0].type).toBe('http'); + expect(((await engine.getFlow('callout'))!.nodes[1]!.config as any).body.nodes[0].type).toBe('http'); const result = await engine.execute('callout', { params: {}, event: 'schedule' } as never); expect(result.success).toBe(true); expect(called).toEqual(['nested']); }); - it('canonicalizes a nested CRUD alias — an unconverted `filters` leaves no filter at all', () => { + it('canonicalizes a nested CRUD alias — an unconverted `filters` leaves no filter at all', async () => { const engine = new AutomationEngine(silentLogger()); registerLoopNode(engine, ctx()); engine.registerNodeExecutor({ type: 'delete_record', async execute() { return { success: true }; } } as NodeExecutor); @@ -177,7 +177,7 @@ describe('#4347 — the conversion table reaches a node inside a region', () => edges: [{ id: 'e1', source: 'start', target: 'loop', type: 'default' }], }); - expect((engine.flows.get('purge')!.nodes[1]!.config as any).body.nodes[0].config) + expect(((await engine.getFlow('purge'))!.nodes[1]!.config as any).body.nodes[0].config) .toEqual({ objectName: 'lead', filter: { status: 'stale' } }); }); }); diff --git a/packages/services/service-automation/tsconfig.test.json b/packages/services/service-automation/tsconfig.test.json new file mode 100644 index 0000000000..151ada1a66 --- /dev/null +++ b/packages/services/service-automation/tsconfig.test.json @@ -0,0 +1,88 @@ +// The TEST-layer type-check program (#15048 — the `packages/services/**` +// instance of the class #14062 settled for `packages/plugins/**` and #14181 +// carried to `service-cluster` (PR #15032), itself adopting the mechanism +// #5286 set for `packages/spec`, #5449 generalised, #12542 carried to +// `packages/rest` and #13176 to `packages/plugins/plugin-security`). +// `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. +// +// ⚠️ WHY THIS PACKAGE COPIES `plugin-webhooks` / `service-cluster` RATHER THAN +// `plugin-auth` / `plugin-sharing` / `core`: the deciding property is what the +// BUILD config does with tests. `service-automation`'s `tsconfig.json` does +// NOT exclude `src/**/*.test.ts` and never did — its `include` is `["src"]` +// with no test exclusion — so the program that would have read them already +// existed; it was simply never invoked (no `typecheck` script at all, only +// `build` and `test`). That is the `plugin-webhooks`/`service-cluster` shape, +// not the `exclude`-and-compensate shape the other three packages carry, and +// AGENTS.md forbids ADDING such an exclusion, so their route does not +// transfer here. +// +// 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`, +// `plugin-security`, `plugin-webhooks` and `service-cluster` 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 at 2cc4610304 (origin/main), workspace closure built first +// (`pnpm --filter '@objectstack/service-automation^...' build`, then +// `tsc --noEmit --pretty false --listFiles -p tsconfig.test.json`, and the +// same command without `--listFiles`), BEFORE any fix: +// +// files in this program 555 +// own `src/**/*.test.ts` in it 103 +// errors under BUILD semantics (tsc -p tsconfig.json, which already +// included the tests) 3 +// errors under THIS config 3 +// +// The two readings AGREE, so this package carried no config-tier pile at all +// — unlike `@objectstack/core` (#14916: 98 undivided -> 4 after the split, +// nearly all TS7006 cascading from one unresolved import) — and the 3 were +// genuinely code-tier from the start: all TS2341 ("Property 'flows' is +// private…"), all in `src/nested-region-parity.test.ts` (lines 95/151/180), +// where three tests dot-read the PRIVATE `AutomationEngine#flows` map +// directly instead of going through the class's own public accessor. That +// accessor already exists and is already the idiom every other test file in +// this package uses — `await engine.getFlow(name)` (defined at +// `src/engine.ts`, returns `this.flows.get(name) ?? null`) — so the fix is +// not a workaround: it replaces three private-internals reads with the public +// surface the class was already offering, exactly as the rest of the suite +// does. AFTER: 0 and 0, across a 555-file program covering all 103 of this +// package's `src/**/*.test.ts`. +// +// There is NO `test-typecheck-debt.json` beside this config, and its ABSENCE +// is the zero: `check:test-typecheck` reads a missing ledger as +// `{ entries: {} }`, under which ANY error in ANY file here is red +// immediately, with no entry to be added to. That is strictly stronger than a +// ledger holding nothing, and it is the same call `plugin-webhooks`, +// `plugin-security` (#13176) and `service-cluster` (#14181) each recorded for +// themselves. If this package ever acquires residue that cannot be fixed in +// the PR that causes it, THAT is when a ledger and a `gen:test-typecheck-debt` +// script are owed — and adding one is maintainer-only (#5286), exactly as the +// gate says when it refuses. +{ + "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 29dc9584df..2e9f9de270 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2336,6 +2336,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 diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 46e198d986..bf1e773c2c 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -673,6 +673,26 @@ const ROOT_PROGRAM_COUPLED_SCRIPT = 'scripts/check-test-typecheck.mts'; // ran even though that config DOES include the tests. Repaired by the #5286 // 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-automation` GRADUATED from this ledger (#15048; entry: +// 3 raw, repaired to 0), the `packages/services/**` sibling of the +// `service-cluster` graduation above (#14181/PR #15032) -- same road in: no +// `typecheck` script at all (only `build` and `test`), and a BUILD +// `tsconfig.json` that does NOT exclude tests, so the program that would have +// read them already existed and was simply never invoked. Measured BOTH ways +// (`tsc -p tsconfig.json`, which already included the tests, vs the new +// `tsconfig.test.json`): 3 and 3 -- the two readings AGREE, so this package +// carried no config-tier pile either, and the 3 were genuinely code-tier from +// the start. All 3 were TS2341 ("Property 'flows' is private..."), all in +// `src/nested-region-parity.test.ts` (95/151/180), where three tests dot-read +// the PRIVATE `AutomationEngine#flows` map directly instead of the class's own +// public accessor -- `await engine.getFlow(name)`, already the idiom every +// other test file in this package uses. Repaired by replacing the three +// private reads with that existing public call (no widened source signature, +// no cast, no bracket-notation workaround); the tests were made `async` where +// they were not already. Repaired by the #5286 route -- a `tsconfig.test.json` +// over the test layer, named by a new `typecheck` script -- so the entry is +// deleted rather than lowered. const DEBT = { '@objectstack/cloud-connection': { errors: 13, @@ -686,20 +706,6 @@ const DEBT = { errors: 11, note: 'all code-tier (TS2554 wrong arity x10, TS2552).', }, - '@objectstack/service-automation': { - errors: 3, - note: 'code-tier 3 (TS2341 x3), all in src/nested-region-parity.test.ts at 95/151/180, where the ' - + 'tests dot-read the private `engine.flows` -- not `engine[\'flows\']`, not `as any` (the casts on ' - + 'two of those lines sit on `.config`, not on the engine, so they do not suppress it). Re-measured ' - + '3 at 53a48c93f4, DOWN from 5 at 5ab08428: the two TS2741 in engine.test.ts this note used to ' - + 'itemise alongside them have graduated -- that file now builds its pausing fixtures through a ' - + 'single defineActionDescriptor helper that declares resumeAuthority (#5561), and engine.test.ts ' - + 'still compiles in this project (`--listFiles` lists it) while reporting nothing. The residue is ' - + 'therefore one decision, not an oversight: whether tests may read private state at all. This ' - + 'entry is the specimen #5278 cites for composition drift and has now drifted BOTH ways -- 2 -> 5 ' - + 'by acquiring a second file, then 5 -> 3 by graduating the first -- so re-read what the pile is ' - + 'made of before sizing it, never just the number.', - }, '@objectstack/service-knowledge': { errors: 10, note: 'code-tier 3 (TS2339/TS2352/TS2493); config-tier 3 (TS2835); noise 4 (TS7006). Re-measured 10 at ' From 0d3e94bc2deddc02fa178f64f22c9630357a4078 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 03:06:00 +0000 Subject: [PATCH 2/3] wip: onboarding gate registry entry + changeset --- .../service-automation-test-tsc-program.md | 62 +++++++++++++++++++ scripts/check-type-source-resolution.mjs | 50 +++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 .changeset/service-automation-test-tsc-program.md diff --git a/.changeset/service-automation-test-tsc-program.md b/.changeset/service-automation-test-tsc-program.md new file mode 100644 index 0000000000..8ca814c360 --- /dev/null +++ b/.changeset/service-automation-test-tsc-program.md @@ -0,0 +1,62 @@ +--- +"@objectstack/service-automation": patch +--- + +fix(service-automation): put the test layer in front of tsc, and repair the TS2341 x3 it was hiding (#15048) + +`packages/services/service-automation` had **no `typecheck` script at all** — +its scripts were `build` and `test` — so no tsc program anywhere read this +package (`turbo run typecheck` selects only packages that declare the task, so +it skipped this one silently). `tsup` transpiles with esbuild and `vitest` +runs through esbuild type-**stripping**; neither type-checks. The package's +own `tsconfig.json` does include the tests and always did, so the program that +would have read them already existed and was simply never invoked. This is +the `packages/services/**` sibling of `@objectstack/service-cluster`'s same +graduation (#14181 / PR #15032), reached by the same road in. + +What that hid was three `TS2341`s, all in +`src/nested-region-parity.test.ts` (lines 95/151/180): + +``` +error TS2341: Property 'flows' is private and only accessible within class 'AutomationEngine'. +``` + +Three tests dot-read the private `AutomationEngine#flows` map directly +instead of going through the class's own public accessor, +`await engine.getFlow(name)` — already the idiom every other test file in +this package uses. The fix replaces the three private reads with that +existing public call (making the two synchronous test bodies `async` where +they were not already); no source signature was widened, no cast was added. + +Wired by the route the `packages/plugins/**` family settled on in #14062 and +`service-cluster` carried into `packages/services/**` in #14181: a sibling +`tsconfig.test.json` that changes **module semantics only** (`esnext` / +`bundler` / `lib: ES2022`, matching how vitest actually executes these files) +with **strictness inherited and untouched**, named by a new `typecheck` +script through the shared `check:test-typecheck` gate. Measured before the +repair: 3 errors under build semantics (`tsc -p tsconfig.json`, which already +included the tests), 3 under the new config — the two readings agree, so this +package carried no config-tier pile, and all 3 were genuinely code-tier from +the start. After: 0 and 0, across a 555-file program covering all 103 of its +`src/**/*.test.ts`. + +No `test-typecheck-debt.json` is added, and its **absence is the zero**: the +gate reads a missing ledger as `{ entries: {} }`, under which any error in any +file here is red immediately. The package's `DEBT` entry in +`scripts/check-type-check-coverage.mjs` (`errors: 3`) is deleted in this PR +rather than lowered — that is the graduation the ratchet's own invariant +requires, and it is why the errors were fixed rather than ledgered. + +`scripts/check-type-source-resolution.mjs` also gains a registry entry for +this package: onboarding `tsconfig.test.json` moved the package's tsc program +set (per that gate's documented onboarding-limb terms), exposing 9 workspace +deps whose types resolve through `dist/` with no pre-existing program for them +to have been laundered through. `paths` was measured and rejected as the +alternative — it takes this package's test layer from 0 errors to 648, nearly +all billed to other packages' source. + +No runtime code changes: `src/**` (excluding the one edited test file, whose +own assertions are unchanged — only how it reaches the flow moved) is +otherwise 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/scripts/check-type-source-resolution.mjs b/scripts/check-type-source-resolution.mjs index 246a684eed..28d26d2189 100644 --- a/scripts/check-type-source-resolution.mjs +++ b/scripts/check-type-source-resolution.mjs @@ -625,6 +625,56 @@ const KNOWN_DIST_RESOLVED_TYPE_IMPORTS = { '@objectstack/lint', '@objectstack/mcp', '@objectstack/platform-objects', '@objectstack/plugin-auth', '@objectstack/spec', '@objectstack/types', ], + // #15048 re-baseline (the onboarding limb above): a NEW entry, reached ONLY + // through `tsconfig.test.json` -- a program this card ADDED. Same shape as + // the `service-cluster` re-baseline below (#14181): `service-automation` had + // NO `typecheck` script AT ALL before (its scripts were `build` and `test`), + // and its build `tsconfig.json` -- which is ALWAYS a counted program per this + // gate's own design (see `programConfigsFor`'s doc-block) -- measured clean + // on its own, so there is no pre-existing program a dep could be laundered + // through. All 9 deps here are annotated `via tsconfig.test.json` by this + // gate's own failure text. + // + // Provenance measured four ways on one checkout, by varying only what the + // `typecheck` script NAMES (`--list`, totals as printed): + // + // no `typecheck` script (origin/main) absent 119 programs / 290 pairs + // names `tsconfig.json` only absent 119 programs / 290 pairs + // names `tsconfig.test.json` only PRESENT 120 programs / 299 pairs + // names both (this card) PRESENT 120 programs / 299 pairs + // + // Row 2 is the load-bearing one: the BUILD program (which already includes + // every test file -- `tsconfig.json`'s `include` has never excluded them) + // carries no dist-resolved workspace type import at all, so the exposure is + // not merely first SEEN through the onboarded program, it is only REACHABLE + // through it. Numbers, before/after on the same checkout: + // + // before 58 of 78 packages, 119 programs, 290 pairs, 20 clean + // after 59 of 78 packages, 120 programs, 299 pairs, 19 clean + // + // so +1 package, +1 program, +9 pairs (one per dep below) -- this entry and + // nothing else. + // + // Why the entry and not `paths`: MEASURED, not argued. Redirecting these 9 + // deps to source takes this package's test layer from 0 errors to 648 (647 + // TS6059 `not under rootDir` + 1 TS6133), ALL 647 of the TS6059 in ANOTHER + // package's source -- zero name a file under this package's own `src/`: + // `packages/spec/src/**` 379, `packages/core/src/**` 62, + // `packages/plugins/plugin-security/src/**` 60, `packages/objectql/src/**` + // 49, `packages/services/service-messaging/src/**` 41, `packages/ + // metadata-core/src/**` 29, `packages/formula/src/**` 15, `packages/ + // services/service-job/src/**` 6, `packages/drivers/driver-sql/src/**` 6 -- + // billed to packages that cannot pay them down. Same finding as the + // `service-cluster` re-baseline below (#14181: 0 -> 435) and PR #12570's + // before it, reproduced again at a larger scale because this package pulls + // more workspace deps. The #5286 route this entry backs makes this + // package's OWN test files compile clean; `paths` would immediately re-bury + // that result under other packages' diagnostics. + '@objectstack/service-automation': [ + '@objectstack/core', '@objectstack/driver-sql', '@objectstack/formula', + '@objectstack/metadata-core', '@objectstack/objectql', '@objectstack/plugin-security', + '@objectstack/service-job', '@objectstack/service-messaging', '@objectstack/spec', + ], // #14181 re-baseline (the onboarding limb above): a NEW entry, reached ONLY // through `tsconfig.test.json` -- a program this card ADDED. This is the // limb's cleanest case rather than a borderline one: `service-cluster` had NO From c6d25b738fb898f2f3834c3de39f386f60e361fc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:55:27 +0000 Subject: [PATCH 3/3] fix(scripts): re-measure this entry's provenance totals on the merged tree The `service-knowledge` onboarding landed on `main` between this entry's first reading and this merge, so every absolute in its provenance block (programs, pairs, packages, clean count) was a number about a tree that no longer exists. Re-taken with `--list` on the merge commit itself, all four rows plus the before/after pair, by varying only what the `typecheck` script names: no `typecheck` script absent 120 programs / 293 pairs names tsconfig.json absent 120 programs / 293 pairs names tsconfig.test PRESENT 121 programs / 302 pairs names both (the card) PRESENT 121 programs / 302 pairs before 59 of 78 packages, 120 programs, 293 pairs, 19 clean after 60 of 78 packages, 121 programs, 302 pairs, 18 clean The deltas this block actually claims (+1 package, +1 program, +9 pairs, one per dep) are unchanged; only the absolutes moved, and the block now says which merge moved them. The sibling entries' own blocks keep their own historical readings untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --- scripts/check-type-source-resolution.mjs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/check-type-source-resolution.mjs b/scripts/check-type-source-resolution.mjs index e8864c95b3..d895d3e321 100644 --- a/scripts/check-type-source-resolution.mjs +++ b/scripts/check-type-source-resolution.mjs @@ -636,12 +636,16 @@ const KNOWN_DIST_RESOLVED_TYPE_IMPORTS = { // gate's own failure text. // // Provenance measured four ways on one checkout, by varying only what the - // `typecheck` script NAMES (`--list`, totals as printed): + // `typecheck` script NAMES (`--list`, totals as printed). RE-MEASURED on the + // merge of `origin/main` @ 919beca43b, which had landed the `service-knowledge` + // onboarding below (#15049) since this card's first reading: that merge moved + // every ABSOLUTE here (+1 program, +3 pairs, +1 package before this entry + // exists) and moved none of the DELTAS, which are what this block claims. // - // no `typecheck` script (origin/main) absent 119 programs / 290 pairs - // names `tsconfig.json` only absent 119 programs / 290 pairs - // names `tsconfig.test.json` only PRESENT 120 programs / 299 pairs - // names both (this card) PRESENT 120 programs / 299 pairs + // no `typecheck` script (origin/main) absent 120 programs / 293 pairs + // names `tsconfig.json` only absent 120 programs / 293 pairs + // names `tsconfig.test.json` only PRESENT 121 programs / 302 pairs + // names both (this card) PRESENT 121 programs / 302 pairs // // Row 2 is the load-bearing one: the BUILD program (which already includes // every test file -- `tsconfig.json`'s `include` has never excluded them) @@ -649,8 +653,8 @@ const KNOWN_DIST_RESOLVED_TYPE_IMPORTS = { // not merely first SEEN through the onboarded program, it is only REACHABLE // through it. Numbers, before/after on the same checkout: // - // before 58 of 78 packages, 119 programs, 290 pairs, 20 clean - // after 59 of 78 packages, 120 programs, 299 pairs, 19 clean + // before 59 of 78 packages, 120 programs, 293 pairs, 19 clean + // after 60 of 78 packages, 121 programs, 302 pairs, 18 clean // // so +1 package, +1 program, +9 pairs (one per dep below) -- this entry and // nothing else.