From faddb7427ed73ca1418c76211a9098a85e6e8a7d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 00:40:54 +0000 Subject: [PATCH] =?UTF-8?q?fix(runtime,spec):=20=E8=AE=A9=20@objectstack/r?= =?UTF-8?q?untime=20=E7=9A=84=20src=20=E7=9C=9F=E6=AD=A3=E8=A2=AB=20tsc=20?= =?UTF-8?q?=E8=AF=BB=20=E2=80=94=E2=80=94=20=E4=BF=AE=2025=20=E6=9D=A1?= =?UTF-8?q?=E5=B9=B6=E6=8E=A5=E8=BF=9B=20turbo=20typecheck=20(#4311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #4311 (第一片:@objectstack/runtime 的 src 层)。 - packages/runtime: 新增 `typecheck: tsc --noEmit`,清掉实测的 25 条错误 (18 条 TS6133 未用形参/导入,7 条 TS2339 真契约缺陷)。 - packages/spec: `EngineSchemaRegistryView` 补齐六个 package-lifecycle 成员 —— SchemaRegistry 一直实现、三个包一直在调,#4404 落契约时漏declare, 因为 runtime 没有 typecheck 所以没人发现。 - scripts/check-type-check-coverage.mjs: 删 runtime 的 DEBT 条目(毕业要求, 否则 RECONCILED 不变式失败),并订正 TEST_DEBT 里已失真的一句注释。 覆盖率 61/77 → 62/77,DEBT 16 → 15 个包。TEST_DEBT 未被激活(tsconfig 仍 exclude 测试文件,220 条留给后续切片)。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ --- .changeset/runtime-typecheck-wired.md | 18 +++++++++++ packages/runtime/package.json | 1 + packages/runtime/src/action-execution.ts | 26 ++++++++-------- packages/runtime/src/domains/auth.ts | 9 +++++- packages/runtime/src/domains/mcp.ts | 2 +- packages/runtime/src/domains/meta.ts | 1 - .../spec/src/contracts/objectql-engine.ts | 31 ++++++++++++++++++- scripts/check-type-check-coverage.mjs | 6 +--- 8 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 .changeset/runtime-typecheck-wired.md diff --git a/.changeset/runtime-typecheck-wired.md b/.changeset/runtime-typecheck-wired.md new file mode 100644 index 0000000000..b701e0c936 --- /dev/null +++ b/.changeset/runtime-typecheck-wired.md @@ -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). diff --git a/packages/runtime/package.json b/packages/runtime/package.json index d28e7670f8..1b36f42724 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -16,6 +16,7 @@ "scripts": { "build": "tsup --config tsup.config.ts", "dev": "tsc -w", + "typecheck": "tsc --noEmit", "test": "vitest run" }, "dependencies": { diff --git a/packages/runtime/src/action-execution.ts b/packages/runtime/src/action-execution.ts index a5b2b02b94..eacaa808ce 100644 --- a/packages/runtime/src/action-execution.ts +++ b/packages/runtime/src/action-execution.ts @@ -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 : []; @@ -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 ( @@ -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; @@ -359,7 +359,7 @@ const SERVER_DISPATCHED_ACTION_TYPES: ReadonlySet = 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'; @@ -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; @@ -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'); } @@ -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'; @@ -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 = obj?.fields ?? {}; const out: ResolvedActionParam[] = []; for (const p of (Array.isArray(action?.params) ? action.params : [])) { @@ -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) } : {}), @@ -724,7 +724,7 @@ export function buildActionExecutionContext(ec: any): Record { * 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)); @@ -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): Promise<{ id: string }> { @@ -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; @@ -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[], diff --git a/packages/runtime/src/domains/auth.ts b/packages/runtime/src/domains/auth.ts index 941260f900..2e752f798e 100644 --- a/packages/runtime/src/domains/auth.ts +++ b/packages/runtime/src/domains/auth.ts @@ -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 { +export async function handleAuthRequest(deps: DomainHandlerDeps, _path: string, _method: string, _body: any, context: HttpProtocolContext): Promise { // 1. Try generic Auth Service. // // [#4127] This probed `authService.handler(request, response)` — a method diff --git a/packages/runtime/src/domains/mcp.ts b/packages/runtime/src/domains/mcp.ts index e6cf3ce179..94f82660ec 100644 --- a/packages/runtime/src/domains/mcp.ts +++ b/packages/runtime/src/domains/mcp.ts @@ -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') { diff --git a/packages/runtime/src/domains/meta.ts b/packages/runtime/src/domains/meta.ts index 1ef489ec5c..9f0322a975 100644 --- a/packages/runtime/src/domains/meta.ts +++ b/packages/runtime/src/domains/meta.ts @@ -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'; diff --git a/packages/spec/src/contracts/objectql-engine.ts b/packages/spec/src/contracts/objectql-engine.ts index 7b9f5e54ba..f5f4f0461f 100644 --- a/packages/spec/src/contracts/objectql-engine.ts +++ b/packages/spec/src/contracts/objectql-engine.ts @@ -50,7 +50,7 @@ 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 @@ -58,6 +58,15 @@ import type { FlowFunctionEffect, FlowFunctionEntry } from '../automation/flow-f * 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`. */ @@ -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): 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): 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; } /** diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 714126bd1f..a9fa65470c 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -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.', @@ -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.' },