Skip to content

Commit 460134a

Browse files
os-salesclaude
andauthored
fix(service-automation): compile the test layer with tsc, and repair the TS2341 x3 it hid (#15152)
* wip: onboard service-automation typecheck, fix TS2341 residue * wip: onboarding gate registry entry + changeset * 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d4d5646 commit 460134a

7 files changed

Lines changed: 236 additions & 20 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(service-automation): put the test layer in front of tsc, and repair the TS2341 x3 it was hiding (#15048)
6+
7+
`packages/services/service-automation` had **no `typecheck` script at all**
8+
its scripts were `build` and `test` — so no tsc program anywhere read this
9+
package (`turbo run typecheck` selects only packages that declare the task, so
10+
it skipped this one silently). `tsup` transpiles with esbuild and `vitest`
11+
runs through esbuild type-**stripping**; neither type-checks. The package's
12+
own `tsconfig.json` does include the tests and always did, so the program that
13+
would have read them already existed and was simply never invoked. This is
14+
the `packages/services/**` sibling of `@objectstack/service-cluster`'s same
15+
graduation (#14181 / PR #15032), reached by the same road in.
16+
17+
What that hid was three `TS2341`s, all in
18+
`src/nested-region-parity.test.ts` (lines 95/151/180):
19+
20+
```
21+
error TS2341: Property 'flows' is private and only accessible within class 'AutomationEngine'.
22+
```
23+
24+
Three tests dot-read the private `AutomationEngine#flows` map directly
25+
instead of going through the class's own public accessor,
26+
`await engine.getFlow(name)` — already the idiom every other test file in
27+
this package uses. The fix replaces the three private reads with that
28+
existing public call (making the two synchronous test bodies `async` where
29+
they were not already); no source signature was widened, no cast was added.
30+
31+
Wired by the route the `packages/plugins/**` family settled on in #14062 and
32+
`service-cluster` carried into `packages/services/**` in #14181: a sibling
33+
`tsconfig.test.json` that changes **module semantics only** (`esnext` /
34+
`bundler` / `lib: ES2022`, matching how vitest actually executes these files)
35+
with **strictness inherited and untouched**, named by a new `typecheck`
36+
script through the shared `check:test-typecheck` gate. Measured before the
37+
repair: 3 errors under build semantics (`tsc -p tsconfig.json`, which already
38+
included the tests), 3 under the new config — the two readings agree, so this
39+
package carried no config-tier pile, and all 3 were genuinely code-tier from
40+
the start. After: 0 and 0, across a 555-file program covering all 103 of its
41+
`src/**/*.test.ts`.
42+
43+
No `test-typecheck-debt.json` is added, and its **absence is the zero**: the
44+
gate reads a missing ledger as `{ entries: {} }`, under which any error in any
45+
file here is red immediately. The package's `DEBT` entry in
46+
`scripts/check-type-check-coverage.mjs` (`errors: 3`) is deleted in this PR
47+
rather than lowered — that is the graduation the ratchet's own invariant
48+
requires, and it is why the errors were fixed rather than ledgered.
49+
50+
`scripts/check-type-source-resolution.mjs` also gains a registry entry for
51+
this package: onboarding `tsconfig.test.json` moved the package's tsc program
52+
set (per that gate's documented onboarding-limb terms), exposing 9 workspace
53+
deps whose types resolve through `dist/` with no pre-existing program for them
54+
to have been laundered through. `paths` was measured and rejected as the
55+
alternative — it takes this package's test layer from 0 errors to 648, nearly
56+
all billed to other packages' source.
57+
58+
No runtime code changes: `src/**` (excluding the one edited test file, whose
59+
own assertions are unchanged — only how it reaches the flow moved) is
60+
otherwise byte-identical, so no shipped behaviour moves. The `patch` level
61+
reflects the published `package.json` gaining `typecheck` /
62+
`check:test-typecheck` scripts and a `tsx` devDependency.

packages/services/service-automation/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
},
2121
"scripts": {
2222
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
23-
"test": "vitest run"
23+
"test": "vitest run",
24+
"typecheck": "tsc --noEmit && pnpm check:test-typecheck",
25+
"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"
2426
},
2527
"dependencies": {
2628
"@objectstack/core": "workspace:*",
@@ -38,6 +40,7 @@
3840
"@objectstack/service-job": "workspace:*",
3941
"@objectstack/service-messaging": "workspace:*",
4042
"@types/node": "^26.2.0",
43+
"tsx": "^4.23.12",
4144
"typescript": "^6.0.3",
4245
"vitest": "^4.1.10"
4346
},

packages/services/service-automation/src/nested-region-parity.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ describe('#4347 — a loop-body predicate is canonicalized like a top-level one'
9090
it.each([
9191
['a bare string', CONDITION],
9292
['an explicit CEL envelope', ENVELOPE],
93-
])('stores %s as the canonical envelope on BOTH edges', (_label, condition) => {
93+
])('stores %s as the canonical envelope on BOTH edges', async (_label, condition) => {
9494
engine.registerFlow('repro', reproFlow(condition));
95-
const flow = engine.flows.get('repro')!;
95+
const flow = (await engine.getFlow('repro'))!;
9696

9797
const topEdge = flow.edges.find(e => e.id === 'e2')!.condition;
9898
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', () =>
148148
edges: [{ id: 'e1', source: 'start', target: 'loop', type: 'default' }],
149149
});
150150

151-
expect((engine.flows.get('callout')!.nodes[1]!.config as any).body.nodes[0].type).toBe('http');
151+
expect(((await engine.getFlow('callout'))!.nodes[1]!.config as any).body.nodes[0].type).toBe('http');
152152
const result = await engine.execute('callout', { params: {}, event: 'schedule' } as never);
153153
expect(result.success).toBe(true);
154154
expect(called).toEqual(['nested']);
155155
});
156156

157-
it('canonicalizes a nested CRUD alias — an unconverted `filters` leaves no filter at all', () => {
157+
it('canonicalizes a nested CRUD alias — an unconverted `filters` leaves no filter at all', async () => {
158158
const engine = new AutomationEngine(silentLogger());
159159
registerLoopNode(engine, ctx());
160160
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', () =>
177177
edges: [{ id: 'e1', source: 'start', target: 'loop', type: 'default' }],
178178
});
179179

180-
expect((engine.flows.get('purge')!.nodes[1]!.config as any).body.nodes[0].config)
180+
expect(((await engine.getFlow('purge'))!.nodes[1]!.config as any).body.nodes[0].config)
181181
.toEqual({ objectName: 'lead', filter: { status: 'stale' } });
182182
});
183183
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// The TEST-layer type-check program (#15048 — the `packages/services/**`
2+
// instance of the class #14062 settled for `packages/plugins/**` and #14181
3+
// carried to `service-cluster` (PR #15032), itself adopting the mechanism
4+
// #5286 set for `packages/spec`, #5449 generalised, #12542 carried to
5+
// `packages/rest` and #13176 to `packages/plugins/plugin-security`).
6+
// `tsconfig.json` beside this one stays exactly as it is: it is the BUILD
7+
// config. This sibling puts the test layer in front of tsc under the module
8+
// semantics vitest really executes it with, and `package.json`'s `typecheck`
9+
// script NAMES it (via `check:test-typecheck --project`), because a config no
10+
// script invokes is exactly the phantom this whole change is about.
11+
//
12+
// ⚠️ WHY THIS PACKAGE COPIES `plugin-webhooks` / `service-cluster` RATHER THAN
13+
// `plugin-auth` / `plugin-sharing` / `core`: the deciding property is what the
14+
// BUILD config does with tests. `service-automation`'s `tsconfig.json` does
15+
// NOT exclude `src/**/*.test.ts` and never did — its `include` is `["src"]`
16+
// with no test exclusion — so the program that would have read them already
17+
// existed; it was simply never invoked (no `typecheck` script at all, only
18+
// `build` and `test`). That is the `plugin-webhooks`/`service-cluster` shape,
19+
// not the `exclude`-and-compensate shape the other three packages carry, and
20+
// AGENTS.md forbids ADDING such an exclusion, so their route does not
21+
// transfer here.
22+
//
23+
// What differs from the build config, and what deliberately does NOT:
24+
// - MODULE SEMANTICS ONLY, plus `lib`. The tests are written and executed as
25+
// ESM by vitest (esbuild/vite). Matching that is FIDELITY, not laxity: it
26+
// is the same subtraction `packages/spec`, `packages/rest`,
27+
// `plugin-security`, `plugin-webhooks` and `service-cluster` each made.
28+
// - ⛔ STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`,
29+
// `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`,
30+
// `rootDir`, `paths` and `types` are all INHERITED from `tsconfig.json`
31+
// (and through it the root config), and none of them is re-declared here.
32+
// ⚠️ A child that declared its own `paths` would REPLACE the parent map
33+
// rather than merge into it, silently sending a source-resolved specifier
34+
// back to `dist/` — a BUILD ARTIFACT — so this file declares none.
35+
// Nothing here may loosen a type rule; if a test does not compile, that is
36+
// the finding.
37+
// - `lib: ["ES2022"]`, for the same reason `packages/rest` states: the root
38+
// config's `lib` is ES2020 and vitest runs on a Node that has es2022
39+
// builtins, so the gap is reported as TS2550 about the CHECK. No `DOM`:
40+
// nothing in this layer touches a browser global.
41+
//
42+
// MEASURED at 2cc4610304 (origin/main), workspace closure built first
43+
// (`pnpm --filter '@objectstack/service-automation^...' build`, then
44+
// `tsc --noEmit --pretty false --listFiles -p tsconfig.test.json`, and the
45+
// same command without `--listFiles`), BEFORE any fix:
46+
//
47+
// files in this program 555
48+
// own `src/**/*.test.ts` in it 103
49+
// errors under BUILD semantics (tsc -p tsconfig.json, which already
50+
// included the tests) 3
51+
// errors under THIS config 3
52+
//
53+
// The two readings AGREE, so this package carried no config-tier pile at all
54+
// — unlike `@objectstack/core` (#14916: 98 undivided -> 4 after the split,
55+
// nearly all TS7006 cascading from one unresolved import) — and the 3 were
56+
// genuinely code-tier from the start: all TS2341 ("Property 'flows' is
57+
// private…"), all in `src/nested-region-parity.test.ts` (lines 95/151/180),
58+
// where three tests dot-read the PRIVATE `AutomationEngine#flows` map
59+
// directly instead of going through the class's own public accessor. That
60+
// accessor already exists and is already the idiom every other test file in
61+
// this package uses — `await engine.getFlow(name)` (defined at
62+
// `src/engine.ts`, returns `this.flows.get(name) ?? null`) — so the fix is
63+
// not a workaround: it replaces three private-internals reads with the public
64+
// surface the class was already offering, exactly as the rest of the suite
65+
// does. AFTER: 0 and 0, across a 555-file program covering all 103 of this
66+
// package's `src/**/*.test.ts`.
67+
//
68+
// There is NO `test-typecheck-debt.json` beside this config, and its ABSENCE
69+
// is the zero: `check:test-typecheck` reads a missing ledger as
70+
// `{ entries: {} }`, under which ANY error in ANY file here is red
71+
// immediately, with no entry to be added to. That is strictly stronger than a
72+
// ledger holding nothing, and it is the same call `plugin-webhooks`,
73+
// `plugin-security` (#13176) and `service-cluster` (#14181) each recorded for
74+
// themselves. If this package ever acquires residue that cannot be fixed in
75+
// the PR that causes it, THAT is when a ledger and a `gen:test-typecheck-debt`
76+
// script are owed — and adding one is maintainer-only (#5286), exactly as the
77+
// gate says when it refuses.
78+
{
79+
"extends": "./tsconfig.json",
80+
"compilerOptions": {
81+
"noEmit": true,
82+
"module": "esnext",
83+
"moduleResolution": "bundler",
84+
"lib": ["ES2022"]
85+
},
86+
"include": ["src/**/*"],
87+
"exclude": ["node_modules", "dist"]
88+
}

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: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,26 @@ const ROOT_PROGRAM_COUPLED_SCRIPT = 'scripts/check-test-typecheck.mts';
700700
// field the real call passes (TS2339). All 4 are fixed in the test file,
701701
// matching each mock's type to the call site it stubs; `ExecutionContext`
702702
// itself was not touched (it was correct -- the test's field name was stale).
703+
//
704+
// `@objectstack/service-automation` GRADUATED from this ledger (#15048; entry:
705+
// 3 raw, repaired to 0), the `packages/services/**` sibling of the
706+
// `service-cluster` graduation above (#14181/PR #15032) -- same road in: no
707+
// `typecheck` script at all (only `build` and `test`), and a BUILD
708+
// `tsconfig.json` that does NOT exclude tests, so the program that would have
709+
// read them already existed and was simply never invoked. Measured BOTH ways
710+
// (`tsc -p tsconfig.json`, which already included the tests, vs the new
711+
// `tsconfig.test.json`): 3 and 3 -- the two readings AGREE, so this package
712+
// carried no config-tier pile either, and the 3 were genuinely code-tier from
713+
// the start. All 3 were TS2341 ("Property 'flows' is private..."), all in
714+
// `src/nested-region-parity.test.ts` (95/151/180), where three tests dot-read
715+
// the PRIVATE `AutomationEngine#flows` map directly instead of the class's own
716+
// public accessor -- `await engine.getFlow(name)`, already the idiom every
717+
// other test file in this package uses. Repaired by replacing the three
718+
// private reads with that existing public call (no widened source signature,
719+
// no cast, no bracket-notation workaround); the tests were made `async` where
720+
// they were not already. Repaired by the #5286 route -- a `tsconfig.test.json`
721+
// over the test layer, named by a new `typecheck` script -- so the entry is
722+
// deleted rather than lowered.
703723
const DEBT = {
704724
'@objectstack/cloud-connection': {
705725
errors: 13,
@@ -713,20 +733,6 @@ const DEBT = {
713733
errors: 11,
714734
note: 'all code-tier (TS2554 wrong arity x10, TS2552).',
715735
},
716-
'@objectstack/service-automation': {
717-
errors: 3,
718-
note: 'code-tier 3 (TS2341 x3), all in src/nested-region-parity.test.ts at 95/151/180, where the '
719-
+ 'tests dot-read the private `engine.flows` -- not `engine[\'flows\']`, not `as any` (the casts on '
720-
+ 'two of those lines sit on `.config`, not on the engine, so they do not suppress it). Re-measured '
721-
+ '3 at 53a48c93f4, DOWN from 5 at 5ab08428: the two TS2741 in engine.test.ts this note used to '
722-
+ 'itemise alongside them have graduated -- that file now builds its pausing fixtures through a '
723-
+ 'single defineActionDescriptor helper that declares resumeAuthority (#5561), and engine.test.ts '
724-
+ 'still compiles in this project (`--listFiles` lists it) while reporting nothing. The residue is '
725-
+ 'therefore one decision, not an oversight: whether tests may read private state at all. This '
726-
+ 'entry is the specimen #5278 cites for composition drift and has now drifted BOTH ways -- 2 -> 5 '
727-
+ 'by acquiring a second file, then 5 -> 3 by graduating the first -- so re-read what the pile is '
728-
+ 'made of before sizing it, never just the number.',
729-
},
730736
'@objectstack/service-storage': {
731737
errors: 51,
732738
note: 'code-tier 8 (TS2339 x4, TS2347 x4); config-tier 26 (TS2835 x23, TS2550 x3); noise 17 '

scripts/check-type-source-resolution.mjs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,60 @@ const KNOWN_DIST_RESOLVED_TYPE_IMPORTS = {
625625
'@objectstack/lint', '@objectstack/mcp', '@objectstack/platform-objects',
626626
'@objectstack/plugin-auth', '@objectstack/spec', '@objectstack/types',
627627
],
628+
// #15048 re-baseline (the onboarding limb above): a NEW entry, reached ONLY
629+
// through `tsconfig.test.json` -- a program this card ADDED. Same shape as
630+
// the `service-cluster` re-baseline below (#14181): `service-automation` had
631+
// NO `typecheck` script AT ALL before (its scripts were `build` and `test`),
632+
// and its build `tsconfig.json` -- which is ALWAYS a counted program per this
633+
// gate's own design (see `programConfigsFor`'s doc-block) -- measured clean
634+
// on its own, so there is no pre-existing program a dep could be laundered
635+
// through. All 9 deps here are annotated `via tsconfig.test.json` by this
636+
// gate's own failure text.
637+
//
638+
// Provenance measured four ways on one checkout, by varying only what the
639+
// `typecheck` script NAMES (`--list`, totals as printed). RE-MEASURED on the
640+
// merge of `origin/main` @ 919beca43b, which had landed the `service-knowledge`
641+
// onboarding below (#15049) since this card's first reading: that merge moved
642+
// every ABSOLUTE here (+1 program, +3 pairs, +1 package before this entry
643+
// exists) and moved none of the DELTAS, which are what this block claims.
644+
//
645+
// no `typecheck` script (origin/main) absent 120 programs / 293 pairs
646+
// names `tsconfig.json` only absent 120 programs / 293 pairs
647+
// names `tsconfig.test.json` only PRESENT 121 programs / 302 pairs
648+
// names both (this card) PRESENT 121 programs / 302 pairs
649+
//
650+
// Row 2 is the load-bearing one: the BUILD program (which already includes
651+
// every test file -- `tsconfig.json`'s `include` has never excluded them)
652+
// carries no dist-resolved workspace type import at all, so the exposure is
653+
// not merely first SEEN through the onboarded program, it is only REACHABLE
654+
// through it. Numbers, before/after on the same checkout:
655+
//
656+
// before 59 of 78 packages, 120 programs, 293 pairs, 19 clean
657+
// after 60 of 78 packages, 121 programs, 302 pairs, 18 clean
658+
//
659+
// so +1 package, +1 program, +9 pairs (one per dep below) -- this entry and
660+
// nothing else.
661+
//
662+
// Why the entry and not `paths`: MEASURED, not argued. Redirecting these 9
663+
// deps to source takes this package's test layer from 0 errors to 648 (647
664+
// TS6059 `not under rootDir` + 1 TS6133), ALL 647 of the TS6059 in ANOTHER
665+
// package's source -- zero name a file under this package's own `src/`:
666+
// `packages/spec/src/**` 379, `packages/core/src/**` 62,
667+
// `packages/plugins/plugin-security/src/**` 60, `packages/objectql/src/**`
668+
// 49, `packages/services/service-messaging/src/**` 41, `packages/
669+
// metadata-core/src/**` 29, `packages/formula/src/**` 15, `packages/
670+
// services/service-job/src/**` 6, `packages/drivers/driver-sql/src/**` 6 --
671+
// billed to packages that cannot pay them down. Same finding as the
672+
// `service-cluster` re-baseline below (#14181: 0 -> 435) and PR #12570's
673+
// before it, reproduced again at a larger scale because this package pulls
674+
// more workspace deps. The #5286 route this entry backs makes this
675+
// package's OWN test files compile clean; `paths` would immediately re-bury
676+
// that result under other packages' diagnostics.
677+
'@objectstack/service-automation': [
678+
'@objectstack/core', '@objectstack/driver-sql', '@objectstack/formula',
679+
'@objectstack/metadata-core', '@objectstack/objectql', '@objectstack/plugin-security',
680+
'@objectstack/service-job', '@objectstack/service-messaging', '@objectstack/spec',
681+
],
628682
// #14181 re-baseline (the onboarding limb above): a NEW entry, reached ONLY
629683
// through `tsconfig.test.json` -- a program this card ADDED. This is the
630684
// limb's cleanest case rather than a borderline one: `service-cluster` had NO

0 commit comments

Comments
 (0)