Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .changeset/core-typecheck-script.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
---
"@objectstack/core": patch
---

feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)

`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
selects only packages that declare the task — so the lint workflow's typecheck
job had no way to reach this package, and `pnpm --filter @objectstack/core
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
and rest, runtime, mcp, services and plugins all import it.

The state was tracked but not runnable. `check:type-check-coverage` carried
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
not something a contributor working in the package can run, which is how a
dispatched task came to assume the script existed.

**Measured at `84b8190ae`, dependency closure built first.** The undivided
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
same program restricted to the 63 non-test source files reports **zero**. So the
build layer graduated as it stood, and the 98 did not have to be repaired before
the script could exist.

**94 of the 98 were the check, not the code.** The repair is the split this
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
stays the build config and excludes the test layer; a new `tsconfig.test.json`
compiles that layer under the module semantics vitest actually executes it with
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
that does not resolve makes every symbol it names `any`. **No test file was
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
shrink-only.

**The `examples/` half was found by the new script, not by the card.** Declaring
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
`packages/core/examples` — 2 non-test source files in no tsc program at all.
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
(above the package root, never existed) and `phase2-integration.ts` imported
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
no dependency block. Collapsing that cascade exposed rather than removed errors,
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
**private** `logger`; four members of the security scan result that do not exist
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
`status` and per-severity counts); and two config literals passing the unparsed
shapes where `PluginHealthMonitor.registerPlugin` and
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
pair is retirement drift — this file was edited by two retirements (restart keys,
`watchPatterns`) while no tsc program could check the result. Every correction is
pinned to this package's own signatures; `packages/spec` was not touched.

`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
4 changes: 2 additions & 2 deletions packages/core/examples/kernel-features-example.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ import {
PluginMetadata,
ServiceLifecycle,
PluginContext
} from '../index.js';
} from '../src/index.js';

// ============================================================================
// Example 1: Database Plugin with Health Checks
Expand DownExpand Up@@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = {
ctx.logger.info('Disconnecting from database...');
this.connected = false;
},
async query(sql: string) {
async query(_sql: string) {
if (!this.connected) {
throw new Error('Database not connected');
}
Expand Down
91 changes: 53 additions & 38 deletions packages/core/examples/phase2-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,28 +14,34 @@ import {
ObjectKernel,
PluginHealthMonitor,
HotReloadManager,
DependencyResolver,
PluginPermissionManager,
PluginSandboxRuntime,
PluginSecurityScanner
} from '@objectstack/core';

import type { Plugin } from '@objectstack/core';
PluginSecurityScanner,
createLogger
} from '../src/index.js';

import type { Plugin, ObjectLogger } from '../src/index.js';
// [#14613] The PARSED variants, because that is what the methods below take:
// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed`
// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed`
// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every
// key optional, so passing them was a real type error this file carried while no
// tsc program read it.
import type {
PluginHealthCheck,
HotReloadConfig,
PermissionSet,
PluginHealthCheckParsed,
HotReloadConfigParsed,
PluginPermissionSet,
SandboxConfig
} from '@objectstack/spec/system';
} from '@objectstack/spec/kernel';

/**
* Example: Enterprise Plugin Platform with Phase 2 Features
*/
export class EnterprisePluginPlatform {
private kernel: ObjectKernel;
private logger: ObjectLogger;
private healthMonitor: PluginHealthMonitor;
private hotReload: HotReloadManager;
private depResolver: DependencyResolver;
private permManager: PluginPermissionManager;
private sandbox: PluginSandboxRuntime;
private scanner: PluginSecurityScanner;
Expand All@@ -49,13 +55,18 @@ export class EnterprisePluginPlatform {
},
});

// [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so
// every one of the 20 reads this file made of it was a type error --
// invisible until this package declared a `typecheck` script, because no
// tsc program had ever compiled this directory.
this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' });

// Initialize Phase 2 components
this.healthMonitor = new PluginHealthMonitor(this.kernel.logger);
this.hotReload = new HotReloadManager(this.kernel.logger);
this.depResolver = new DependencyResolver(this.kernel.logger);
this.permManager = new PluginPermissionManager(this.kernel.logger);
this.sandbox = new PluginSandboxRuntime(this.kernel.logger);
this.scanner = new PluginSecurityScanner(this.kernel.logger);
this.healthMonitor = new PluginHealthMonitor(this.logger);
this.hotReload = new HotReloadManager(this.logger);
this.permManager = new PluginPermissionManager(this.logger);
this.sandbox = new PluginSandboxRuntime(this.logger);
this.scanner = new PluginSecurityScanner(this.logger);
}

/**
Expand All@@ -64,38 +75,42 @@ export class EnterprisePluginPlatform {
async installPlugin(
plugin: Plugin,
config: {
health?: PluginHealthCheck;
hotReload?: HotReloadConfig;
permissions?: PermissionSet;
health?: PluginHealthCheckParsed;
hotReload?: HotReloadConfigParsed;
permissions?: PluginPermissionSet;
sandbox?: SandboxConfig;
securityScan?: boolean;
}
): Promise<void> {
const pluginName = plugin.name;
const pluginVersion = plugin.version || '1.0.0';

this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);

// Step 1: Security Scan
if (config.securityScan !== false) {
this.kernel.logger.info('Running security scan...');
this.logger.info('Running security scan...');

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

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

this.kernel.logger.info(
`Security scan passed: ${scanResult.score}/100`
this.logger.info(
`Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)`
);
}

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

this.kernel.logger.info(
this.logger.info(
`Permissions registered: ${config.permissions.permissions.length} permissions`
);
}

// Step 3: Create Sandbox
if (config.sandbox) {
this.sandbox.createSandbox(pluginName, config.sandbox);
this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`);
this.logger.info(`Sandbox created: ${config.sandbox.level} level`);
}

// Step 4: Register for Health Monitoring
if (config.health) {
this.healthMonitor.registerPlugin(pluginName, config.health);
this.kernel.logger.info(
this.logger.info(
`Health monitoring configured: ${config.health.interval}ms interval`
);
}

// Step 5: Register for Hot Reload
if (config.hotReload) {
this.hotReload.registerPlugin(pluginName, config.hotReload);
this.kernel.logger.info(
this.logger.info(
`Hot reload enabled: ${config.hotReload.stateStrategy} state strategy`
);
}

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

this.kernel.logger.info(`Plugin ${pluginName} installed successfully`);
this.logger.info(`Plugin ${pluginName} installed successfully`);
}

/**
Expand All@@ -153,14 +168,14 @@ export class EnterprisePluginPlatform {
}
}

this.kernel.logger.info('Platform started successfully');
this.logger.info('Platform started successfully');
}

/**
* Shutdown the platform
*/
async shutdown(): Promise<void> {
this.kernel.logger.info('Shutting down platform...');
this.logger.info('Shutting down platform...');

// Stop health monitoring
this.healthMonitor.shutdown();
Expand All@@ -171,7 +186,7 @@ export class EnterprisePluginPlatform {
// Shutdown kernel
await this.kernel.shutdown();

this.kernel.logger.info('Platform shutdown complete');
this.logger.info('Platform shutdown complete');
}

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

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

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

Expand All@@ -230,7 +245,7 @@ export class EnterprisePluginPlatform {
restoreState
);

this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`);
this.logger.info(`Plugin ${pluginName} reloaded successfully`);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,9 @@
},
"scripts": {
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test-typecheck-debt.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
{
"_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",
"_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.",
"entries": {
"src/plugin-loader.test.ts": {
"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
},
"src/security/plugin-permission-enforcer.test.ts": {
"TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1
},
"src/utils/filter-tokens.test.ts": {
"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
},
"src/utils/migration-journal.test.ts": {
"TS6133: 'rows' is declared but its value is never read.": 1
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .changeset/core-typecheck-script.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
---
"@objectstack/core": patch
---

feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)

`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
selects only packages that declare the task — so the lint workflow's typecheck
job had no way to reach this package, and `pnpm --filter @objectstack/core
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
and rest, runtime, mcp, services and plugins all import it.

The state was tracked but not runnable. `check:type-check-coverage` carried
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
not something a contributor working in the package can run, which is how a
dispatched task came to assume the script existed.

**Measured at `84b8190ae`, dependency closure built first.** The undivided
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
same program restricted to the 63 non-test source files reports **zero**. So the
build layer graduated as it stood, and the 98 did not have to be repaired before
the script could exist.

**94 of the 98 were the check, not the code.** The repair is the split this
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
stays the build config and excludes the test layer; a new `tsconfig.test.json`
compiles that layer under the module semantics vitest actually executes it with
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
that does not resolve makes every symbol it names `any`. **No test file was
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
shrink-only.

**The `examples/` half was found by the new script, not by the card.** Declaring
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
`packages/core/examples` — 2 non-test source files in no tsc program at all.
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
(above the package root, never existed) and `phase2-integration.ts` imported
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
no dependency block. Collapsing that cascade exposed rather than removed errors,
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
**private** `logger`; four members of the security scan result that do not exist
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
`status` and per-severity counts); and two config literals passing the unparsed
shapes where `PluginHealthMonitor.registerPlugin` and
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
pair is retirement drift — this file was edited by two retirements (restart keys,
`watchPatterns`) while no tsc program could check the result. Every correction is
pinned to this package's own signatures; `packages/spec` was not touched.

`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
4 changes: 2 additions & 2 deletions packages/core/examples/kernel-features-example.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ import {
PluginMetadata,
ServiceLifecycle,
PluginContext
} from '../index.js';
} from '../src/index.js';

