Skip to content

Commit 63e0803

Browse files
authored
Merge branch 'main' into claude/issue-14595-sweep-step6-run-record
2 parents 21ba6f2 + 44ffa21 commit 63e0803

25 files changed

Lines changed: 446 additions & 118 deletions
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@objectstack/core": patch
3+
---
4+
5+
feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)
6+
7+
`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
8+
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
9+
selects only packages that declare the task — so the lint workflow's typecheck
10+
job had no way to reach this package, and `pnpm --filter @objectstack/core
11+
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
12+
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
13+
and rest, runtime, mcp, services and plugins all import it.
14+
15+
The state was tracked but not runnable. `check:type-check-coverage` carried
16+
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
17+
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
18+
not something a contributor working in the package can run, which is how a
19+
dispatched task came to assume the script existed.
20+
21+
**Measured at `84b8190ae`, dependency closure built first.** The undivided
22+
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
23+
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
24+
same program restricted to the 63 non-test source files reports **zero**. So the
25+
build layer graduated as it stood, and the 98 did not have to be repaired before
26+
the script could exist.
27+
28+
**94 of the 98 were the check, not the code.** The repair is the split this
29+
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
30+
stays the build config and excludes the test layer; a new `tsconfig.test.json`
31+
compiles that layer under the module semantics vitest actually executes it with
32+
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
33+
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
34+
that does not resolve makes every symbol it names `any`. **No test file was
35+
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
36+
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
37+
shrink-only.
38+
39+
**The `examples/` half was found by the new script, not by the card.** Declaring
40+
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
41+
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
42+
`packages/core/examples` — 2 non-test source files in no tsc program at all.
43+
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
44+
(above the package root, never existed) and `phase2-integration.ts` imported
45+
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
46+
no dependency block. Collapsing that cascade exposed rather than removed errors,
47+
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
48+
**private** `logger`; four members of the security scan result that do not exist
49+
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
50+
`status` and per-severity counts); and two config literals passing the unparsed
51+
shapes where `PluginHealthMonitor.registerPlugin` and
52+
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
53+
pair is retirement drift — this file was edited by two retirements (restart keys,
54+
`watchPatterns`) while no tsc program could check the result. Every correction is
55+
pinned to this package's own signatures; `packages/spec` was not touched.
56+
57+
`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
58+
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.

packages/core/examples/kernel-features-example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
PluginMetadata,
2222
ServiceLifecycle,
2323
PluginContext
24-
} from '../index.js';
24+
} from '../src/index.js';
2525

2626
// ============================================================================
2727
// Example 1: Database Plugin with Health Checks
@@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = {
4949
ctx.logger.info('Disconnecting from database...');
5050
this.connected = false;
5151
},
52-
async query(sql: string) {
52+
async query(_sql: string) {
5353
if (!this.connected) {
5454
throw new Error('Database not connected');
5555
}

packages/core/examples/phase2-integration.ts

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,34 @@ import {
1414
ObjectKernel,
1515
PluginHealthMonitor,
1616
HotReloadManager,
17-
DependencyResolver,
1817
PluginPermissionManager,
1918
PluginSandboxRuntime,
20-
PluginSecurityScanner
21-
} from '@objectstack/core';
22-
23-
import type { Plugin } from '@objectstack/core';
19+
PluginSecurityScanner,
20+
createLogger
21+
} from '../src/index.js';
22+
23+
import type { Plugin, ObjectLogger } from '../src/index.js';
24+
// [#14613] The PARSED variants, because that is what the methods below take:
25+
// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed`
26+
// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed`
27+
// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every
28+
// key optional, so passing them was a real type error this file carried while no
29+
// tsc program read it.
2430
import type {
25-
PluginHealthCheck,
26-
HotReloadConfig,
27-
PermissionSet,
31+
PluginHealthCheckParsed,
32+
HotReloadConfigParsed,
33+
PluginPermissionSet,
2834
SandboxConfig
29-
} from '@objectstack/spec/system';
35+
} from '@objectstack/spec/kernel';
3036

