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
18 changes: 18 additions & 0 deletions .changeset/runtime-typecheck-wired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/spec': patch
---

fix(spec,runtime): `EngineSchemaRegistryView` now declares the six package-lifecycle members it always had (#4311).

`getPackage` / `installPackage` / `uninstallPackage` / `enablePackage` / `disablePackage` /
`updatePackageManifest` are additions to the exported `EngineSchemaRegistryView` type only —
`SchemaRegistry` has implemented all six since long before the contract existed, and three
packages outside the engine already call them (`runtime`'s `/packages` domain handler,
`metadata-protocol`'s install/update primitives, `service-package`'s hydration). The contract
landed in #4404 declaring eight members; these six were missed, and nothing caught it because
`@objectstack/runtime` had no `typecheck` script to read the caller. Zero runtime behaviour
change: no implementation, call site or response shape moves.

`@objectstack/runtime` itself is not released by this change — it gains a `typecheck` script and
loses its `check-type-check-coverage` DEBT entry, plus type-only annotations (unused parameters
renamed to `_`-prefixed, one unused import dropped).
1 change: 1 addition & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"scripts": {
"build": "tsup --config tsup.config.ts",
"dev": "tsc -w",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
Expand Down
26 changes: 13 additions & 13 deletions packages/runtime/src/action-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export async function callData(deps: ActionExecutionDeps,
* `requiredPermissions` is ungated. Single-sourced so the REST `/actions/...`
* route and the MCP `run_action` bridge enforce the SAME declaration.
*/
export function actionPermissionError(deps: ActionExecutionDeps, actionDef: any, ec: any, objectName?: string): string | null {
export function actionPermissionError(_deps: ActionExecutionDeps, actionDef: any, ec: any, objectName?: string): string | null {
const required: string[] = Array.isArray(actionDef?.requiredPermissions)
? actionDef.requiredPermissions
: [];
Expand Down Expand Up @@ -315,7 +315,7 @@ export function actionPermissionError(deps: ActionExecutionDeps, actionDef: any,
* data-layer backstop — therefore decides what AI may trigger. Fail-closed by
* default.
*/
export function actionAiExposureError(deps: ActionExecutionDeps, actionDef: any, objectName?: string): string | null {
export function actionAiExposureError(_deps: ActionExecutionDeps, actionDef: any, objectName?: string): string | null {
if (actionDef?.ai?.exposed === true) return null;
const on = objectName ? ` on '${objectName}'` : '';
return (
Expand All @@ -331,7 +331,7 @@ export function actionAiExposureError(deps: ActionExecutionDeps, actionDef: any,
* `flow` needs a `target` and an automation service. UI-only types
* (`url`, `modal`, `form`) and `api` have no server dispatch here.
*/
export function isHeadlessInvokableAction(deps: ActionExecutionDeps, action: any, hasAutomation: boolean): boolean {
export function isHeadlessInvokableAction(_deps: ActionExecutionDeps, action: any, hasAutomation: boolean): boolean {
const type: string = action?.type ?? 'script';
if (type === 'script') return Boolean(action?.target || action?.body);
if (type === 'flow') return Boolean(action?.target) && hasAutomation;
Expand Down Expand Up @@ -359,7 +359,7 @@ const SERVER_DISPATCHED_ACTION_TYPES: ReadonlySet<string> = new Set(['script', '
* `Action '' on object '*' not found`. Naming the type and the prescription
* turns that dead end into an actionable 400.
*/
export function headlessActionTypeError(deps: ActionExecutionDeps, action: any, objectName?: string): string | null {
export function headlessActionTypeError(_deps: ActionExecutionDeps, action: any, objectName?: string): string | null {
const type: string = action?.type ?? 'script';
if (SERVER_DISPATCHED_ACTION_TYPES.has(type)) return null;
const name: string = action?.name ?? 'unknown';
Expand Down Expand Up @@ -424,7 +424,7 @@ export function flowActionUnavailableError(action: any): string {
* `recordIdParam` that nothing honours is the `declared ≠ enforced` shape in
* miniature.
*/
export function seedFlowActionParams(deps: ActionExecutionDeps,
export function seedFlowActionParams(_deps: ActionExecutionDeps,
action: any,
input: {
objectName: string;
Expand Down Expand Up @@ -522,7 +522,7 @@ export async function dispatchFlowAction(deps: ActionExecutionDeps,
return result ?? null;
}

export function actionLooksDestructive(deps: ActionExecutionDeps, action: any): boolean {
export function actionLooksDestructive(_deps: ActionExecutionDeps, action: any): boolean {
if (action?.ai?.requiresConfirmation !== undefined) return Boolean(action.ai.requiresConfirmation);
return Boolean(action?.confirmText || action?.mode === 'delete' || action?.variant === 'danger');
}
Expand Down Expand Up @@ -550,7 +550,7 @@ export function summarizeAction(deps: ActionExecutionDeps, action: any, obj: any
};
}

export function jsonTypeOf(deps: ActionExecutionDeps, t: string | undefined): 'string' | 'number' | 'boolean' | 'array' {
export function jsonTypeOf(_deps: ActionExecutionDeps, t: string | undefined): 'string' | 'number' | 'boolean' | 'array' {
switch (t) {
case 'number': case 'currency': case 'percent': case 'rating': case 'slider': case 'autonumber':
return 'number';
Expand Down Expand Up @@ -600,7 +600,7 @@ export function summarizeActionParams(deps: ActionExecutionDeps, action: any, ob
* parent object schema (holds `.fields`); pass `undefined` for a global
* action with only inline params.
*/
export function resolveDeclaredActionParams(deps: ActionExecutionDeps, action: any, obj: any): ResolvedActionParam[] {
export function resolveDeclaredActionParams(_deps: ActionExecutionDeps, action: any, obj: any): ResolvedActionParam[] {
const fields: Record<string, any> = obj?.fields ?? {};
const out: ResolvedActionParam[] = [];
for (const p of (Array.isArray(action?.params) ? action.params : [])) {
Expand Down Expand Up @@ -673,7 +673,7 @@ export function enforceActionParams(deps: ActionExecutionDeps,
* context-less / self-invoked call so a body can distinguish "no session" the
* same way hooks do.
*/
export function buildActionSession(deps: ActionExecutionDeps, ec: any): any | undefined {
export function buildActionSession(_deps: ActionExecutionDeps, ec: any): any | undefined {
if (!ec || (ec.userId == null && ec.tenantId == null)) return undefined;
return {
...(ec.userId != null ? { userId: String(ec.userId) } : {}),
Expand Down Expand Up @@ -724,7 +724,7 @@ export function buildActionExecutionContext(ec: any): Record<string, unknown> {
* facade proxied every call context-less. Returns `undefined` when the engine
* predates `createContext`, leaving the sandbox's own fallback in charge.
*/
export function buildActionApi(deps: ActionExecutionDeps, ql: any, ec: any): any | undefined {
export function buildActionApi(_deps: ActionExecutionDeps, ql: any, ec: any): any | undefined {
if (!ql || typeof ql.createContext !== 'function') return undefined;
try {
return ql.createContext(buildActionExecutionContext(ec));
Expand All @@ -745,7 +745,7 @@ export function buildActionApi(deps: ActionExecutionDeps, ql: any, ec: any): any
* and `ctx.api` write under the SAME identity (#3914); passing `ec` is what
* separates a trusted write from a context-less one.
*/
export function buildActionEngineFacade(deps: ActionExecutionDeps, ql: any, ec?: any): any {
export function buildActionEngineFacade(_deps: ActionExecutionDeps, ql: any, ec?: any): any {
const context = buildActionExecutionContext(ec);
return {
async insert(object: string, data: Record<string, unknown>): Promise<{ id: string }> {
Expand Down Expand Up @@ -1007,7 +1007,7 @@ export async function collectActionDeclarations(deps: ActionExecutionDeps,
* `executeAction` will find: spec `objectName`, bundle-collector `object`,
* else the `'global'` wildcard.
*/
export function standaloneActionObjectName(deps: ActionExecutionDeps, action: any): string {
export function standaloneActionObjectName(_deps: ActionExecutionDeps, action: any): string {
if (typeof action?.objectName === 'string' && action.objectName.length > 0) return action.objectName;
if (typeof action?.object === 'string' && action.object.length > 0) return action.object;
return GLOBAL_ACTION_OBJECT_KEY;
Expand Down Expand Up @@ -1050,7 +1050,7 @@ export function isActionNotRegisteredError(err: any): boolean {
* failed" (a business outcome, which propagates). Each surface words its own
* miss: REST 404s naming the routed object, MCP throws naming the action.
*/
export async function executeRegisteredAction(deps: ActionExecutionDeps,
export async function executeRegisteredAction(_deps: ActionExecutionDeps,
ql: any,
objectName: string,
candidates: string[],
Expand Down
9 changes: 8 additions & 1 deletion packages/runtime/src/domains/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@ export function createAuthDomain(deps: DomainHandlerDeps): DomainRoute {
/**
* Handles Auth requests
* path: sub-path after /auth/
*
* `_path` / `_method` / `_body` are unread by design and kept only for
* positional symmetry with the other domain handlers: since #4113 removed the
* mock session, this domain does not route on the sub-path at all — it hands
* `context.request` to the auth service whole, and that service owns the
* routing. They stay in the signature (rather than being dropped) because
* every caller passes them positionally, `createAuthDomain` included.
*/
export async function handleAuthRequest(deps: DomainHandlerDeps, path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
export async function handleAuthRequest(deps: DomainHandlerDeps, _path: string, _method: string, _body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
// 1. Try generic Auth Service.
//
// [#4127] This probed `authService.handler(request, response)` — a method
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/domains/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ async function getMcpResourceMetadataUrl(deps: DomainHandlerDeps, context: HttpP
* shape is unusable. The body is carried separately via `parsedBody`, so a
* GET/DELETE (no body) and a POST (JSON-RPC) both normalise cleanly.
*/
function toMcpWebRequest(deps: DomainHandlerDeps, raw: any, parsedBody: any): Request | undefined {
function toMcpWebRequest(_deps: DomainHandlerDeps, raw: any, parsedBody: any): Request | undefined {
if (!raw) return undefined;
// Already a Web Request.
if (typeof raw.headers?.get === 'function' && typeof raw.url === 'string' && typeof raw.method === 'string') {
Expand Down
1 change: 0 additions & 1 deletion packages/runtime/src/domains/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
} from '@objectstack/core';
import { pluralToSingular } from '@objectstack/spec/shared';
import { CoreServiceName } from '@objectstack/spec/system';
import * as actionExec from '../action-execution.js';
import type { HttpProtocolContext, HttpDispatcherResult } from '../http-dispatcher.js';
import type { DomainHandlerDeps, DomainRoute } from '../domain-handler-registry.js';

Expand Down
31 changes: 30 additions & 1 deletion packages/spec/src/contracts/objectql-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,23 @@ import type { IDataDriver } from './data-driver';
import type { FlowFunctionEffect, FlowFunctionEntry } from '../automation/flow-function.zod';

/**
* The engine's schema-registry view — the eight members reached through the
* The engine's schema-registry view — the members reached through the
* `objectql` slot from outside the engine package.
*
* ObjectQL exposes the registry as a public `registry` getter over a private
* `_registry` field. Every consumer belongs on the GETTER: the `/me/apps`
* handler reaching `_registry` through `as any` while its sibling handler read
* the public getter (B2), and plugin-security's declared-metadata readers doing
* the same, are the reaches this view retires.
*
* The package-lifecycle block below was missing from the original eight (#4404)
* and added by #4311's runtime slice. `SchemaRegistry` has always implemented
* all six; three packages outside the engine have always called them — the
* `/packages` domain handler in `runtime` (the REST owner of the whole family),
* `metadata-protocol`'s install/update primitives, and `service-package`'s
* hydration. Nothing caught the omission because `runtime` had no `typecheck`
* script, which is #4311's thesis in one line: the narrowing compiled only
* because no `tsc` ever read the caller.
*/
export interface EngineSchemaRegistryView {
/** The registered object schema, or `undefined`. */
Expand All @@ -76,6 +85,26 @@ export interface EngineSchemaRegistryView {
unregisterItem(type: string, name: string): void;
/** Seed the persisted disabled-package set before artifact load (AppPlugin boot). */
setInitialDisabledPackageIds(ids: Iterable<string>): void;

// ── Package lifecycle (the in-memory half of `/packages`) ────────────
// The durable half lives in `sys_packages` and is the protocol service's;
// these six are the registry side the REST handlers fall back to and the
// protocol service writes through.
/** One installed package by id, or `undefined` — the duplicate-install guard's reader. */
getPackage(id: string): unknown;
/** Register a package manifest in the in-memory registry. */
installPackage(manifest: unknown, settings?: Record<string, unknown>): unknown;
/** Drop a package from the registry; `false` when no package had that id. */
uninstallPackage(id: string): boolean;
/** Flip a package to enabled; `undefined` when no package had that id. */
enablePackage(id: string): unknown;
/** Flip a package to disabled; `undefined` when no package had that id. */
disablePackage(id: string): unknown;
/** Merge the human-editable manifest fields (name / description / version) — a metadata edit, not a reinstall. */
updatePackageManifest(
id: string,
patch: { name?: string; description?: string; version?: string },
): unknown;
}

/**
Expand Down
6 changes: 1 addition & 5 deletions scripts/check-type-check-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,6 @@ const DEBT = {
errors: 2,
note: 'code-tier 2 (TS2345).',
},
'@objectstack/runtime': {
errors: 18,
note: 'noise only (TS6133 unused); no code-tier finding in #4311.',
},
'@objectstack/service-analytics': {
errors: 3,
note: 'code-tier 2 (TS7053) + 1 noise.',
Expand Down Expand Up @@ -214,7 +210,7 @@ const TEST_DEBT = {
errors: 467,
note: 'TS2339 x255, TS2345 x188. Larger than driver-sql; src is clean, so the whole pile is test-only and invisible to every gate today.',
},
'@objectstack/runtime': { tests: 66, errors: 220, note: 'TS18048 x81 (possibly-undefined), TS2345 x26, TS6133 x25. Also in DEBT: its src does not check either.' },
'@objectstack/runtime': { tests: 66, errors: 220, note: 'TS18048 x81 (possibly-undefined), TS2345 x26, TS6133 x25. Src graduated in #4311 (declares `typecheck`); this is now purely the hidden test layer.' },
'@objectstack/objectql': { tests: 87, errors: 219, note: 'TS2339 x88, TS2554 x28 (wrong arity), TS7006 x25.' },
'@objectstack/plugin-auth': { tests: 26, errors: 124, note: 'TS2493 x40 (tuple index out of range), TS18048 x24, TS2740 x18.' },
'@objectstack/rest': { tests: 35, errors: 105, note: 'TS2835 x43 (NodeNext extensions), TS7006 x42. Also in DEBT.' },
Expand Down
Loading