// ============================================================================
// Example 1: Database Plugin with Health Checks
Expand DownExpand Up@@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = {
ctx.logger.info('Disconnecting from database...');
this.connected = false;
},
async query(sql: string) {
async query(_sql: string) {
if (!this.connected) {
throw new Error('Database not connected');
}
Expand Down
91 changes: 53 additions & 38 deletions packages/core/examples/phase2-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,28 +14,34 @@ import {
ObjectKernel,
PluginHealthMonitor,
HotReloadManager,
DependencyResolver,
PluginPermissionManager,
PluginSandboxRuntime,
PluginSecurityScanner
} from '@objectstack/core';

import type { Plugin } from '@objectstack/core';
PluginSecurityScanner,
createLogger
} from '../src/index.js';

import type { Plugin, ObjectLogger } from '../src/index.js';
// [#14613] The PARSED variants, because that is what the methods below take:
// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed`
// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed`
// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every
// key optional, so passing them was a real type error this file carried while no
// tsc program read it.
import type {
PluginHealthCheck,
HotReloadConfig,
PermissionSet,
PluginHealthCheckParsed,
HotReloadConfigParsed,
PluginPermissionSet,
SandboxConfig
} from '@objectstack/spec/system';
} from '@objectstack/spec/kernel';

/**
* Example: Enterprise Plugin Platform with Phase 2 Features
*/
export class EnterprisePluginPlatform {
private kernel: ObjectKernel;
private logger: ObjectLogger;
private healthMonitor: PluginHealthMonitor;
private hotReload: HotReloadManager;
private depResolver: DependencyResolver;
private permManager: PluginPermissionManager;
private sandbox: PluginSandboxRuntime;
private scanner: PluginSecurityScanner;
Expand All@@ -49,13 +55,18 @@ export class EnterprisePluginPlatform {
},
});

// [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so
// every one of the 20 reads this file made of it was a type error --
// invisible until this package declared a `typecheck` script, because no
// tsc program had ever compiled this directory.
this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' });

// Initialize Phase 2 components
this.healthMonitor = new PluginHealthMonitor(this.kernel.logger);
this.hotReload = new HotReloadManager(this.kernel.logger);
this.depResolver = new DependencyResolver(this.kernel.logger);
this.permManager = new PluginPermissionManager(this.kernel.logger);
this.sandbox = new PluginSandboxRuntime(this.kernel.logger);
this.scanner = new PluginSecurityScanner(this.kernel.logger);
this.healthMonitor = new PluginHealthMonitor(this.logger);
this.hotReload = new HotReloadManager(this.logger);
this.permManager = new PluginPermissionManager(this.logger);
this.sandbox = new PluginSandboxRuntime(this.logger);
this.scanner = new PluginSecurityScanner(this.logger);
}

/**
Expand All@@ -64,38 +75,42 @@ export class EnterprisePluginPlatform {
async installPlugin(
plugin: Plugin,
config: {
health?: PluginHealthCheck;
hotReload?: HotReloadConfig;
permissions?: PermissionSet;
health?: PluginHealthCheckParsed;
hotReload?: HotReloadConfigParsed;
permissions?: PluginPermissionSet;
sandbox?: SandboxConfig;
securityScan?: boolean;
}
): Promise<void> {
const pluginName = plugin.name;
const pluginVersion = plugin.version || '1.0.0';

this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);

// Step 1: Security Scan
if (config.securityScan !== false) {
this.kernel.logger.info('Running security scan...');
this.logger.info('Running security scan...');

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

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

this.kernel.logger.info(
`Security scan passed: ${scanResult.score}/100`
this.logger.info(
`Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)`
);
}

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

this.kernel.logger.info(
this.logger.info(
`Permissions registered: ${config.permissions.permissions.length} permissions`
);
}

// Step 3: Create Sandbox
if (config.sandbox) {
this.sandbox.createSandbox(pluginName, config.sandbox);
this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`);
this.logger.info(`Sandbox created: ${config.sandbox.level} level`);
}

// Step 4: Register for Health Monitoring
if (config.health) {
this.healthMonitor.registerPlugin(pluginName, config.health);
this.kernel.logger.info(
this.logger.info(
`Health monitoring configured: ${config.health.interval}ms interval`
);
}

// Step 5: Register for Hot Reload
if (config.hotReload) {
this.hotReload.registerPlugin(pluginName, config.hotReload);
this.kernel.logger.info(
this.logger.info(
`Hot reload enabled: ${config.hotReload.stateStrategy} state strategy`
);
}

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

this.kernel.logger.info(`Plugin ${pluginName} installed successfully`);
this.logger.info(`Plugin ${pluginName} installed successfully`);
}

/**
Expand All@@ -153,14 +168,14 @@ export class EnterprisePluginPlatform {
}
}

this.kernel.logger.info('Platform started successfully');
this.logger.info('Platform started successfully');
}

/**
* Shutdown the platform
*/
async shutdown(): Promise<void> {
this.kernel.logger.info('Shutting down platform...');
this.logger.info('Shutting down platform...');

// Stop health monitoring
this.healthMonitor.shutdown();
Expand All@@ -171,7 +186,7 @@ export class EnterprisePluginPlatform {
// Shutdown kernel
await this.kernel.shutdown();

this.kernel.logger.info('Platform shutdown complete');
this.logger.info('Platform shutdown complete');
}

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

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

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

Expand All@@ -230,7 +245,7 @@ export class EnterprisePluginPlatform {
restoreState
);

this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`);
this.logger.info(`Plugin ${pluginName} reloaded successfully`);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,9 @@
},
"scripts": {
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test-typecheck-debt.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
{
"_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",
"_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.",
"entries": {
"src/plugin-loader.test.ts": {
"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
},
"src/security/plugin-permission-enforcer.test.ts": {
"TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1
},
"src/utils/filter-tokens.test.ts": {
"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
},
"src/utils/migration-journal.test.ts": {
"TS6133: 'rows' is declared but its value is never read.": 1
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .changeset/core-typecheck-script.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
---
"@objectstack/core": patch
---

feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)

`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
selects only packages that declare the task — so the lint workflow's typecheck
job had no way to reach this package, and `pnpm --filter @objectstack/core
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
and rest, runtime, mcp, services and plugins all import it.

The state was tracked but not runnable. `check:type-check-coverage` carried
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
not something a contributor working in the package can run, which is how a
dispatched task came to assume the script existed.

**Measured at `84b8190ae`, dependency closure built first.** The undivided
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
same program restricted to the 63 non-test source files reports **zero**. So the
build layer graduated as it stood, and the 98 did not have to be repaired before
the script could exist.

**94 of the 98 were the check, not the code.** The repair is the split this
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
stays the build config and excludes the test layer; a new `tsconfig.test.json`
compiles that layer under the module semantics vitest actually executes it with
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
that does not resolve makes every symbol it names `any`. **No test file was
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
shrink-only.

**The `examples/` half was found by the new script, not by the card.** Declaring
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
`packages/core/examples` — 2 non-test source files in no tsc program at all.
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
(above the package root, never existed) and `phase2-integration.ts` imported
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
no dependency block. Collapsing that cascade exposed rather than removed errors,
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
**private** `logger`; four members of the security scan result that do not exist
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
`status` and per-severity counts); and two config literals passing the unparsed
shapes where `PluginHealthMonitor.registerPlugin` and
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
pair is retirement drift — this file was edited by two retirements (restart keys,
`watchPatterns`) while no tsc program could check the result. Every correction is
pinned to this package's own signatures; `packages/spec` was not touched.

`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
4 changes: 2 additions & 2 deletions packages/core/examples/kernel-features-example.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ import {
PluginMetadata,
ServiceLifecycle,
PluginContext
} from '../index.js';
} from '../src/index.js';