3137
/**
3238
* Example: Enterprise Plugin Platform with Phase 2 Features
3339
*/
3440
export class EnterprisePluginPlatform {
3541
private kernel: ObjectKernel;
42+
private logger: ObjectLogger;
3643
private healthMonitor: PluginHealthMonitor;
3744
private hotReload: HotReloadManager;
38-
private depResolver: DependencyResolver;
3945
private permManager: PluginPermissionManager;
4046
private sandbox: PluginSandboxRuntime;
4147
private scanner: PluginSecurityScanner;
@@ -49,13 +55,18 @@ export class EnterprisePluginPlatform {
4955
},
5056
});
5157

58+
// [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so
59+
// every one of the 20 reads this file made of it was a type error --
60+
// invisible until this package declared a `typecheck` script, because no
61+
// tsc program had ever compiled this directory.
62+
this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' });
63+
5264
// Initialize Phase 2 components
53-
this.healthMonitor = new PluginHealthMonitor(this.kernel.logger);
54-
this.hotReload = new HotReloadManager(this.kernel.logger);
55-
this.depResolver = new DependencyResolver(this.kernel.logger);
56-
this.permManager = new PluginPermissionManager(this.kernel.logger);
57-
this.sandbox = new PluginSandboxRuntime(this.kernel.logger);
58-
this.scanner = new PluginSecurityScanner(this.kernel.logger);
65+
this.healthMonitor = new PluginHealthMonitor(this.logger);
66+
this.hotReload = new HotReloadManager(this.logger);
67+
this.permManager = new PluginPermissionManager(this.logger);
68+
this.sandbox = new PluginSandboxRuntime(this.logger);
69+
this.scanner = new PluginSecurityScanner(this.logger);
5970
}
6071

