Skip to content

Commit 558c02a

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4001-ui-batch14
2 parents a61dc86 + 94f7b6a commit 558c02a

8 files changed

Lines changed: 72 additions & 22 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec,runtime): `EngineSchemaRegistryView` now declares the six package-lifecycle members it always had (#4311).
6+
7+
`getPackage` / `installPackage` / `uninstallPackage` / `enablePackage` / `disablePackage` /
8+
`updatePackageManifest` are additions to the exported `EngineSchemaRegistryView` type only —
9+
`SchemaRegistry` has implemented all six since long before the contract existed, and three
10+
packages outside the engine already call them (`runtime`'s `/packages` domain handler,
11+
`metadata-protocol`'s install/update primitives, `service-package`'s hydration). The contract
12+
landed in #4404 declaring eight members; these six were missed, and nothing caught it because
13+
`@objectstack/runtime` had no `typecheck` script to read the caller. Zero runtime behaviour
14+
change: no implementation, call site or response shape moves.
15+
16+
`@objectstack/runtime` itself is not released by this change — it gains a `typecheck` script and
17+
loses its `check-type-check-coverage` DEBT entry, plus type-only annotations (unused parameters
18+
renamed to `_`-prefixed, one unused import dropped).

packages/runtime/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"scripts": {
1717
"build": "tsup --config tsup.config.ts",
1818
"dev": "tsc -w",
19+
"typecheck": "tsc --noEmit",
1920
"test": "vitest run"
2021
},
2122
"dependencies": {

packages/runtime/src/action-execution.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export async function callData(deps: ActionExecutionDeps,
286286
* `requiredPermissions` is ungated. Single-sourced so the REST `/actions/...`
287287
* route and the MCP `run_action` bridge enforce the SAME declaration.
288288
*/
289-
export function actionPermissionError(deps: ActionExecutionDeps, actionDef: any, ec: any, objectName?: string): string | null {
289+
export function actionPermissionError(_deps: ActionExecutionDeps, actionDef: any, ec: any, objectName?: string): string | null {
290290
const required: string[] = Array.isArray(actionDef?.requiredPermissions)
291291
? actionDef.requiredPermissions
292292
: [];
@@ -315,7 +315,7 @@ export function actionPermissionError(deps: ActionExecutionDeps, actionDef: any,
315315
* data-layer backstop — therefore decides what AI may trigger. Fail-closed by
316316
* default.
317317
*/
318-
export function actionAiExposureError(deps: ActionExecutionDeps, actionDef: any, objectName?: string): string | null {
318+
export function actionAiExposureError(_deps: ActionExecutionDeps, actionDef: any, objectName?: string): string | null {
319319
if (actionDef?.ai?.exposed === true) return null;
320320
const on = objectName ? ` on '${objectName}'` : '';
321321
return (
@@ -331,7 +331,7 @@ export function actionAiExposureError(deps: ActionExecutionDeps, actionDef: any,
331331
* `flow` needs a `target` and an automation service. UI-only types
332332
* (`url`, `modal`, `form`) and `api` have no server dispatch here.
333333
*/
334-
export function isHeadlessInvokableAction(deps: ActionExecutionDeps, action: any, hasAutomation: boolean): boolean {
334+
export function isHeadlessInvokableAction(_deps: ActionExecutionDeps, action: any, hasAutomation: boolean): boolean {
335335
const type: string = action?.type ?? 'script';
336336
if (type === 'script') return Boolean(action?.target || action?.body);
337337
if (type === 'flow') return Boolean(action?.target) && hasAutomation;
@@ -359,7 +359,7 @@ const SERVER_DISPATCHED_ACTION_TYPES: ReadonlySet<string> = new Set(['script', '
359359
* `Action '' on object '*' not found`. Naming the type and the prescription
360360
* turns that dead end into an actionable 400.
361361
*/
362-
export function headlessActionTypeError(deps: ActionExecutionDeps, action: any, objectName?: string): string | null {
362+
export function headlessActionTypeError(_deps: ActionExecutionDeps, action: any, objectName?: string): string | null {
363363
const type: string = action?.type ?? 'script';
364364
if (SERVER_DISPATCHED_ACTION_TYPES.has(type)) return null;
365365
const name: string = action?.name ?? 'unknown';
@@ -424,7 +424,7 @@ export function flowActionUnavailableError(action: any): string {
424424
* `recordIdParam` that nothing honours is the `declared ≠ enforced` shape in
425425
* miniature.
426426
*/
427-
export function seedFlowActionParams(deps: ActionExecutionDeps,
427+
export function seedFlowActionParams(_deps: ActionExecutionDeps,
428428
action: any,
429429
input: {
430430
objectName: string;
@@ -522,7 +522,7 @@ export async function dispatchFlowAction(deps: ActionExecutionDeps,
522522
return result ?? null;
523523
}
524524

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

553-
export function jsonTypeOf(deps: ActionExecutionDeps, t: string | undefined): 'string' | 'number' | 'boolean' | 'array' {
553+
export function jsonTypeOf(_deps: ActionExecutionDeps, t: string | undefined): 'string' | 'number' | 'boolean' | 'array' {
554554
switch (t) {
555555
case 'number': case 'currency': case 'percent': case 'rating': case 'slider': case 'autonumber':
556556
return 'number';
@@ -600,7 +600,7 @@ export function summarizeActionParams(deps: ActionExecutionDeps, action: any, ob
600600
* parent object schema (holds `.fields`); pass `undefined` for a global
601601
* action with only inline params.
602602
*/
603-
export function resolveDeclaredActionParams(deps: ActionExecutionDeps, action: any, obj: any): ResolvedActionParam[] {
603+
export function resolveDeclaredActionParams(_deps: ActionExecutionDeps, action: any, obj: any): ResolvedActionParam[] {
604604
const fields: Record<string, any> = obj?.fields ?? {};
605605
const out: ResolvedActionParam[] = [];
606606
for (const p of (Array.isArray(action?.params) ? action.params : [])) {
@@ -673,7 +673,7 @@ export function enforceActionParams(deps: ActionExecutionDeps,
673673
* context-less / self-invoked call so a body can distinguish "no session" the
674674
* same way hooks do.
675675
*/
676-
export function buildActionSession(deps: ActionExecutionDeps, ec: any): any | undefined {
676+
export function buildActionSession(_deps: ActionExecutionDeps, ec: any): any | undefined {
677677
if (!ec || (ec.userId == null && ec.tenantId == null)) return undefined;
678678
return {
679679
...(ec.userId != null ? { userId: String(ec.userId) } : {}),
@@ -724,7 +724,7 @@ export function buildActionExecutionContext(ec: any): Record<string, unknown> {
724724
* facade proxied every call context-less. Returns `undefined` when the engine
725725
* predates `createContext`, leaving the sandbox's own fallback in charge.
726726
*/
727-
export function buildActionApi(deps: ActionExecutionDeps, ql: any, ec: any): any | undefined {
727+
export function buildActionApi(_deps: ActionExecutionDeps, ql: any, ec: any): any | undefined {
728728
if (!ql || typeof ql.createContext !== 'function') return undefined;
729729
try {
730730
return ql.createContext(buildActionExecutionContext(ec));
@@ -745,7 +745,7 @@ export function buildActionApi(deps: ActionExecutionDeps, ql: any, ec: any): any
745745
* and `ctx.api` write under the SAME identity (#3914); passing `ec` is what
746746
* separates a trusted write from a context-less one.
747747
*/
748-
export function buildActionEngineFacade(deps: ActionExecutionDeps, ql: any, ec?: any): any {
748+
export function buildActionEngineFacade(_deps: ActionExecutionDeps, ql: any, ec?: any): any {
749749
const context = buildActionExecutionContext(ec);
750750
return {
751751
async insert(object: string, data: Record<string, unknown>): Promise<{ id: string }> {
@@ -1007,7 +1007,7 @@ export async function collectActionDeclarations(deps: ActionExecutionDeps,
10071007
* `executeAction` will find: spec `objectName`, bundle-collector `object`,
10081008
* else the `'global'` wildcard.
10091009
*/
1010-
export function standaloneActionObjectName(deps: ActionExecutionDeps, action: any): string {
1010+
export function standaloneActionObjectName(_deps: ActionExecutionDeps, action: any): string {
10111011
if (typeof action?.objectName === 'string' && action.objectName.length > 0) return action.objectName;
10121012
if (typeof action?.object === 'string' && action.object.length > 0) return action.object;
10131013
return GLOBAL_ACTION_OBJECT_KEY;
@@ -1050,7 +1050,7 @@ export function isActionNotRegisteredError(err: any): boolean {
10501050
* failed" (a business outcome, which propagates). Each surface words its own
10511051
* miss: REST 404s naming the routed object, MCP throws naming the action.
10521052
*/
1053-
export async function executeRegisteredAction(deps: ActionExecutionDeps,
1053+
export async function executeRegisteredAction(_deps: ActionExecutionDeps,
10541054
ql: any,
10551055
objectName: string,
10561056
candidates: string[],

packages/runtime/src/domains/auth.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,15 @@ export function createAuthDomain(deps: DomainHandlerDeps): DomainRoute {
2121
/**
2222
* Handles Auth requests
2323
* path: sub-path after /auth/
24+
*
25+
* `_path` / `_method` / `_body` are unread by design and kept only for
26+
* positional symmetry with the other domain handlers: since #4113 removed the
27+
* mock session, this domain does not route on the sub-path at all — it hands
28+
* `context.request` to the auth service whole, and that service owns the
29+
* routing. They stay in the signature (rather than being dropped) because
30+
* every caller passes them positionally, `createAuthDomain` included.
2431
*/
25-
export async function handleAuthRequest(deps: DomainHandlerDeps, path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
32+
export async function handleAuthRequest(deps: DomainHandlerDeps, _path: string, _method: string, _body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
2633
// 1. Try generic Auth Service.
2734
//
2835
// [#4127] This probed `authService.handler(request, response)` — a method

packages/runtime/src/domains/mcp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ async function getMcpResourceMetadataUrl(deps: DomainHandlerDeps, context: HttpP
262262
* shape is unusable. The body is carried separately via `parsedBody`, so a
263263
* GET/DELETE (no body) and a POST (JSON-RPC) both normalise cleanly.
264264
*/
265-
function toMcpWebRequest(deps: DomainHandlerDeps, raw: any, parsedBody: any): Request | undefined {
265+
function toMcpWebRequest(_deps: DomainHandlerDeps, raw: any, parsedBody: any): Request | undefined {
266266
if (!raw) return undefined;
267267
// Already a Web Request.
268268
if (typeof raw.headers?.get === 'function' && typeof raw.url === 'string' && typeof raw.method === 'string') {

packages/runtime/src/domains/meta.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
} from '@objectstack/core';
1414
import { pluralToSingular } from '@objectstack/spec/shared';
1515
import { CoreServiceName } from '@objectstack/spec/system';
16-
import * as actionExec from '../action-execution.js';
1716
import type { HttpProtocolContext, HttpDispatcherResult } from '../http-dispatcher.js';
1817
import type { DomainHandlerDeps, DomainRoute } from '../domain-handler-registry.js';
1918

packages/spec/src/contracts/objectql-engine.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,23 @@ import type { IDataDriver } from './data-driver';
5050
import type { FlowFunctionEffect, FlowFunctionEntry } from '../automation/flow-function.zod';
5151

5252
/**
53-
* The engine's schema-registry view — the eight members reached through the
53+
* The engine's schema-registry view — the members reached through the
5454
* `objectql` slot from outside the engine package.
5555
*
5656
* ObjectQL exposes the registry as a public `registry` getter over a private
5757
* `_registry` field. Every consumer belongs on the GETTER: the `/me/apps`
5858
* handler reaching `_registry` through `as any` while its sibling handler read
5959
* the public getter (B2), and plugin-security's declared-metadata readers doing
6060
* the same, are the reaches this view retires.
61+
*
62+
* The package-lifecycle block below was missing from the original eight (#4404)
63+
* and added by #4311's runtime slice. `SchemaRegistry` has always implemented
64+
* all six; three packages outside the engine have always called them — the
65+
* `/packages` domain handler in `runtime` (the REST owner of the whole family),
66+
* `metadata-protocol`'s install/update primitives, and `service-package`'s
67+
* hydration. Nothing caught the omission because `runtime` had no `typecheck`
68+
* script, which is #4311's thesis in one line: the narrowing compiled only
69+
* because no `tsc` ever read the caller.
6170
*/
6271
export interface EngineSchemaRegistryView {
6372
/** The registered object schema, or `undefined`. */
@@ -76,6 +85,26 @@ export interface EngineSchemaRegistryView {
7685
unregisterItem(type: string, name: string): void;
7786
/** Seed the persisted disabled-package set before artifact load (AppPlugin boot). */
7887
setInitialDisabledPackageIds(ids: Iterable<string>): void;
88+
89+
// ── Package lifecycle (the in-memory half of `/packages`) ────────────
90+
// The durable half lives in `sys_packages` and is the protocol service's;
91+
// these six are the registry side the REST handlers fall back to and the
92+
// protocol service writes through.
93+
/** One installed package by id, or `undefined` — the duplicate-install guard's reader. */
94+
getPackage(id: string): unknown;
95+
/** Register a package manifest in the in-memory registry. */
96+
installPackage(manifest: unknown, settings?: Record<string, unknown>): unknown;
97+
/** Drop a package from the registry; `false` when no package had that id. */
98+
uninstallPackage(id: string): boolean;
99+
/** Flip a package to enabled; `undefined` when no package had that id. */
100+
enablePackage(id: string): unknown;
101+
/** Flip a package to disabled; `undefined` when no package had that id. */
102+
disablePackage(id: string): unknown;
103+
/** Merge the human-editable manifest fields (name / description / version) — a metadata edit, not a reinstall. */
104+
updatePackageManifest(
105+
id: string,
106+
patch: { name?: string; description?: string; version?: string },
107+
): unknown;
79108
}
80109

81110
/**

scripts/check-type-check-coverage.mjs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,6 @@ const DEBT = {
153153
errors: 2,
154154
note: 'code-tier 2 (TS2345).',
155155
},
156-
'@objectstack/runtime': {
157-
errors: 18,
158-
note: 'noise only (TS6133 unused); no code-tier finding in #4311.',
159-
},
160156
'@objectstack/service-analytics': {
161157
errors: 3,
162158
note: 'code-tier 2 (TS7053) + 1 noise.',
@@ -214,7 +210,7 @@ const TEST_DEBT = {
214210
errors: 467,
215211
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.',
216212
},
217-
'@objectstack/runtime': { tests: 66, errors: 220, note: 'TS18048 x81 (possibly-undefined), TS2345 x26, TS6133 x25. Also in DEBT: its src does not check either.' },
213+
'@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.' },
218214
'@objectstack/objectql': { tests: 87, errors: 219, note: 'TS2339 x88, TS2554 x28 (wrong arity), TS7006 x25.' },
219215
'@objectstack/plugin-auth': { tests: 26, errors: 124, note: 'TS2493 x40 (tuple index out of range), TS18048 x24, TS2740 x18.' },
220216
'@objectstack/rest': { tests: 35, errors: 105, note: 'TS2835 x43 (NodeNext extensions), TS7006 x42. Also in DEBT.' },

0 commit comments

Comments
 (0)