// ============================================================================
// Example 1: Database Plugin with Health Checks
Expand DownExpand Up@@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = {
ctx.logger.info('Disconnecting from database...');
this.connected = false;
},
async query(sql: string) {
async query(_sql: string) {
if (!this.connected) {
throw new Error('Database not connected');
}
Expand Down
91 changes: 53 additions & 38 deletions packages/core/examples/phase2-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,28 +14,34 @@ import {
ObjectKernel,
PluginHealthMonitor,
HotReloadManager,
DependencyResolver,
PluginPermissionManager,
PluginSandboxRuntime,
PluginSecurityScanner
} from '@objectstack/core';

import type { Plugin } from '@objectstack/core';
PluginSecurityScanner,
createLogger
} from '../src/index.js';

import type { Plugin, ObjectLogger } from '../src/index.js';
// [#14613] The PARSED variants, because that is what the methods below take:
// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed`
// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed`
// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every
// key optional, so passing them was a real type error this file carried while no
// tsc program read it.
import type {
PluginHealthCheck,
HotReloadConfig,
PermissionSet,
PluginHealthCheckParsed,
HotReloadConfigParsed,
PluginPermissionSet,
SandboxConfig
} from '@objectstack/spec/system';
} from '@objectstack/spec/kernel';

/**
* Example: Enterprise Plugin Platform with Phase 2 Features
*/
export class EnterprisePluginPlatform {
private kernel: ObjectKernel;
private logger: ObjectLogger;
private healthMonitor: PluginHealthMonitor;
private hotReload: HotReloadManager;
private depResolver: DependencyResolver;
private permManager: PluginPermissionManager;
private sandbox: PluginSandboxRuntime;
private scanner: PluginSecurityScanner;
Expand All@@ -49,13 +55,18 @@ export class EnterprisePluginPlatform {
},
});

// [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so
// every one of the 20 reads this file made of it was a type error --
// invisible until this package declared a `typecheck` script, because no
// tsc program had ever compiled this directory.
this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' });

// Initialize Phase 2 components
this.healthMonitor = new PluginHealthMonitor(this.kernel.logger);
this.hotReload = new HotReloadManager(this.kernel.logger);
this.depResolver = new DependencyResolver(this.kernel.logger);
this.permManager = new PluginPermissionManager(this.kernel.logger);
this.sandbox = new PluginSandboxRuntime(this.kernel.logger);
this.scanner = new PluginSecurityScanner(this.kernel.logger);
this.healthMonitor = new PluginHealthMonitor(this.logger);
this.hotReload = new HotReloadManager(this.logger);
this.permManager = new PluginPermissionManager(this.logger);
this.sandbox = new PluginSandboxRuntime(this.logger);
this.scanner = new PluginSecurityScanner(this.logger);
}

/**
Expand All@@ -64,38 +75,42 @@ export class EnterprisePluginPlatform {
async installPlugin(
plugin: Plugin,
config: {
health?: PluginHealthCheck;
hotReload?: HotReloadConfig;
permissions?: PermissionSet;
health?: PluginHealthCheckParsed;
hotReload?: HotReloadConfigParsed;
permissions?: PluginPermissionSet;
sandbox?: SandboxConfig;
securityScan?: boolean;
}
): Promise<void> {
const pluginName = plugin.name;
const pluginVersion = plugin.version || '1.0.0';

this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);

// Step 1: Security Scan
if (config.securityScan !== false) {
this.kernel.logger.info('Running security scan...');
this.logger.info('Running security scan...');

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

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

this.kernel.logger.info(
`Security scan passed: ${scanResult.score}/100`
this.logger.info(
`Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)`
);
}

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

this.kernel.logger.info(
this.logger.info(
`Permissions registered: ${config.permissions.permissions.length} permissions`
);
}

// Step 3: Create Sandbox
if (config.sandbox) {
this.sandbox.createSandbox(pluginName, config.sandbox);
this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`);
this.logger.info(`Sandbox created: ${config.sandbox.level} level`);
}

// Step 4: Register for Health Monitoring
if (config.health) {
this.healthMonitor.registerPlugin(pluginName, config.health);
this.kernel.logger.info(
this.logger.info(
`Health monitoring configured: ${config.health.interval}ms interval`
);
}

// Step 5: Register for Hot Reload
if (config.hotReload) {
this.hotReload.registerPlugin(pluginName, config.hotReload);
this.kernel.logger.info(
this.logger.info(
`Hot reload enabled: ${config.hotReload.stateStrategy} state strategy`
);
}

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

this.kernel.logger.info(`Plugin ${pluginName} installed successfully`);
this.logger.info(`Plugin ${pluginName} installed successfully`);
}

/**
Expand All@@ -153,14 +168,14 @@ export class EnterprisePluginPlatform {
}
}

this.kernel.logger.info('Platform started successfully');
this.logger.info('Platform started successfully');
}

/**
* Shutdown the platform
*/
async shutdown(): Promise<void> {
this.kernel.logger.info('Shutting down platform...');
this.logger.info('Shutting down platform...');

// Stop health monitoring
this.healthMonitor.shutdown();
Expand All@@ -171,7 +186,7 @@ export class EnterprisePluginPlatform {
// Shutdown kernel
await this.kernel.shutdown();

this.kernel.logger.info('Platform shutdown complete');
this.logger.info('Platform shutdown complete');
}

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

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

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

Expand All@@ -230,7 +245,7 @@ export class EnterprisePluginPlatform {
restoreState
);

this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`);
this.logger.info(`Plugin ${pluginName} reloaded successfully`);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,9 @@
},
"scripts": {
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test-typecheck-debt.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
{
"_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",
"_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.",
"entries": {
"src/plugin-loader.test.ts": {
"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
},
"src/security/plugin-permission-enforcer.test.ts": {
"TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1
},
"src/utils/filter-tokens.test.ts": {
"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
},
"src/utils/migration-journal.test.ts": {
"TS6133: 'rows' is declared but its value is never read.": 1
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .changeset/core-typecheck-script.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
---
"@objectstack/core": patch
---

feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)

`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
selects only packages that declare the task — so the lint workflow's typecheck
job had no way to reach this package, and `pnpm --filter @objectstack/core
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
and rest, runtime, mcp, services and plugins all import it.

The state was tracked but not runnable. `check:type-check-coverage` carried
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
not something a contributor working in the package can run, which is how a
dispatched task came to assume the script existed.

**Measured at `84b8190ae`, dependency closure built first.** The undivided
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
same program restricted to the 63 non-test source files reports **zero**. So the
build layer graduated as it stood, and the 98 did not have to be repaired before
the script could exist.

**94 of the 98 were the check, not the code.** The repair is the split this
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
stays the build config and excludes the test layer; a new `tsconfig.test.json`
compiles that layer under the module semantics vitest actually executes it with
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
that does not resolve makes every symbol it names `any`. **No test file was
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
shrink-only.

**The `examples/` half was found by the new script, not by the card.** Declaring
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
`packages/core/examples` — 2 non-test source files in no tsc program at all.
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
(above the package root, never existed) and `phase2-integration.ts` imported
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
no dependency block. Collapsing that cascade exposed rather than removed errors,
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
**private** `logger`; four members of the security scan result that do not exist
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
`status` and per-severity counts); and two config literals passing the unparsed
shapes where `PluginHealthMonitor.registerPlugin` and
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
pair is retirement drift — this file was edited by two retirements (restart keys,
`watchPatterns`) while no tsc program could check the result. Every correction is
pinned to this package's own signatures; `packages/spec` was not touched.

`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
4 changes: 2 additions & 2 deletions packages/core/examples/kernel-features-example.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ import {
PluginMetadata,
ServiceLifecycle,
PluginContext
} from '../index.js';
} from '../src/index.js';