6172
/**
@@ -64,38 +75,42 @@ export class EnterprisePluginPlatform {
6475
async installPlugin(
6576
plugin: Plugin,
6677
config: {
67-
health?: PluginHealthCheck;
68-
hotReload?: HotReloadConfig;
69-
permissions?: PermissionSet;
78+
health?: PluginHealthCheckParsed;
79+
hotReload?: HotReloadConfigParsed;
80+
permissions?: PluginPermissionSet;
7081
sandbox?: SandboxConfig;
7182
securityScan?: boolean;
7283
}
7384
): Promise<void> {
7485
const pluginName = plugin.name;
7586
const pluginVersion = plugin.version || '1.0.0';
7687

77-
this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
88+
this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
7889

7990
// Step 1: Security Scan
8091
if (config.securityScan !== false) {
81-
this.kernel.logger.info('Running security scan...');
92+
this.logger.info('Running security scan...');
8293

8394
const scanResult = await this.scanner.scan({
8495
pluginId: pluginName,
8596
version: pluginVersion,
8697
// In real implementation, would provide actual files and dependencies
8798
});
8899

89-
if (!scanResult.passed) {
100+
// [#14613] `KernelSecurityScanResult` carries `status` and per-severity
101+
// COUNTS; it has never had `passed`, `score`, `summary.critical` or
102+
// `summary.high`. This block read four members that do not exist.
103+
if (scanResult.status !== 'passed') {
90104
throw new Error(
91-
`Security scan failed: Score ${scanResult.score}/100, ` +
92-
`Critical: ${scanResult.summary.critical}, ` +
93-
`High: ${scanResult.summary.high}`
105+
`Security scan ${scanResult.status}: ` +
106+
`${scanResult.summary.totalVulnerabilities} vulnerability(ies), ` +
107+
`Critical: ${scanResult.summary.criticalCount}, ` +
108+
`High: ${scanResult.summary.highCount}`
94109
);
95110
}
96111

97-
this.kernel.logger.info(
98-
`Security scan passed: ${scanResult.score}/100`
112+
this.logger.info(
113+
`Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)`
99114
);
100115
}
101116

@@ -106,37 +121,37 @@ export class EnterprisePluginPlatform {
106121
// Auto-grant all permissions (in production, would prompt user)
107122
this.permManager.grantAllPermissions(pluginName, 'system');
108123

109-
this.kernel.logger.info(
124+
this.logger.info(
110125
`Permissions registered: ${config.permissions.permissions.length} permissions`
111126
);
112127
}
113128

114129
// Step 3: Create Sandbox
115130
if (config.sandbox) {
116131
this.sandbox.createSandbox(pluginName, config.sandbox);
117-
this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`);
132+
this.logger.info(`Sandbox created: ${config.sandbox.level} level`);
118133
}
119134

120135
// Step 4: Register for Health Monitoring
121136
if (config.health) {
122137
this.healthMonitor.registerPlugin(pluginName, config.health);
123-
this.kernel.logger.info(
138+
this.logger.info(
124139
`Health monitoring configured: ${config.health.interval}ms interval`
125140
);
126141
}
127142

128143
// Step 5: Register for Hot Reload
129144
if (config.hotReload) {
130145
this.hotReload.registerPlugin(pluginName, config.hotReload);
131-
this.kernel.logger.info(
146+
this.logger.info(
132147
`Hot reload enabled: ${config.hotReload.stateStrategy} state strategy`
133148
);
134149
}
135150

136151
// Step 6: Register with Kernel
137152
this.kernel.use(plugin);
138153

139-
this.kernel.logger.info(`Plugin ${pluginName} installed successfully`);
154+
this.logger.info(`Plugin ${pluginName} installed successfully`);
140155
}
141156

142157
/**
@@ -153,14 +168,14 @@ export class EnterprisePluginPlatform {
153168
}
154169
}
155170

156-
this.kernel.logger.info('Platform started successfully');
171+
this.logger.info('Platform started successfully');
157172
}
158173

159174
/**
160175
* Shutdown the platform
161176
*/
162177
async shutdown(): Promise<void> {
163-
this.kernel.logger.info('Shutting down platform...');
178+
this.logger.info('Shutting down platform...');
164179

165180
// Stop health monitoring
166181
this.healthMonitor.shutdown();
@@ -171,7 +186,7 @@ export class EnterprisePluginPlatform {
171186
// Shutdown kernel
172187
await this.kernel.shutdown();
173188

174-
this.kernel.logger.info('Platform shutdown complete');
189+
this.logger.info('Platform shutdown complete');
175190
}
176191

177192
/**
@@ -203,7 +218,7 @@ export class EnterprisePluginPlatform {
203218
* Perform hot reload of a plugin
204219
*/
205220
async reloadPlugin(pluginName: string): Promise<void> {
206-
this.kernel.logger.info(`Hot reloading plugin: ${pluginName}`);
221+
this.logger.info(`Hot reloading plugin: ${pluginName}`);
207222

208223
const plugin = this.kernel['plugins'].get(pluginName);
209224
if (!plugin) {
@@ -218,7 +233,7 @@ export class EnterprisePluginPlatform {
218233

219234
// Restore state (simplified - would need plugin cooperation)
220235
const restoreState = (state: Record<string, any>) => {
221-
this.kernel.logger.info(`Restoring state from ${new Date(state.timestamp)}`);
236+
this.logger.info(`Restoring state from ${new Date(state.timestamp)}`);
222237
// ... restore plugin state
223238
};
224239

@@ -230,7 +245,7 @@ export class EnterprisePluginPlatform {
230245
restoreState
231246
);
232247

233-
this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`);
248+
this.logger.info(`Plugin ${pluginName} reloaded successfully`);
234249
}
235250
}
236251

packages/core/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
},
2121
"scripts": {
2222
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
23+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
24+
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
25+
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",
2326
"test": "vitest run",
2427
"test:watch": "vitest"
2528
},
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"_comment": "Per-file tsc error debt of the @objectstack/core TEST layer (#5286). `tsconfig.test.json` compiles `src/**/*.test.ts` — which `tsconfig.json` excludes and therefore no gate ever read — and every file below still carries errors from before that gate existed. THIS FIELD IS GENERATED: every regeneration rewrites it from scripts/check-test-typecheck.mts, and the EXACT ratchet below requires a regeneration on every repair — so an edit made here is gone by the next one. Anything true of THIS package goes in the sibling `_note` field, which is authored, is preserved verbatim, and is never written by the generator (#12624). This comment states NO cause for the errors, deliberately: the classes differ per package and per file, they move as the debt is paid down, and a cause written here is rewritten verbatim into every ledger by every regeneration — so it outlives its own repair and cannot be corrected in the file where it is read. Measure instead, before repairing anything: `tsc --noEmit --pretty false -p tsconfig.test.json` in the package prints the real classes with their TS codes. Each entry maps a file to its per-SIGNATURE error counts, never to a bare total (#13470): a signature is the TS code plus the diagnostic message with structural type blobs collapsed, and it carries NO line or column — so the pin survives edits that move code around, and only stops matching when the error itself becomes a different error. EXACT ratchet, judged by re-running tsc: a file that gains errors is red, a file that loses them is red until its number is re-recorded, a file that reaches zero is red until its entry is deleted, a signature that ARRIVES or VANISHES is red even when the file total is unchanged, and a file NOT listed here may have no errors at all. Regenerate with: pnpm --filter @objectstack/core gen:test-typecheck-debt",
3+
"_note": "OPENED AT 4, NOT AT 98, and the difference is the CHECK rather than any repair to a test file (#14613). This package declared NO typecheck script at all until this ledger existed, so `turbo run typecheck` -- which selects only packages declaring the task -- could not reach it; the state was tracked, as a `check:type-check-coverage` DEBT entry of 98 re-measured once from 91, but nothing a contributor could RUN reported it, which is how a dispatched task came to assume `pnpm --filter @objectstack/core typecheck` existed. Measured at 84b8190ae with the dependency closure built: the undivided program (`tsc --noEmit -p tsconfig.json`, tests included, as the DEBT entry measured it) reports 98 errors over 12 files, all 12 of them `.test.ts`; the same program over only the 63 non-test source files reports ZERO; and THIS program -- the same 48 test files under vitest's own module semantics -- reports 4. So 94 of the 98 were the build config's NodeNext judging vitest-executed ESM (TS2835 x22 and the TS2347 beside them, plus the share of TS7006 x71 they cause: an import that does not resolve makes every symbol it names `any`). Not one test file was edited to retire them. That is the `check-type-check-coverage` header's own discipline applied literally -- fix the config first, then read the residue -- and it is why the 98 in that ledger was an upper bound on nothing. WHAT THE 4 ACTUALLY ARE, deliberately left ledgered rather than repaired here. Two are ONE defect twice over: `src/plugin-loader.test.ts` (TS2352) and `src/security/plugin-permission-enforcer.test.ts` (TS2739) each build a mock PluginContext literal missing `registerServiceFactory`, `replaceService` and `getServiceScoped`. ⚠️ That is the SAME shape as the 30 x TS2345 the DEBT entry for `@objectstack/metadata` records ('every one the same mock PluginContext literal missing registerServiceFactory and getServiceScoped'), and that package's repair is in flight on its own card -- so the shared fixture those two want should be authored ONCE, by whoever closes that, rather than twice in parallel. Repairing them here would have raced it. The third, `src/utils/filter-tokens.test.ts` (TS2352), is a genuine question about a signature and not a fixture typo: a `$and` array of single-key literals is asserted into `Record<string, string>[]`, and the union's absent keys are `undefined`, which no index signature of `string` admits -- repairing it means deciding whether the test's intent or the parameter's type is the wrong one. Only the fourth, `src/utils/migration-journal.test.ts` (TS6133, an unread `rows`), is mechanical, and a lone mechanical fix beside three judgement calls buys nothing while making the diff that opens this gate harder to read. ⛔ None of the 4 is a reason to widen `exclude` in `tsconfig.test.json`: the whole point of the split beside it is that the strictness flags are INHERITED and untouched. Each is red on the PR that changes it, and the ratchet is EXACT in both directions -- a file that loses its error is red until re-recorded, and reaching zero here means deleting the entry, not lowering a number.",
4+
"entries": {
5+
"src/plugin-loader.test.ts": {
6+
"TS2352: Conversion of type '…' to type 'PluginContext' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.": 1
7+
},
8+
"src/security/plugin-permission-enforcer.test.ts": {
9+
"TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1
10+
},
11+
"src/utils/filter-tokens.test.ts": {
12+
"TS2352: Conversion of type '…' to type '…' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.": 1
13+
},
14+
"src/utils/migration-journal.test.ts": {
15+
"TS6133: 'rows' is declared but its value is never read.": 1
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)