Skip to content

Commit 8422acd

Browse files
os-salesclaude
andauthored
fix(service-knowledge): put the test layer in front of tsc, and repair the four defects it was hiding (#15049) (#15147)
packages/services/service-knowledge had no typecheck script at all, so no tsc program anywhere read this package. Undivided (existing tsconfig.json) measured 10 raw errors, matching the DEBT entry this PR deletes; the correct split (new tsconfig.test.json) measured 4 -- not the ledger's own 3-code-tier guess, because fixing the 3 TS2835 config-tier imports re-enabled an excess-property check the resulting `any` cascade had been suppressing, uncovering a 4th real error (a stale `roles` field, renamed to `positions`). All 4 code-tier defects and the 3 TS2835 are repaired in the test files; both readings are now 0. DEBT entry deleted (not lowered). check:type-source-resolution gains an onboarding-limb registry entry for the 3 workspace deps now reached only through tsconfig.test.json (paths was measured and rejected: 0 -> 487 TS6059, all billed to other packages' source). Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 25a59bd commit 8422acd

8 files changed

Lines changed: 269 additions & 14 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
"@objectstack/service-knowledge": patch
3+
---
4+
5+
fix(service-knowledge): put the test layer in front of tsc, and repair the four defects it was hiding (#15049)
6+
7+
`packages/services/service-knowledge` had **no `typecheck` script at all**
8+
its scripts were `build` and `test` — so no tsc program anywhere read this
9+
package. Turbo/CI typecheck lanes skipped it silently, because a zero-matching
10+
filter run exits 0. `tsup` transpiles with esbuild and `vitest` runs through
11+
esbuild type-**stripping**; neither type-checks. The package's own
12+
`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 — the same
14+
shape `@objectstack/service-cluster` (#14181) reached the ledger by.
15+
16+
**Measured before any repair**, dependency closure built first: `tsc --noEmit`
17+
against the existing `tsconfig.json` (undivided, BUILD/NodeNext semantics)
18+
read **10** raw errors, matching the `DEBT` entry this PR deletes exactly. The
19+
new sibling `tsconfig.test.json` (module semantics only — `esnext` / `bundler`
20+
/ `lib: ES2022`, matching how vitest actually executes these files; strictness
21+
untouched) read **4** under the correct split — not the ledger's own 3-code-tier
22+
guess. Fixing the 3 TS2835 (three relative test imports missing `.js`, required
23+
by `moduleResolution: NodeNext`) removed the noise cascade (4 TS7006, every
24+
`(h) => h.documentId)` callback over a `KnowledgeService` search result that
25+
had degraded to `any`) and, in doing so, re-enabled a TypeScript excess-property
26+
check the cascade had been suppressing — uncovering a 4th real error the
27+
undivided reading had masked entirely.
28+
29+
**The four code-tier defects, all in the test file, all in the test file's own
30+
typing — never in `src/`:**
31+
32+
1. `roles: ['member']` in one `ExecutionContext` object literal (TS2353 once
33+
the excess-property check could see it) — a field the spec renamed to
34+
`positions` (`execution-context.zod.ts`: *"Position names held by the
35+
user … Formerly `roles`"*), that no check had ever read against the
36+
renamed type. Every other `executionContext` literal in this file already
37+
used `positions`; this one was simply never checked before. Fixed by
38+
renaming it — `ExecutionContext` itself is untouched and correct.
39+
2. `buildSetup`'s `vi.fn()` stub for `IDataEngine.find` typed its second
40+
parameter as `{ context: { isSystem?: boolean } }`, omitting the `where`
41+
field the real call site (`knowledge-service.ts`'s RLS re-check) actually
42+
passes. `expect(opts.where).toEqual(...)` then read a property TypeScript
43+
correctly said did not exist (TS2339). Fixed by widening the mock's
44+
parameter type to match the call it stubs (`where` and `fields` added),
45+
not by loosening the assertion.
46+
3 & 4. Two more `vi.fn()` mocks (`upsertSpy`/`deleteSpy`/`searchSpy` in
47+
`makeAdapter`, and a `find` mock in the reindex test) had **no** parameter
48+
type at all, so TypeScript inferred a zero-argument implementation and
49+
`.mock.calls[N]` was typed as an array of **empty tuples**. Indexing past
50+
that boundary (`.mock.calls[0][1]`) is a genuine tuple-length error
51+
(TS2493), and casting the resulting `undefined` onward compounded into
52+
TS2352. Fixed the reindex-test `find` mock by typing its parameters to
53+
match the real `reindexSource` call site (`where`, `limit`, `context`);
54+
the `makeAdapter` stubs were reached only through the 3 TS2835 (below) and
55+
needed no change of their own once those were fixed.
56+
57+
**The three TS2835 are repaired directly** (`.js` added to three relative
58+
specifiers in the test files), not routed around by excluding tests from
59+
`tsconfig.json` — which stays exactly as it is, per the family's own rule
60+
(AGENTS.md: never add such an exclusion). Because this package's build config
61+
already includes the tests, its `typecheck` script's own `tsc --noEmit tsconfig.json`
62+
step reads these same files under NodeNext regardless of the new sibling
63+
config, so they needed fixing either way — unlike `service-cluster`, whose test
64+
files already carried the extension and needed no import repair.
65+
66+
Wired by the #14062 / #5286 route: `tsconfig.test.json` named by a new
67+
`typecheck` script through the shared `check:test-typecheck` gate. No
68+
`test-typecheck-debt.json` is added — its **absence is the zero**: the gate
69+
reads a missing ledger as no entries, under which any error in any file here
70+
is immediately red. After the repair, **both** readings (`tsconfig.json` and
71+
`tsconfig.test.json`) are 0.
72+
73+
The package's `DEBT` entry in `scripts/check-type-check-coverage.mjs`
74+
(`errors: 10`) is **deleted**, not lowered — the graduation the ratchet's own
75+
invariant requires. `scripts/check-type-source-resolution.mjs` gains a
76+
registry entry for the three workspace deps (`core`, `objectql`, `spec`) now
77+
reached only through the new `tsconfig.test.json` program (the #11490
78+
onboarding-limb re-baseline, same route `service-cluster` took): `paths` was
79+
measured and rejected — redirecting those three deps to source takes this
80+
package's test layer from 0 errors to 487, all TS6059, all in another
81+
package's source.
82+
83+
No runtime code changes: `src/**` (excluding tests) is byte-identical, so no
84+
shipped behaviour moves. The `patch` level reflects the published
85+
`package.json` gaining `typecheck` / `check:test-typecheck` scripts and a
86+
`tsx` devDependency.

packages/services/service-knowledge/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-knowledge --project tsconfig.test.json"
2426
},
2527
"dependencies": {
2628
"@objectstack/core": "workspace:*",
@@ -29,6 +31,7 @@
2931
"devDependencies": {
3032
"@objectstack/objectql": "workspace:*",
3133
"@types/node": "^26.2.0",
34+
"tsx": "^4.23.12",
3235
"typescript": "^6.0.3",
3336
"vitest": "^4.1.10"
3437
},

packages/services/service-knowledge/src/__tests__/event-sync-data-events.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
import { describe, it, expect, vi } from 'vitest';
1414
import type { RealtimeEventHandler, RealtimeEventPayload } from '@objectstack/spec/contracts';
15-
import { KnowledgeServicePlugin } from '../knowledge-service-plugin';
16-
import type { KnowledgeService } from '../knowledge-service';
15+
import { KnowledgeServicePlugin } from '../knowledge-service-plugin.js';
16+
import type { KnowledgeService } from '../knowledge-service.js';
1717

1818
function makeCtx() {
1919
let readyHook: (() => Promise<void>) | undefined;

packages/services/service-knowledge/src/__tests__/knowledge-service.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { describe, it, expect, vi } from 'vitest';
4-
import { KnowledgeService, documentIdFor, recordToDocument } from '../knowledge-service';
4+
import { KnowledgeService, documentIdFor, recordToDocument } from '../knowledge-service.js';
55
import type {
66
IDataEngine,
77
IKnowledgeAdapter,
@@ -87,7 +87,10 @@ describe('KnowledgeService — adapter & source registry', () => {
8787
describe('KnowledgeService — permission-aware search', () => {
8888
function buildSetup(hits: KnowledgeHit[]) {
8989
const adapter = makeAdapter('memory', hits);
90-
const findSpy = vi.fn(async (_obj: string, opts: { context: { isSystem?: boolean } }) => {
90+
const findSpy = vi.fn(async (
91+
_obj: string,
92+
opts: { where?: Record<string, unknown>; fields?: string[]; context: { isSystem?: boolean } },
93+
) => {
9194
if (opts.context?.isSystem) return [{ id: 'rec_1' }, { id: 'rec_2' }];
9295
return [{ id: 'rec_1' }];
9396
});
@@ -140,7 +143,7 @@ describe('KnowledgeService — permission-aware search', () => {
140143
];
141144
const { svc, findSpy } = buildSetup(hits);
142145
const out = await svc.search('q', {
143-
executionContext: { userId: 'u1', roles: ['member'], permissions: [], isSystem: false },
146+
executionContext: { userId: 'u1', positions: ['member'], permissions: [], isSystem: false },
144147
});
145148
expect(out.map((h) => h.documentId)).toEqual(['d1']);
146149
expect(findSpy).toHaveBeenCalledOnce();
@@ -256,7 +259,10 @@ describe('KnowledgeService — event sync', () => {
256259

257260
describe('KnowledgeService — reindex', () => {
258261
it('object source: walks IDataEngine with isSystem context and pushes docs', async () => {
259-
const find = vi.fn(async () => [
262+
const find = vi.fn(async (
263+
_obj: string,
264+
_opts: { where?: unknown; limit?: number; context: { isSystem?: boolean } },
265+
) => [
260266
{ id: 'r1', title: 'T1', notes: 'N1', status: 'open' },
261267
{ id: 'r2', title: 'T2', notes: 'N2', status: 'done' },
262268
]);
@@ -268,7 +274,7 @@ describe('KnowledgeService — reindex', () => {
268274
expect(res.ok).toBe(true);
269275
expect(res.indexed).toBe(2);
270276
expect(res.discovered).toBe(2);
271-
expect((find.mock.calls[0][1] as { context: { isSystem?: boolean } }).context.isSystem).toBe(true);
277+
expect(find.mock.calls[0][1].context.isSystem).toBe(true);
272278
expect(adapter.upsertSpy).toHaveBeenCalledOnce();
273279
});
274280

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// The TEST-layer type-check program (#15049 — 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`, #13176 to `packages/plugins/
5+
// plugin-security`, and #14181 / PR #15032 to `packages/services/
6+
// service-cluster` — the worked example this file is copied from).
7+
// `tsconfig.json` beside this one stays exactly as it is: it is the BUILD
8+
// config. This sibling puts the test layer in front of tsc under the module
9+
// semantics vitest really executes it with, and `package.json`'s `typecheck`
10+
// script NAMES it (via `check:test-typecheck --project`), because a config no
11+
// script invokes is exactly the phantom this whole change is about.
12+
//
13+
// ⚠️ WHY `service-knowledge`, LIKE `service-cluster` AND UNLIKE `plugin-auth` /
14+
// `plugin-sharing` / `core`: this package's `tsconfig.json` does NOT exclude
15+
// tests (`include: ["src"]`, no `**/*.test.ts` exclusion) and never did, so the
16+
// program that would have read them already existed and was simply never
17+
// invoked -- no `typecheck` script named it. `tsup` type-strips, `vitest`
18+
// type-strips; neither ran tsc.
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`, the
24+
// `packages/plugins/**` family and `service-cluster` 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, workspace closure built first (`tsc --noEmit --pretty false
40+
// --listFiles -p <config>`, and the same command without `--listFiles`), on a
41+
// checkout at merge-base 2cc4610304, BEFORE any repair:
42+
//
43+
// files in this program (test config) 409
44+
// files in tsconfig.json (build config) 441
45+
// own `src/**/*.test.ts` in the program 4
46+
// errors under BUILD semantics (tsconfig.json, undivided) 10
47+
// errors under THIS config (the split) 4
48+
//
49+
// The 10 undivided matched the DEBT entry this PR deletes exactly: 3 TS2835
50+
// (config-tier -- three relative test imports missing `.js`, required by
51+
// `moduleResolution: NodeNext`) + 4 TS7006 (noise -- `KnowledgeService`
52+
// resolving to `any` through the unresolved imports cascades into every
53+
// `(h) => h.documentId)` callback over its return value) + 3 code-tier the
54+
// ledger's note itemised (TS2339/TS2352/TS2493).
55+
//
56+
// The split did NOT confirm the ledger's 3-code-tier guess -- it found 4, the
57+
// same "a tier split read off an unrepaired config is a guess about what is
58+
// UNDER it" lesson this file's sibling ledger note states for `metadata` and
59+
// `service-storage`: stripping the config-tier noise re-enabled an EXCESS
60+
// PROPERTY CHECK that the `any`-typed parameter had been suppressing, and it
61+
// caught a real one -- `roles: ['member']` in one `ExecutionContext` literal,
62+
// stale since the field was renamed to `positions` (`execution-context.zod.ts`
63+
// docs it: "Formerly `roles`"). The other 3 were exactly the ledger's guess:
64+
// two `vi.fn()` mocks stubbing `IDataEngine.find` typed with fewer parameters
65+
// than the real call site they stand in for, so `.mock.calls[N]` indexed past
66+
// a TS-inferred EMPTY tuple (TS2493, plus the `as` cast off it reading TS2352)
67+
// and a third mock's param type omitted the `where` field the real call
68+
// passes (TS2339). All 4 are fixed in the test file, matching each mock's type
69+
// to the call site it stubs -- never widening the mock, never touching
70+
// `ExecutionContext` (which is correct; the test's stale field name was not).
71+
//
72+
// The three TS2835 are ALSO repaired directly (`.js` added to the three
73+
// relative specifiers), because `tsconfig.json` genuinely DOES include the
74+
// tests -- same as `service-cluster` -- so the package's `typecheck` script's
75+
// bare `tsc --noEmit` step reads these same files under NodeNext and needs
76+
// them resolvable regardless of this sibling config; unlike `service-cluster`,
77+
// this package's test files had NOT already been written with the extension.
78+
// After both repairs, BOTH readings are 0 -- see the PR body / changeset for
79+
// the fix-by-fix account.
80+
//
81+
// There is NO `test-typecheck-debt.json` beside this config, and its ABSENCE is
82+
// the zero: `check:test-typecheck` reads a missing ledger as `{ entries: {} }`,
83+
// under which ANY error in ANY file here is red immediately, with no entry to be
84+
// added to. If this package ever acquires residue that cannot be fixed in the
85+
// PR that causes it, THAT is when a ledger and a `gen:test-typecheck-debt`
86+
// script are owed — and adding one is maintainer-only (#5286), exactly as the
87+
// gate says when it refuses.
88+
{
89+
"extends": "./tsconfig.json",
90+
"compilerOptions": {
91+
"noEmit": true,
92+
"module": "esnext",
93+
"moduleResolution": "bundler",
94+
"lib": ["ES2022"]
95+
},
96+
"include": ["src/**/*"],
97+
"exclude": ["node_modules", "dist"]
98+
}

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: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,27 @@ const ROOT_PROGRAM_COUPLED_SCRIPT = 'scripts/check-test-typecheck.mts';
679679
// ran even though that config DOES include the tests. Repaired by the #5286
680680
// route -- a `tsconfig.test.json` over the test layer, named by a new `typecheck`
681681
// script -- so the entry is deleted rather than lowered.
682+
//
683+
// `@objectstack/service-knowledge` GRADUATED from this ledger too (#15049,
684+
// PR #15032's sibling for `packages/services/**`; entry: 10 raw, repaired to
685+
// 0 under BOTH the build config and the new `tsconfig.test.json` split). This
686+
// one is worth a line because the split did NOT confirm this entry's own
687+
// 3-code-tier guess -- it found 4, the same "a tier split read off an
688+
// unrepaired config is a guess about what is UNDER it" lesson the paragraph
689+
// above states for `metadata` and `service-storage`. Fixing the 3 TS2835 (the
690+
// config-tier third, and the noise: the unresolved imports made
691+
// `KnowledgeService` `any`, which suppressed the TypeScript excess-property
692+
// check on an `ExecutionContext` literal) uncovered a 4th real error the
693+
// undivided reading had masked: `roles: ['member']`, a field the spec renamed
694+
// to `positions` (`execution-context.zod.ts`: "Formerly `roles`") that no
695+
// check had ever read with the renamed type. The other 3 code-tier errors
696+
// were exactly this entry's guess: two `vi.fn()` mocks stubbing
697+
// `IDataEngine.find` typed with fewer parameters than the call site they
698+
// stand in for, so `.mock.calls[N]` indexed past a TS-inferred EMPTY tuple
699+
// (TS2493/TS2352), and a third mock's parameter type omitted the `where`
700+
// field the real call passes (TS2339). All 4 are fixed in the test file,
701+
// matching each mock's type to the call site it stubs; `ExecutionContext`
702+
// itself was not touched (it was correct -- the test's field name was stale).
682703
const DEBT = {
683704
'@objectstack/cloud-connection': {
684705
errors: 13,
@@ -706,12 +727,6 @@ const DEBT = {
706727
+ 'by acquiring a second file, then 5 -> 3 by graduating the first -- so re-read what the pile is '
707728
+ 'made of before sizing it, never just the number.',
708729
},
709-
'@objectstack/service-knowledge': {
710-
errors: 10,
711-
note: 'code-tier 3 (TS2339/TS2352/TS2493); config-tier 3 (TS2835); noise 4 (TS7006). Re-measured 10 at '
712-
+ '5ab08428, up from 8; code-tier is unchanged at 3, so the +2 is config-tier/noise. 8 of the 10 are '
713-
+ 'in __tests__/knowledge-service.test.ts.',
714-
},
715730
'@objectstack/service-storage': {
716731
errors: 51,
717732
note: 'code-tier 8 (TS2339 x4, TS2347 x4); config-tier 26 (TS2835 x23, TS2550 x3); noise 17 '

0 commit comments

Comments
 (0)