// ============================================================================
// Example 1: Database Plugin with Health Checks
Expand DownExpand Up@@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = {
ctx.logger.info('Disconnecting from database...');
this.connected = false;
},
async query(sql: string) {
async query(_sql: string) {
if (!this.connected) {
throw new Error('Database not connected');
}
Expand Down
91 changes: 53 additions & 38 deletions packages/core/examples/phase2-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,28 +14,34 @@ import {
ObjectKernel,
PluginHealthMonitor,
HotReloadManager,
DependencyResolver,
PluginPermissionManager,
PluginSandboxRuntime,
PluginSecurityScanner
} from '@objectstack/core';

import type { Plugin } from '@objectstack/core';
PluginSecurityScanner,
createLogger
} from '../src/index.js';

import type { Plugin, ObjectLogger } from '../src/index.js';
// [#14613] The PARSED variants, because that is what the methods below take:
// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed`
// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed`
// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every
// key optional, so passing them was a real type error this file carried while no
// tsc program read it.
import type {
PluginHealthCheck,
HotReloadConfig,
PermissionSet,
PluginHealthCheckParsed,
HotReloadConfigParsed,
PluginPermissionSet,
SandboxConfig
} from '@objectstack/spec/system';
} from '@objectstack/spec/kernel';

/**
* Example: Enterprise Plugin Platform with Phase 2 Features
*/
export class EnterprisePluginPlatform {
private kernel: ObjectKernel;
private logger: ObjectLogger;
private healthMonitor: PluginHealthMonitor;
private hotReload: HotReloadManager;
private depResolver: DependencyResolver;
private permManager: PluginPermissionManager;
private sandbox: PluginSandboxRuntime;
private scanner: PluginSecurityScanner;
Expand All@@ -49,13 +55,18 @@ export class EnterprisePluginPlatform {
},
});

// [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so
// every one of the 20 reads this file made of it was a type error --
// invisible until this package declared a `typecheck` script, because no
// tsc program had ever compiled this directory.
this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' });

// Initialize Phase 2 components
this.healthMonitor = new PluginHealthMonitor(this.kernel.logger);
this.hotReload = new HotReloadManager(this.kernel.logger);
this.depResolver = new DependencyResolver(this.kernel.logger);
this.permManager = new PluginPermissionManager(this.kernel.logger);
this.sandbox = new PluginSandboxRuntime(this.kernel.logger);
this.scanner = new PluginSecurityScanner(this.kernel.logger);
this.healthMonitor = new PluginHealthMonitor(this.logger);
this.hotReload = new HotReloadManager(this.logger);
this.permManager = new PluginPermissionManager(this.logger);
this.sandbox = new PluginSandboxRuntime(this.logger);
this.scanner = new PluginSecurityScanner(this.logger);
}

/**
Expand All@@ -64,38 +75,42 @@ export class EnterprisePluginPlatform {
async installPlugin(
plugin: Plugin,
config: {
health?: PluginHealthCheck;
hotReload?: HotReloadConfig;
permissions?: PermissionSet;
health?: PluginHealthCheckParsed;
hotReload?: HotReloadConfigParsed;
permissions?: PluginPermissionSet;
sandbox?: SandboxConfig;
securityScan?: boolean;
}
): Promise<void> {
const pluginName = plugin.name;
const pluginVersion = plugin.version || '1.0.0';

this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);

// Step 1: Security Scan
if (config.securityScan !== false) {
this.kernel.logger.info('Running security scan...');
this.logger.info('Running security scan...');

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

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

this.kernel.logger.info(
`Security scan passed: ${scanResult.score}/100`
this.logger.info(
`Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)`
);
}

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

this.kernel.logger.info(
this.logger.info(
`Permissions registered: ${config.permissions.permissions.length} permissions`
);
}

// Step 3: Create Sandbox
if (config.sandbox) {
this.sandbox.createSandbox(pluginName, config.sandbox);
this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`);
this.logger.info(`Sandbox created: ${config.sandbox.level} level`);
}

// Step 4: Register for Health Monitoring
if (config.health) {
this.healthMonitor.registerPlugin(pluginName, config.health);
this.kernel.logger.info(
this.logger.info(
`Health monitoring configured: ${config.health.interval}ms interval`
);
}

// Step 5: Register for Hot Reload
if (config.hotReload) {
this.hotReload.registerPlugin(pluginName, config.hotReload);
this.kernel.logger.info(
this.logger.info(
`Hot reload enabled: ${config.hotReload.stateStrategy} state strategy`
);
}

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

this.kernel.logger.info(`Plugin ${pluginName} installed successfully`);
this.logger.info(`Plugin ${pluginName} installed successfully`);
}

/**
Expand All@@ -153,14 +168,14 @@ export class EnterprisePluginPlatform {
}
}

this.kernel.logger.info('Platform started successfully');
this.logger.info('Platform started successfully');
}

/**
* Shutdown the platform
*/
async shutdown(): Promise<void> {
this.kernel.logger.info('Shutting down platform...');
this.logger.info('Shutting down platform...');

// Stop health monitoring
this.healthMonitor.shutdown();
Expand All@@ -171,7 +186,7 @@ export class EnterprisePluginPlatform {
// Shutdown kernel
await this.kernel.shutdown();

this.kernel.logger.info('Platform shutdown complete');
this.logger.info('Platform shutdown complete');
}

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

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

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

Expand All@@ -230,7 +245,7 @@ export class EnterprisePluginPlatform {
restoreState
);

this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`);
this.logger.info(`Plugin ${pluginName} reloaded successfully`);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,9 @@
},
"scripts": {
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test-typecheck-debt.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
{
"_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",
"_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.",
"entries": {
"src/plugin-loader.test.ts": {
"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
},
"src/security/plugin-permission-enforcer.test.ts": {
"TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1
},
"src/utils/filter-tokens.test.ts": {
"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
},
"src/utils/migration-journal.test.ts": {
"TS6133: 'rows' is declared but its value is never read.": 1
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .changeset/core-typecheck-script.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
---
"@objectstack/core": patch
---

feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)

`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
selects only packages that declare the task — so the lint workflow's typecheck
job had no way to reach this package, and `pnpm --filter @objectstack/core
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
and rest, runtime, mcp, services and plugins all import it.

The state was tracked but not runnable. `check:type-check-coverage` carried
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
not something a contributor working in the package can run, which is how a
dispatched task came to assume the script existed.

**Measured at `84b8190ae`, dependency closure built first.** The undivided
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
same program restricted to the 63 non-test source files reports **zero**. So the
build layer graduated as it stood, and the 98 did not have to be repaired before
the script could exist.

**94 of the 98 were the check, not the code.** The repair is the split this
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
stays the build config and excludes the test layer; a new `tsconfig.test.json`
compiles that layer under the module semantics vitest actually executes it with
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
that does not resolve makes every symbol it names `any`. **No test file was
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
shrink-only.

**The `examples/` half was found by the new script, not by the card.** Declaring
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
`packages/core/examples` — 2 non-test source files in no tsc program at all.
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
(above the package root, never existed) and `phase2-integration.ts` imported
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
no dependency block. Collapsing that cascade exposed rather than removed errors,
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
**private** `logger`; four members of the security scan result that do not exist
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
`status` and per-severity counts); and two config literals passing the unparsed
shapes where `PluginHealthMonitor.registerPlugin` and
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
pair is retirement drift — this file was edited by two retirements (restart keys,
`watchPatterns`) while no tsc program could check the result. Every correction is
pinned to this package's own signatures; `packages/spec` was not touched.

`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
4 changes: 2 additions & 2 deletions packages/core/examples/kernel-features-example.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ import {
PluginMetadata,
ServiceLifecycle,
PluginContext
} from '../index.js';
} from '../src/index.js';

// ============================================================================
// Example 1: Database Plugin with Health Checks
Expand DownExpand Up@@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = {
ctx.logger.info('Disconnecting from database...');
this.connected = false;
},
async query(sql: string) {
async query(_sql: string) {
if (!this.connected) {
throw new Error('Database not connected');
}
Expand Down
91 changes: 53 additions & 38 deletions packages/core/examples/phase2-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,28 +14,34 @@ import {
ObjectKernel,
PluginHealthMonitor,
HotReloadManager,
DependencyResolver,
PluginPermissionManager,
PluginSandboxRuntime,
PluginSecurityScanner
} from '@objectstack/core';

import type { Plugin } from '@objectstack/core';
PluginSecurityScanner,
createLogger
} from '../src/index.js';

import type { Plugin, ObjectLogger } from '../src/index.js';
// [#14613] The PARSED variants, because that is what the methods below take:
// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed`
// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed`
// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every
// key optional, so passing them was a real type error this file carried while no
// tsc program read it.
import type {
PluginHealthCheck,
HotReloadConfig,
PermissionSet,
PluginHealthCheckParsed,
HotReloadConfigParsed,
PluginPermissionSet,
SandboxConfig
} from '@objectstack/spec/system';
} from '@objectstack/spec/kernel';

/**
* Example: Enterprise Plugin Platform with Phase 2 Features
*/
export class EnterprisePluginPlatform {
private kernel: ObjectKernel;
private logger: ObjectLogger;
private healthMonitor: PluginHealthMonitor;
private hotReload: HotReloadManager;
private depResolver: DependencyResolver;
private permManager: PluginPermissionManager;
private sandbox: PluginSandboxRuntime;
private scanner: PluginSecurityScanner;
Expand All@@ -49,13 +55,18 @@ export class EnterprisePluginPlatform {
},
});

// [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so
// every one of the 20 reads this file made of it was a type error --
// invisible until this package declared a `typecheck` script, because no
// tsc program had ever compiled this directory.
this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' });

// Initialize Phase 2 components
this.healthMonitor = new PluginHealthMonitor(this.kernel.logger);
this.hotReload = new HotReloadManager(this.kernel.logger);
this.depResolver = new DependencyResolver(this.kernel.logger);
this.permManager = new PluginPermissionManager(this.kernel.logger);
this.sandbox = new PluginSandboxRuntime(this.kernel.logger);
this.scanner = new PluginSecurityScanner(this.kernel.logger);
this.healthMonitor = new PluginHealthMonitor(this.logger);
this.hotReload = new HotReloadManager(this.logger);
this.permManager = new PluginPermissionManager(this.logger);
this.sandbox = new PluginSandboxRuntime(this.logger);
this.scanner = new PluginSecurityScanner(this.logger);
}

/**
Expand All@@ -64,38 +75,42 @@ export class EnterprisePluginPlatform {
async installPlugin(
plugin: Plugin,
config: {
health?: PluginHealthCheck;
hotReload?: HotReloadConfig;
permissions?: PermissionSet;
health?: PluginHealthCheckParsed;
hotReload?: HotReloadConfigParsed;
permissions?: PluginPermissionSet;
sandbox?: SandboxConfig;
securityScan?: boolean;
}
): Promise<void> {
const pluginName = plugin.name;
const pluginVersion = plugin.version || '1.0.0';

this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);

// Step 1: Security Scan
if (config.securityScan !== false) {
this.kernel.logger.info('Running security scan...');
this.logger.info('Running security scan...');

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

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

this.kernel.logger.info(
`Security scan passed: ${scanResult.score}/100`
this.logger.info(
`Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)`
);
}

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

this.kernel.logger.info(
this.logger.info(
`Permissions registered: ${config.permissions.permissions.length} permissions`
);
}

// Step 3: Create Sandbox
if (config.sandbox) {
this.sandbox.createSandbox(pluginName, config.sandbox);
this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`);
this.logger.info(`Sandbox created: ${config.sandbox.level} level`);
}

// Step 4: Register for Health Monitoring
if (config.health) {
this.healthMonitor.registerPlugin(pluginName, config.health);
this.kernel.logger.info(
this.logger.info(
`Health monitoring configured: ${config.health.interval}ms interval`
);
}

// Step 5: Register for Hot Reload
if (config.hotReload) {
this.hotReload.registerPlugin(pluginName, config.hotReload);
this.kernel.logger.info(
this.logger.info(
`Hot reload enabled: ${config.hotReload.stateStrategy} state strategy`
);
}

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

this.kernel.logger.info(`Plugin ${pluginName} installed successfully`);
this.logger.info(`Plugin ${pluginName} installed successfully`);
}

/**
Expand All@@ -153,14 +168,14 @@ export class EnterprisePluginPlatform {
}
}

this.kernel.logger.info('Platform started successfully');
this.logger.info('Platform started successfully');
}

/**
* Shutdown the platform
*/
async shutdown(): Promise<void> {
this.kernel.logger.info('Shutting down platform...');
this.logger.info('Shutting down platform...');

// Stop health monitoring
this.healthMonitor.shutdown();
Expand All@@ -171,7 +186,7 @@ export class EnterprisePluginPlatform {
// Shutdown kernel
await this.kernel.shutdown();

this.kernel.logger.info('Platform shutdown complete');
this.logger.info('Platform shutdown complete');
}

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

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

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

Expand All@@ -230,7 +245,7 @@ export class EnterprisePluginPlatform {
restoreState
);

this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`);
this.logger.info(`Plugin ${pluginName} reloaded successfully`);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,9 @@
},
"scripts": {
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test-typecheck-debt.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
{
"_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",
"_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.",
"entries": {
"src/plugin-loader.test.ts": {
"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
},
"src/security/plugin-permission-enforcer.test.ts": {
"TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1
},
"src/utils/filter-tokens.test.ts": {
"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
},
"src/utils/migration-journal.test.ts": {
"TS6133: 'rows' is declared but its value is never read.": 1
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .changeset/core-typecheck-script.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
---
"@objectstack/core": patch
---

feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)

`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
selects only packages that declare the task — so the lint workflow's typecheck
job had no way to reach this package, and `pnpm --filter @objectstack/core
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
and rest, runtime, mcp, services and plugins all import it.

The state was tracked but not runnable. `check:type-check-coverage` carried
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
not something a contributor working in the package can run, which is how a
dispatched task came to assume the script existed.

**Measured at `84b8190ae`, dependency closure built first.** The undivided
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
same program restricted to the 63 non-test source files reports **zero**. So the
build layer graduated as it stood, and the 98 did not have to be repaired before
the script could exist.

**94 of the 98 were the check, not the code.** The repair is the split this
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
stays the build config and excludes the test layer; a new `tsconfig.test.json`
compiles that layer under the module semantics vitest actually executes it with
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
that does not resolve makes every symbol it names `any`. **No test file was
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
shrink-only.

**The `examples/` half was found by the new script, not by the card.** Declaring
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
`packages/core/examples` — 2 non-test source files in no tsc program at all.
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
(above the package root, never existed) and `phase2-integration.ts` imported
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
no dependency block. Collapsing that cascade exposed rather than removed errors,
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
**private** `logger`; four members of the security scan result that do not exist
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
`status` and per-severity counts); and two config literals passing the unparsed
shapes where `PluginHealthMonitor.registerPlugin` and
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
pair is retirement drift — this file was edited by two retirements (restart keys,
`watchPatterns`) while no tsc program could check the result. Every correction is
pinned to this package's own signatures; `packages/spec` was not touched.

`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
4 changes: 2 additions & 2 deletions packages/core/examples/kernel-features-example.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ import {
PluginMetadata,
ServiceLifecycle,
PluginContext
} from '../index.js';
} from '../src/index.js';

// ============================================================================
// Example 1: Database Plugin with Health Checks
Expand DownExpand Up@@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = {
ctx.logger.info('Disconnecting from database...');
this.connected = false;
},
async query(sql: string) {
async query(_sql: string) {
if (!this.connected) {
throw new Error('Database not connected');
}
Expand Down
91 changes: 53 additions & 38 deletions packages/core/examples/phase2-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,28 +14,34 @@ import {
ObjectKernel,
PluginHealthMonitor,
HotReloadManager,
DependencyResolver,
PluginPermissionManager,
PluginSandboxRuntime,
PluginSecurityScanner
} from '@objectstack/core';

import type { Plugin } from '@objectstack/core';
PluginSecurityScanner,
createLogger
} from '../src/index.js';

import type { Plugin, ObjectLogger } from '../src/index.js';
// [#14613] The PARSED variants, because that is what the methods below take:
// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed`
// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed`
// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every
// key optional, so passing them was a real type error this file carried while no
// tsc program read it.
import type {
PluginHealthCheck,
HotReloadConfig,
PermissionSet,
PluginHealthCheckParsed,
HotReloadConfigParsed,
PluginPermissionSet,
SandboxConfig
} from '@objectstack/spec/system';
} from '@objectstack/spec/kernel';

/**
* Example: Enterprise Plugin Platform with Phase 2 Features
*/
export class EnterprisePluginPlatform {
private kernel: ObjectKernel;
private logger: ObjectLogger;
private healthMonitor: PluginHealthMonitor;
private hotReload: HotReloadManager;
private depResolver: DependencyResolver;
private permManager: PluginPermissionManager;
private sandbox: PluginSandboxRuntime;
private scanner: PluginSecurityScanner;
Expand All@@ -49,13 +55,18 @@ export class EnterprisePluginPlatform {
},
});

// [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so
// every one of the 20 reads this file made of it was a type error --
// invisible until this package declared a `typecheck` script, because no
// tsc program had ever compiled this directory.
this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' });

// Initialize Phase 2 components
this.healthMonitor = new PluginHealthMonitor(this.kernel.logger);
this.hotReload = new HotReloadManager(this.kernel.logger);
this.depResolver = new DependencyResolver(this.kernel.logger);
this.permManager = new PluginPermissionManager(this.kernel.logger);
this.sandbox = new PluginSandboxRuntime(this.kernel.logger);
this.scanner = new PluginSecurityScanner(this.kernel.logger);
this.healthMonitor = new PluginHealthMonitor(this.logger);
this.hotReload = new HotReloadManager(this.logger);
this.permManager = new PluginPermissionManager(this.logger);
this.sandbox = new PluginSandboxRuntime(this.logger);
this.scanner = new PluginSecurityScanner(this.logger);
}

/**
Expand All@@ -64,38 +75,42 @@ export class EnterprisePluginPlatform {
async installPlugin(
plugin: Plugin,
config: {
health?: PluginHealthCheck;
hotReload?: HotReloadConfig;
permissions?: PermissionSet;
health?: PluginHealthCheckParsed;
hotReload?: HotReloadConfigParsed;
permissions?: PluginPermissionSet;
sandbox?: SandboxConfig;
securityScan?: boolean;
}
): Promise<void> {
const pluginName = plugin.name;
const pluginVersion = plugin.version || '1.0.0';

this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);

// Step 1: Security Scan
if (config.securityScan !== false) {
this.kernel.logger.info('Running security scan...');
this.logger.info('Running security scan...');

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

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

this.kernel.logger.info(
`Security scan passed: ${scanResult.score}/100`
this.logger.info(
`Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)`
);
}

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

this.kernel.logger.info(
this.logger.info(
`Permissions registered: ${config.permissions.permissions.length} permissions`
);
}

// Step 3: Create Sandbox
if (config.sandbox) {
this.sandbox.createSandbox(pluginName, config.sandbox);
this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`);
this.logger.info(`Sandbox created: ${config.sandbox.level} level`);
}

// Step 4: Register for Health Monitoring
if (config.health) {
this.healthMonitor.registerPlugin(pluginName, config.health);
this.kernel.logger.info(
this.logger.info(
`Health monitoring configured: ${config.health.interval}ms interval`
);
}

// Step 5: Register for Hot Reload
if (config.hotReload) {
this.hotReload.registerPlugin(pluginName, config.hotReload);
this.kernel.logger.info(
this.logger.info(
`Hot reload enabled: ${config.hotReload.stateStrategy} state strategy`
);
}

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

this.kernel.logger.info(`Plugin ${pluginName} installed successfully`);
this.logger.info(`Plugin ${pluginName} installed successfully`);
}

/**
Expand All@@ -153,14 +168,14 @@ export class EnterprisePluginPlatform {
}
}

this.kernel.logger.info('Platform started successfully');
this.logger.info('Platform started successfully');
}

/**
* Shutdown the platform
*/
async shutdown(): Promise<void> {
this.kernel.logger.info('Shutting down platform...');
this.logger.info('Shutting down platform...');

// Stop health monitoring
this.healthMonitor.shutdown();
Expand All@@ -171,7 +186,7 @@ export class EnterprisePluginPlatform {
// Shutdown kernel
await this.kernel.shutdown();

this.kernel.logger.info('Platform shutdown complete');
this.logger.info('Platform shutdown complete');
}

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

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

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

Expand All@@ -230,7 +245,7 @@ export class EnterprisePluginPlatform {
restoreState
);

this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`);
this.logger.info(`Plugin ${pluginName} reloaded successfully`);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,9 @@
},
"scripts": {
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test-typecheck-debt.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
{
"_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",
"_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.",
"entries": {
"src/plugin-loader.test.ts": {
"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
},
"src/security/plugin-permission-enforcer.test.ts": {
"TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1
},
"src/utils/filter-tokens.test.ts": {
"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
},
"src/utils/migration-journal.test.ts": {
"TS6133: 'rows' is declared but its value is never read.": 1
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .changeset/core-typecheck-script.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
---
"@objectstack/core": patch
---

feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)

`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
selects only packages that declare the task — so the lint workflow's typecheck
job had no way to reach this package, and `pnpm --filter @objectstack/core
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
and rest, runtime, mcp, services and plugins all import it.

The state was tracked but not runnable. `check:type-check-coverage` carried
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
not something a contributor working in the package can run, which is how a
dispatched task came to assume the script existed.

**Measured at `84b8190ae`, dependency closure built first.** The undivided
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
same program restricted to the 63 non-test source files reports **zero**. So the
build layer graduated as it stood, and the 98 did not have to be repaired before
the script could exist.

**94 of the 98 were the check, not the code.** The repair is the split this
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
stays the build config and excludes the test layer; a new `tsconfig.test.json`
compiles that layer under the module semantics vitest actually executes it with
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
that does not resolve makes every symbol it names `any`. **No test file was
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
shrink-only.

**The `examples/` half was found by the new script, not by the card.** Declaring
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
`packages/core/examples` — 2 non-test source files in no tsc program at all.
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
(above the package root, never existed) and `phase2-integration.ts` imported
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
no dependency block. Collapsing that cascade exposed rather than removed errors,
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
**private** `logger`; four members of the security scan result that do not exist
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
`status` and per-severity counts); and two config literals passing the unparsed
shapes where `PluginHealthMonitor.registerPlugin` and
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
pair is retirement drift — this file was edited by two retirements (restart keys,
`watchPatterns`) while no tsc program could check the result. Every correction is
pinned to this package's own signatures; `packages/spec` was not touched.

`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
4 changes: 2 additions & 2 deletions packages/core/examples/kernel-features-example.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ import {
PluginMetadata,
ServiceLifecycle,
PluginContext
} from '../index.js';
} from '../src/index.js';

// ============================================================================
// Example 1: Database Plugin with Health Checks
Expand DownExpand Up@@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = {
ctx.logger.info('Disconnecting from database...');
this.connected = false;
},
async query(sql: string) {
async query(_sql: string) {
if (!this.connected) {
throw new Error('Database not connected');
}
Expand Down
91 changes: 53 additions & 38 deletions packages/core/examples/phase2-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,28 +14,34 @@ import {
ObjectKernel,
PluginHealthMonitor,
HotReloadManager,
DependencyResolver,
PluginPermissionManager,
PluginSandboxRuntime,
PluginSecurityScanner
} from '@objectstack/core';

import type { Plugin } from '@objectstack/core';
PluginSecurityScanner,
createLogger
} from '../src/index.js';

import type { Plugin, ObjectLogger } from '../src/index.js';
// [#14613] The PARSED variants, because that is what the methods below take:
// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed`
// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed`
// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every
// key optional, so passing them was a real type error this file carried while no
// tsc program read it.
import type {
PluginHealthCheck,
HotReloadConfig,
PermissionSet,
PluginHealthCheckParsed,
HotReloadConfigParsed,
PluginPermissionSet,
SandboxConfig
} from '@objectstack/spec/system';
} from '@objectstack/spec/kernel';

/**
* Example: Enterprise Plugin Platform with Phase 2 Features
*/
export class EnterprisePluginPlatform {
private kernel: ObjectKernel;
private logger: ObjectLogger;
private healthMonitor: PluginHealthMonitor;
private hotReload: HotReloadManager;
private depResolver: DependencyResolver;
private permManager: PluginPermissionManager;
private sandbox: PluginSandboxRuntime;
private scanner: PluginSecurityScanner;
Expand All@@ -49,13 +55,18 @@ export class EnterprisePluginPlatform {
},
});

// [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so
// every one of the 20 reads this file made of it was a type error --
// invisible until this package declared a `typecheck` script, because no
// tsc program had ever compiled this directory.
this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' });

// Initialize Phase 2 components
this.healthMonitor = new PluginHealthMonitor(this.kernel.logger);
this.hotReload = new HotReloadManager(this.kernel.logger);
this.depResolver = new DependencyResolver(this.kernel.logger);
this.permManager = new PluginPermissionManager(this.kernel.logger);
this.sandbox = new PluginSandboxRuntime(this.kernel.logger);
this.scanner = new PluginSecurityScanner(this.kernel.logger);
this.healthMonitor = new PluginHealthMonitor(this.logger);
this.hotReload = new HotReloadManager(this.logger);
this.permManager = new PluginPermissionManager(this.logger);
this.sandbox = new PluginSandboxRuntime(this.logger);
this.scanner = new PluginSecurityScanner(this.logger);
}

/**
Expand All@@ -64,38 +75,42 @@ export class EnterprisePluginPlatform {
async installPlugin(
plugin: Plugin,
config: {
health?: PluginHealthCheck;
hotReload?: HotReloadConfig;
permissions?: PermissionSet;
health?: PluginHealthCheckParsed;
hotReload?: HotReloadConfigParsed;
permissions?: PluginPermissionSet;
sandbox?: SandboxConfig;
securityScan?: boolean;
}
): Promise<void> {
const pluginName = plugin.name;
const pluginVersion = plugin.version || '1.0.0';

this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);

// Step 1: Security Scan
if (config.securityScan !== false) {
this.kernel.logger.info('Running security scan...');
this.logger.info('Running security scan...');

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

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

this.kernel.logger.info(
`Security scan passed: ${scanResult.score}/100`
this.logger.info(
`Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)`
);
}

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

this.kernel.logger.info(
this.logger.info(
`Permissions registered: ${config.permissions.permissions.length} permissions`
);
}

// Step 3: Create Sandbox
if (config.sandbox) {
this.sandbox.createSandbox(pluginName, config.sandbox);
this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`);
this.logger.info(`Sandbox created: ${config.sandbox.level} level`);
}

// Step 4: Register for Health Monitoring
if (config.health) {
this.healthMonitor.registerPlugin(pluginName, config.health);
this.kernel.logger.info(
this.logger.info(
`Health monitoring configured: ${config.health.interval}ms interval`
);
}

// Step 5: Register for Hot Reload
if (config.hotReload) {
this.hotReload.registerPlugin(pluginName, config.hotReload);
this.kernel.logger.info(
this.logger.info(
`Hot reload enabled: ${config.hotReload.stateStrategy} state strategy`
);
}

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

this.kernel.logger.info(`Plugin ${pluginName} installed successfully`);
this.logger.info(`Plugin ${pluginName} installed successfully`);
}

/**
Expand All@@ -153,14 +168,14 @@ export class EnterprisePluginPlatform {
}
}

this.kernel.logger.info('Platform started successfully');
this.logger.info('Platform started successfully');
}

/**
* Shutdown the platform
*/
async shutdown(): Promise<void> {
this.kernel.logger.info('Shutting down platform...');
this.logger.info('Shutting down platform...');

// Stop health monitoring
this.healthMonitor.shutdown();
Expand All@@ -171,7 +186,7 @@ export class EnterprisePluginPlatform {
// Shutdown kernel
await this.kernel.shutdown();

this.kernel.logger.info('Platform shutdown complete');
this.logger.info('Platform shutdown complete');
}

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

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

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

Expand All@@ -230,7 +245,7 @@ export class EnterprisePluginPlatform {
restoreState
);

this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`);
this.logger.info(`Plugin ${pluginName} reloaded successfully`);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,9 @@
},
"scripts": {
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test-typecheck-debt.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
{
"_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",
"_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.",
"entries": {
"src/plugin-loader.test.ts": {
"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
},
"src/security/plugin-permission-enforcer.test.ts": {
"TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1
},
"src/utils/filter-tokens.test.ts": {
"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
},
"src/utils/migration-journal.test.ts": {
"TS6133: 'rows' is declared but its value is never read.": 1
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .changeset/core-typecheck-script.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
---
"@objectstack/core": patch
---

feat(tooling): `@objectstack/core` declares a `typecheck` script, and its test and examples layers enter the ratchet (#14613)

`packages/core/package.json` declared exactly `build`, `test` and `test:watch`.
Around twenty sibling packages declare `typecheck`, and `turbo run typecheck`
selects only packages that declare the task — so the lint workflow's typecheck
job had no way to reach this package, and `pnpm --filter @objectstack/core
typecheck` failed with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` for anyone who tried
it. The package's types ship anyway: `build` emits a 233 KB `dist/index.d.ts`,
and rest, runtime, mcp, services and plugins all import it.

The state was tracked but not runnable. `check:type-check-coverage` carried
`@objectstack/core` as a DEBT entry of 98 and had already re-measured it once
(91 to 98), so nothing was invisible — but a ledger only the gate can read is
not something a contributor working in the package can run, which is how a
dispatched task came to assume the script existed.

**Measured at `84b8190ae`, dependency closure built first.** The undivided
program (`tsc --noEmit -p tsconfig.json`, exactly as the DEBT entry measured it)
reports 98 errors across 12 files, and every one of the 12 is a `.test.ts`. The
same program restricted to the 63 non-test source files reports **zero**. So the
build layer graduated as it stood, and the 98 did not have to be repaired before
the script could exist.

**94 of the 98 were the check, not the code.** The repair is the split this
repo already runs for `spec`, `rest`, `objectql` and `client`: `tsconfig.json`
stays the build config and excludes the test layer; a new `tsconfig.test.json`
compiles that layer under the module semantics vitest actually executes it with
(`module: esnext`, `moduleResolution: bundler`), which retires 22 x TS2835, the
TS2347 beside them and the share of 71 x TS7006 they cascade into — an import
that does not resolve makes every symbol it names `any`. **No test file was
edited.** Strictness is inherited and untouched. The residue is 4 errors over 4
files, held per file and per signature in `test-typecheck-debt.json`, EXACT and
shrink-only.

**The `examples/` half was found by the new script, not by the card.** Declaring
`typecheck` flips the package from COVERED-BY-LEDGER to COVERED-BY-SCRIPT, and
`check:type-check-coverage`'s SOURCES_COVERED invariant immediately reported
`packages/core/examples` — 2 non-test source files in no tsc program at all.
Neither had ever compiled: `kernel-features-example.ts` imported `../index.js`
(above the package root, never existed) and `phase2-integration.ts` imported
`@objectstack/core`, i.e. this package self-referencing by a name it declares in
no dependency block. Collapsing that cascade exposed rather than removed errors,
12 to 29, all of them real and none of them new: 20 reads of `ObjectKernel`'s
**private** `logger`; four members of the security scan result that do not exist
(`passed`, `score`, `summary.critical`, `summary.high`, where the type carries
`status` and per-severity counts); and two config literals passing the unparsed
shapes where `PluginHealthMonitor.registerPlugin` and
`HotReloadManager.registerPlugin` are declared over the `Parsed` ones. That last
pair is retirement drift — this file was edited by two retirements (restart keys,
`watchPatterns`) while no tsc program could check the result. Every correction is
pinned to this package's own signatures; `packages/spec` was not touched.

`packages/core` therefore leaves the DEBT ledger: the coverage gate now reads
70/79 packages type-checked with 9 ledgered, where it read 68/78 with 10.
4 changes: 2 additions & 2 deletions packages/core/examples/kernel-features-example.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,7 @@ import {
PluginMetadata,
ServiceLifecycle,
PluginContext
} from '../index.js';
} from '../src/index.js';

// ============================================================================
// Example 1: Database Plugin with Health Checks
Expand DownExpand Up@@ -49,7 +49,7 @@ const databasePlugin: PluginMetadata = {
ctx.logger.info('Disconnecting from database...');
this.connected = false;
},
async query(sql: string) {
async query(_sql: string) {
if (!this.connected) {
throw new Error('Database not connected');
}
Expand Down
91 changes: 53 additions & 38 deletions packages/core/examples/phase2-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,28 +14,34 @@ import {
ObjectKernel,
PluginHealthMonitor,
HotReloadManager,
DependencyResolver,
PluginPermissionManager,
PluginSandboxRuntime,
PluginSecurityScanner
} from '@objectstack/core';

import type { Plugin } from '@objectstack/core';
PluginSecurityScanner,
createLogger
} from '../src/index.js';

import type { Plugin, ObjectLogger } from '../src/index.js';
// [#14613] The PARSED variants, because that is what the methods below take:
// `PluginHealthMonitor.registerPlugin` is declared over `PluginHealthCheckParsed`
// and `HotReloadManager.registerPlugin` over `HotReloadConfigParsed`
// (`src/health-monitor.ts`, `src/hot-reload.ts`). The unparsed shapes have every
// key optional, so passing them was a real type error this file carried while no
// tsc program read it.
import type {
PluginHealthCheck,
HotReloadConfig,
PermissionSet,
PluginHealthCheckParsed,
HotReloadConfigParsed,
PluginPermissionSet,
SandboxConfig
} from '@objectstack/spec/system';
} from '@objectstack/spec/kernel';

/**
* Example: Enterprise Plugin Platform with Phase 2 Features
*/
export class EnterprisePluginPlatform {
private kernel: ObjectKernel;
private logger: ObjectLogger;
private healthMonitor: PluginHealthMonitor;
private hotReload: HotReloadManager;
private depResolver: DependencyResolver;
private permManager: PluginPermissionManager;
private sandbox: PluginSandboxRuntime;
private scanner: PluginSecurityScanner;
Expand All@@ -49,13 +55,18 @@ export class EnterprisePluginPlatform {
},
});

// [#14613] The example's OWN logger. `ObjectKernel.logger` is private, so
// every one of the 20 reads this file made of it was a type error --
// invisible until this package declared a `typecheck` script, because no
// tsc program had ever compiled this directory.
this.logger = createLogger({ level: 'info', name: 'EnterprisePluginPlatform' });

// Initialize Phase 2 components
this.healthMonitor = new PluginHealthMonitor(this.kernel.logger);
this.hotReload = new HotReloadManager(this.kernel.logger);
this.depResolver = new DependencyResolver(this.kernel.logger);
this.permManager = new PluginPermissionManager(this.kernel.logger);
this.sandbox = new PluginSandboxRuntime(this.kernel.logger);
this.scanner = new PluginSecurityScanner(this.kernel.logger);
this.healthMonitor = new PluginHealthMonitor(this.logger);
this.hotReload = new HotReloadManager(this.logger);
this.permManager = new PluginPermissionManager(this.logger);
this.sandbox = new PluginSandboxRuntime(this.logger);
this.scanner = new PluginSecurityScanner(this.logger);
}

/**
Expand All@@ -64,38 +75,42 @@ export class EnterprisePluginPlatform {
async installPlugin(
plugin: Plugin,
config: {
health?: PluginHealthCheck;
hotReload?: HotReloadConfig;
permissions?: PermissionSet;
health?: PluginHealthCheckParsed;
hotReload?: HotReloadConfigParsed;
permissions?: PluginPermissionSet;
sandbox?: SandboxConfig;
securityScan?: boolean;
}
): Promise<void> {
const pluginName = plugin.name;
const pluginVersion = plugin.version || '1.0.0';

this.kernel.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);
this.logger.info(`Installing plugin: ${pluginName} v${pluginVersion}`);

// Step 1: Security Scan
if (config.securityScan !== false) {
this.kernel.logger.info('Running security scan...');
this.logger.info('Running security scan...');

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

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

this.kernel.logger.info(
`Security scan passed: ${scanResult.score}/100`
this.logger.info(
`Security scan passed: ${scanResult.summary.totalVulnerabilities} vulnerability(ies)`
);
}

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

this.kernel.logger.info(
this.logger.info(
`Permissions registered: ${config.permissions.permissions.length} permissions`
);
}

// Step 3: Create Sandbox
if (config.sandbox) {
this.sandbox.createSandbox(pluginName, config.sandbox);
this.kernel.logger.info(`Sandbox created: ${config.sandbox.level} level`);
this.logger.info(`Sandbox created: ${config.sandbox.level} level`);
}

// Step 4: Register for Health Monitoring
if (config.health) {
this.healthMonitor.registerPlugin(pluginName, config.health);
this.kernel.logger.info(
this.logger.info(
`Health monitoring configured: ${config.health.interval}ms interval`
);
}

// Step 5: Register for Hot Reload
if (config.hotReload) {
this.hotReload.registerPlugin(pluginName, config.hotReload);
this.kernel.logger.info(
this.logger.info(
`Hot reload enabled: ${config.hotReload.stateStrategy} state strategy`
);
}

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

this.kernel.logger.info(`Plugin ${pluginName} installed successfully`);
this.logger.info(`Plugin ${pluginName} installed successfully`);
}

/**
Expand All@@ -153,14 +168,14 @@ export class EnterprisePluginPlatform {
}
}

this.kernel.logger.info('Platform started successfully');
this.logger.info('Platform started successfully');
}

/**
* Shutdown the platform
*/
async shutdown(): Promise<void> {
this.kernel.logger.info('Shutting down platform...');
this.logger.info('Shutting down platform...');

// Stop health monitoring
this.healthMonitor.shutdown();
Expand All@@ -171,7 +186,7 @@ export class EnterprisePluginPlatform {
// Shutdown kernel
await this.kernel.shutdown();

this.kernel.logger.info('Platform shutdown complete');
this.logger.info('Platform shutdown complete');
}

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

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

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

Expand All@@ -230,7 +245,7 @@ export class EnterprisePluginPlatform {
restoreState
);

this.kernel.logger.info(`Plugin ${pluginName} reloaded successfully`);
this.logger.info(`Plugin ${pluginName} reloaded successfully`);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,9 @@
},
"scripts": {
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test-typecheck-debt.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
{
"_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",
"_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.",
"entries": {
"src/plugin-loader.test.ts": {
"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
},
"src/security/plugin-permission-enforcer.test.ts": {
"TS2739: Type '…' is missing the following properties from type 'PluginContext': registerServiceFactory, replaceService, getServiceScoped": 1
},
"src/utils/filter-tokens.test.ts": {
"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
},
"src/utils/migration-journal.test.ts": {
"TS6133: 'rows' is declared but its value is never read.": 1
}
}
}
Loading
Loading