diff --git a/packages/metadata-protocol/src/protocol-loud-degradations.ts b/packages/metadata-protocol/src/protocol-loud-degradations.ts new file mode 100644 index 00000000000..25f379fe007 --- /dev/null +++ b/packages/metadata-protocol/src/protocol-loud-degradations.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 33] Two degradations said out loud, from + * `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole: + * + * - `throwMetadataServiceUnavailable` — a degraded MetadataService read + * turned into the same 503 the overlay half of the reads throws; + * - `warnClosureReadUnavailable` — why the batch closure degraded, once per + * process. + * + * A MOVE, not a redesign. Each function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited. + * + * ## How the moved bodies reach the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps each method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link LoudDegradationsHost} members it reads, plus a `.call`; a body + * that reads no member is called as a plain function. The warned-once flag is + * handed over as a getter and a setter, so the read and the write reach the + * class field as it stands at call time. Every caller keeps calling the class + * methods. + * + * What the bodies read from elsewhere, they import from where `protocol.ts` + * imports it; `metadataStoreUnavailableError` is `protocol.ts`'s own and is imported from there. Nothing re-exports this module, so the published surface does + * not move. + */ + +import type { ObjectStackProtocolImplementation } from './protocol.js'; +import { metadataStoreUnavailableError } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the functions below read — + * every one of them, and no other; each function's `this` is the `Pick` of + * the members it reads. Each is typed AS the class member (an indexed access + * on the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface LoudDegradationsHost { + closureReadWarned: ObjectStackProtocolImplementation['closureReadWarned']; +} + +/** The body of `ObjectStackProtocolImplementation#throwMetadataServiceUnavailable`; the method's docblock there is its contract. */ +export function throwMetadataServiceUnavailable(errors: string[]): never { + throw metadataStoreUnavailableError( + new Error( + `The metadata service could not read every loader: ${ + errors.length > 0 ? errors.join('; ') : 'no loader detail reported' + }`, + ), + ); +} + +/** The body of `ObjectStackProtocolImplementation#warnClosureReadUnavailable`; the method's docblock there is its contract. */ +export function warnClosureReadUnavailable(this: Pick, type: string, name: string): void { + if (this.closureReadWarned) return; + this.closureReadWarned = true; + console.warn( + `[Protocol] publishPackageDrafts: this overlay repository declares no 'get', so the batch's own ` + + `pending drafts cannot be read (first reached at ${type}/${name}). Author-time validation falls ` + + `back to the LIVE declarations only, without this batch's own drafts in the closure, so a draft that references a ` + + `sibling drafted in the SAME batch may be refused as unresolved. Nothing is published unchecked: ` + + `the gate still runs, with strictly less resolution context.`, + ); +} diff --git a/packages/metadata-protocol/src/protocol-overlay-index.ts b/packages/metadata-protocol/src/protocol-overlay-index.ts new file mode 100644 index 00000000000..76654c78aa8 --- /dev/null +++ b/packages/metadata-protocol/src/protocol-overlay-index.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 33] `sys_metadata`'s overlay-uniqueness indexes and the + * bundle-member backfill they need first, each run once per process, from + * `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole: + * `ensureOverlayIndex` and `ensureOverlayMembersBackfilled`. + * + * A MOVE, not a redesign. Each function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited. + * + * ## How the moved bodies reach the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps each method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link OverlayIndexHost} members it reads, plus a `.call`; a body that + * reads no member is called as a plain function. The `engine` field and the + * two run-once flags are handed over as getters (and the flags as setters), + * so each read and write reaches the class field as it stands at call time; + * the call to `ensureOverlayMembersBackfilled` is a closure, so it still + * reaches the class method. Every caller keeps calling the class methods. + * + * What the bodies read from elsewhere, they import from where `protocol.ts` + * imports it. Nothing re-exports this module, so the published surface does + * not move. + */ + +import { driverCanRunSql, resolveDriverExec } from './migrations/driver-exec.js'; +import { backfillOverlayBundleMembers, ensureMetadataOverlayIndexes } from './migrations/overlay-index.js'; +import type { ObjectStackProtocolImplementation } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the functions below read — + * every one of them, and no other; each function's `this` is the `Pick` of + * the members it reads. Each is typed AS the class member (an indexed access + * on the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface OverlayIndexHost { + readonly engine: ObjectStackProtocolImplementation['engine']; + readonly ensureOverlayMembersBackfilled: ObjectStackProtocolImplementation['ensureOverlayMembersBackfilled']; + overlayIndexEnsured: ObjectStackProtocolImplementation['overlayIndexEnsured']; + overlayMembersBackfilled: ObjectStackProtocolImplementation['overlayMembersBackfilled']; +} + +/** The body of `ObjectStackProtocolImplementation#ensureOverlayIndex`. */ +export async function ensureOverlayIndex(this: Pick): Promise { + if (this.overlayIndexEnsured) return; + this.overlayIndexEnsured = true; + // [#22877] Rows stored before the member column existed get their + // body's member before the first write can address them. + await this.ensureOverlayMembersBackfilled(); + try { + const engineAny = this.engine as any; + let driver: any = engineAny?.driver ?? engineAny?.getDriver?.(); + if (!driver && engineAny?.drivers instanceof Map) { + for (const candidate of engineAny.drivers.values()) { + if (driverCanRunSql(candidate)) { + driver = candidate; + break; + } + } + } + if (!driver) return; + const resolved = resolveDriverExec(driver); + // The refusal stays INSIDE the seam rather than becoming an early + // return: a driver taken from `engineAny?.driver` is not + // capability-checked above, and when it offers neither surface it is + // the migration that classifies and reports the failure — an early + // return here would put that back into the silent `catch` below. + const exec = async (sql: string): Promise => { + if (!resolved) throw new Error('driver has neither raw nor execute'); + await resolved(sql); + }; + // `console` satisfies the logger surface structurally; this class + // carries no injected logger, and its own diagnostics go to + // `console.warn` throughout (see `emitMetadataMutation`). + await ensureMetadataOverlayIndexes(exec, console); + } catch { + // A boot must never fail over an index. Note this arm is now only + // reachable for driver RESOLUTION failures: every DDL failure past + // this point is classified and reported by the migration itself, + // instead of vanishing into an empty catch (#6418). + } +} + +/** The body of `ObjectStackProtocolImplementation#ensureOverlayMembersBackfilled`. */ +export async function ensureOverlayMembersBackfilled(this: Pick): Promise { + if (this.overlayMembersBackfilled) return; + this.overlayMembersBackfilled = true; + await backfillOverlayBundleMembers(this.engine as any, console); +} diff --git a/packages/metadata-protocol/src/protocol-package-commit-record.ts b/packages/metadata-protocol/src/protocol-package-commit-record.ts new file mode 100644 index 00000000000..65b82042b76 --- /dev/null +++ b/packages/metadata-protocol/src/protocol-package-commit-record.ts @@ -0,0 +1,137 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 33] The ADR-0067 commit row that groups a turn's published + * artifacts, from `ObjectStackProtocolImplementation`, moved out of + * `protocol.ts` whole: `recordPackageCommit` and the named INSERT it calls, + * `persistPackageCommitRow` (named so the durability log-level rule can + * declare it). + * + * A MOVE, not a redesign. Each function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited. + * + * ## How the moved bodies reach the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps each method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link PackageCommitRecordHost} members it reads, plus a `.call`; a + * body that reads no member is called as a plain function. The `engine` field + * and the noted-once flag are handed over as getters (and the flag as a + * setter), so each read and write reaches the class field as it stands at + * call time; the call to `persistPackageCommitRow` is a closure, so it still + * reaches the class method. Every caller keeps calling the class methods. + * + * What the bodies read from elsewhere, they import from where `protocol.ts` + * imports it. Nothing re-exports this module, so the published surface does + * not move. + */ + +import { isMissingTableError } from '@objectstack/metadata/errors'; +import type { CommitPlanItem, ObjectStackProtocolImplementation } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the functions below read — + * every one of them, and no other; each function's `this` is the `Pick` of + * the members it reads. Each is typed AS the class member (an indexed access + * on the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface PackageCommitRecordHost { + commitStoreUnprovisionedNoted: ObjectStackProtocolImplementation['commitStoreUnprovisionedNoted']; + readonly engine: ObjectStackProtocolImplementation['engine']; + readonly persistPackageCommitRow: ObjectStackProtocolImplementation['persistPackageCommitRow']; +} + +/** The body of `ObjectStackProtocolImplementation#persistPackageCommitRow`; the method's docblock there is its contract. */ +export async function persistPackageCommitRow(this: Pick, row: Record): Promise { + // [#21911] The explicit system opt-in — see recordMetadataAudit. + await this.engine.insert('sys_metadata_commit', row, { context: { isSystem: true } }); +} + +/** The body of `ObjectStackProtocolImplementation#recordPackageCommit`; the method's docblock there is its contract. */ +export async function recordPackageCommit(this: Pick, args: { + packageId: string; + operation: 'apply' | 'revert'; + message?: string; + actor?: string; + aiModel?: string; + parentCommitId?: string; + items: CommitPlanItem[]; + eventSeqStart?: number; + eventSeqEnd?: number; +}): Promise<{ commitId: string } | null> { + try { + const commitId = 'cmt_' + (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' + ? crypto.randomUUID() + : `${args.eventSeqEnd ?? 0}-${args.items.length}-${args.packageId}`); + await this.persistPackageCommitRow({ + id: commitId, + package_id: args.packageId, + operation: args.operation, + ...(args.message ? { message: args.message } : {}), + ...(args.actor ? { actor: args.actor } : {}), + ...(args.aiModel ? { ai_model: args.aiModel } : {}), + ...(args.parentCommitId ? { parent_commit_id: args.parentCommitId } : {}), + ...(args.eventSeqStart !== undefined ? { event_seq_start: args.eventSeqStart } : {}), + ...(args.eventSeqEnd !== undefined ? { event_seq_end: args.eventSeqEnd } : {}), + items: JSON.stringify(args.items), + item_count: args.items.length, + created_at: new Date().toISOString(), + }); + return { commitId }; + } catch (error) { + // [#9066] The publish keeps its outcome — `null` still comes back + // and the caller still reports success — but the silence is over. + // + // BENIGN: `sys_metadata_commit` was never provisioned. That is a + // real deployment state (a first boot, or an env kernel composed + // without the commit log — the exact case + // `@objectstack/metadata`'s plugin note records), and it is a + // CONFIGURATION fact, not a store that broke: it is the same on + // every publish and it is fixed in one place. Stated once per + // instance, at `info`, per this card's ruling that an unprovisioned + // commit store stays silent or informational. + // + // EVERYTHING ELSE — a connection drop, a timeout, a permission + // denial, schema drift on that one table — is a write that was + // supposed to land and did not, while the publish it describes DID. + // `error`, per AGENTS.md "Degradation log levels", and the line + // owes the two things that rule requires: the CONSEQUENCE (this + // turn is not revertible, and the system will keep looking healthy) + // and the FIX (repair write access to the commit store). + // + // NOT rethrown, and not surfaced to the caller: the artifacts are + // already live and unwinding them over a missing history row would + // be strictly worse than losing the row. Telling the CALLER that + // the turn is unrevertible is a separate question (a response-field + // change the #8896 ruling forbids for this family) and deliberately + // NOT decided here. + if (isMissingTableError(error, 'sys_metadata_commit')) { + if (!this.commitStoreUnprovisionedNoted) { + this.commitStoreUnprovisionedNoted = true; + console.info( + '[Protocol] sys_metadata_commit is not provisioned — publishes and reverts ' + + 'succeed but record no ADR-0067 commit row, so no turn can be reverted ' + + '(the revert plan of every turn is dropped). Fix: provision the commit ' + + 'store — it is registered alongside sys_metadata_history by ' + + "@objectstack/metadata's plugin; run schema sync. Said once per protocol instance.", + ); + } + return null; + } + const reason = (error as { message?: string } | undefined)?.message ?? String(error); + console.error( + `[Protocol] sys_metadata_commit write FAILED for package '${args.packageId}' ` + + `(${args.operation}, ${args.items.length} item(s)): ${reason}. ` + + 'The publish itself SUCCEEDED and reports success, so nothing looks broken — but ' + + "this turn's revert plan was not persisted: the turn is absent from the package " + + 'commit timeline and can never be reverted, and every further publish loses its ' + + 'own the same way until this is repaired. Fix: restore write access to ' + + 'sys_metadata_commit (connectivity, permissions, or schema drift on that table).', + ); + return null; + } +} diff --git a/packages/metadata-protocol/src/protocol-package-scope.ts b/packages/metadata-protocol/src/protocol-package-scope.ts new file mode 100644 index 00000000000..9765fb15ec1 --- /dev/null +++ b/packages/metadata-protocol/src/protocol-package-scope.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 33] The package-scope answers, from + * `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole: + * + * - `isWritablePackage` — whether a package id is a writable base; + * - `uninstalledNamedBaseRefusal` — the named-base rule: a base an authoring + * write names must be an installed package; + * - `shouldDropStorage` — whether a delete may drop a physical table; + * - `resolveAppPackageIdKey` — the app item read's package-id key. + * + * A MOVE, not a redesign. Each function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited. + * + * ## How the moved bodies reach the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps each method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link PackageScopeHost} members it reads, plus a `.call`; a body that + * reads no member is called as a plain function. Fields are handed over as + * getters, so each read finds the field as it stands at call time; each call + * to another member is a closure, so it still reaches the class method. Every + * caller keeps calling the class methods. + * + * What the bodies read from elsewhere, they import from where `protocol.ts` + * imports it. Nothing re-exports this module, so the published surface does + * not move. + */ + +// [#23183] The app item read's package-id key — the one spelling of the arm +// `GetMetaItemRequestSchema.name` declares ({@link resolveAppPackageIdKey}). +import { isAppPackageIdKey } from '@objectstack/spec/api'; +import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared'; +// [#7560] ADR-0070's read-only-package rule, shared with the `/packages` +// lifecycle gate in `@objectstack/runtime` — see `./package-writability.js`. +import { isWritablePackage as isWritablePackageShared } from './package-writability.js'; +import type { ObjectStackProtocolImplementation } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the functions below read — + * every one of them, and no other; each function's `this` is the `Pick` of + * the members it reads. Each is typed AS the class member (an indexed access + * on the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface PackageScopeHost { + readonly engine: ObjectStackProtocolImplementation['engine']; + readonly getMetaItems: ObjectStackProtocolImplementation['getMetaItems']; + readonly reportedMultiAppPackages: ObjectStackProtocolImplementation['reportedMultiAppPackages']; + readonly resolveWritePackageScope: ObjectStackProtocolImplementation['resolveWritePackageScope']; +} + +/** The body of `ObjectStackProtocolImplementation#resolveAppPackageIdKey`; the method's docblock there is its contract. */ +export async function resolveAppPackageIdKey(this: Pick, request: R): Promise { + if (!isAppPackageIdKey(request.type, request.name)) return request; + const packageId = request.name; + if (request.packageId !== undefined && request.packageId !== packageId) { + const err: any = new Error( + `The app read's key '${packageId}' is a package id, and it names a different package ` + + `than packageId '${request.packageId}'. Name the package once: read the app by its ` + + `package id alone, or by its own app name with packageId as its scope.`, + ); + err.code = 'INVALID_REQUEST'; + err.status = 400; + throw err; + } + const readsDrafts = request.state === 'draft' || request.previewDrafts === true; + const listed = await this.getMetaItems({ + type: 'app', + packageId, + ...(readsDrafts ? { previewDrafts: true } : {}), + }); + const names = [...new Set( + ((listed as { items?: unknown[] } | undefined)?.items ?? []) + .map((app) => (app as { name?: unknown } | null)?.name) + .filter((name): name is string => typeof name === 'string' && name.length > 0), + )]; + if (names.length === 1) return { ...request, name: names[0], packageId }; + if (names.length > 1) { + const key = `${packageId}|${[...names].sort().join(',')}`; + if (!this.reportedMultiAppPackages.has(key)) { + this.reportedMultiAppPackages.add(key); + console.warn( + `[metadata-protocol] Package '${packageId}' holds ${names.length} apps ` + + `(${[...names].sort().join(', ')}), so reading the app by its package id answers ` + + `"not found": with one app per package the app is the package container ` + + `(ADR-0048 §3.1), and a package may define at most one app (ADR-0019 D3). ` + + `Fold them into one app, or move all but one into packages of their own.`, + ); + } + } + return request; +} + +/** The body of `ObjectStackProtocolImplementation#uninstalledNamedBaseRefusal`; the method's docblock there is its contract. */ +export function uninstalledNamedBaseRefusal( + this: Pick, + request: { type: string; name: string; packageId?: string | null }, + how: { verb?: 'save' | 'publish'; inherited?: boolean } = {}, +): Error | null { + const namedBase = request.packageId; + if (!namedBase || namedBase === 'sys_metadata') return null; + if (this.resolveWritePackageScope(namedBase) !== undefined) return null; + const type = PLURAL_TO_SINGULAR[request.type] ?? request.type; + // Kept under the REST door's client-message bound (`truncateClientMessage`) + // for ordinary names, so the last sentence reaches the caller. + const storedWayOut = `Create a package with that id first, or remove the rows bound to it (a delete naming ` + + `that package; discard-drafts for drafts) and save again, into an installed writable package or with ` + + `no package.`; + const err = new Error( + how.verb === 'publish' + ? `Cannot publish ${type} '${request.name}' into package '${namedBase}': no package with that id ` + + `is installed in this deployment, so the ${type} would go live bound to a package that does ` + + `not exist. ${storedWayOut} Nothing was published.` + : how.inherited + ? `Cannot save ${type} '${request.name}' without naming a package: its stored rows are bound to ` + + `package '${namedBase}', which is not installed in this deployment, and this save would ` + + `extend that binding. ${storedWayOut} Nothing was written.` + : `Cannot save ${type} '${request.name}' into package '${namedBase}': no package with that id is ` + + `installed in this deployment, so the ${type} would be stored bound to a package that does not ` + + `exist. Name an installed writable package as the ${type}'s base (or create one first), or save ` + + `the ${type} without naming a package. Nothing was written.`, + ); + (err as any).code = 'WRITABLE_PACKAGE_REQUIRED'; + (err as any).status = 422; + (err as any).packageId = namedBase; + (err as any).docs = 'docs/adr/0070-package-first-authoring.md'; + return err; +} + +/** The body of `ObjectStackProtocolImplementation#isWritablePackage`; the method's docblock there is its contract. */ +export function isWritablePackage(this: Pick, packageId: string | null | undefined): boolean { + return isWritablePackageShared(this.engine, packageId); +} + +/** The body of `ObjectStackProtocolImplementation#shouldDropStorage`; the method's docblock there is its contract. */ +export function shouldDropStorage(type: string, name: string, dropStorage: boolean | undefined, state: 'active' | 'draft'): boolean { + if (!dropStorage) return false; + const singular = PLURAL_TO_SINGULAR[type] ?? type; + if (singular !== 'object') return false; + if (state !== 'active') return false; + if (name.startsWith('sys_')) return false; + return true; +} diff --git a/packages/metadata-protocol/src/protocol-registry-view-predicates.ts b/packages/metadata-protocol/src/protocol-registry-view-predicates.ts new file mode 100644 index 00000000000..dfe70dda07f --- /dev/null +++ b/packages/metadata-protocol/src/protocol-registry-view-predicates.ts @@ -0,0 +1,122 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 33] The registry and view-container predicates, from + * `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole: + * + * - `expandStoredViewContainers` — every item the stored view containers in + * a row list expand, each paired with its row; + * - `runtimeViewContainerObject` — the object a runtime view container binds; + * - `copiesOwnShippedViewContainer` — whether a stored container copies one + * its own package ships; + * - `isAnotherPackagesObject` — whether a code package other than the + * container's owns its object; + * - `registryTakesStoredRows` — whether this protocol's engine registry takes + * the rows this kernel stores; + * - `overlaidShippedContainerViewNames` — the view names of the shipped + * container a container row overlays by its own name. + * + * A MOVE, not a redesign. Each function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited. + * + * ## How the moved bodies reach the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps each method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link RegistryViewPredicatesHost} members it reads, plus a `.call`; a + * body that reads no member is called as a plain function. Fields are handed + * over as getters, so each read finds the field as it stands at call time; + * each call to another member is a closure, so it still reaches the class + * method. Every caller keeps calling the class methods. + * + * What the bodies read from elsewhere, they import from where `protocol.ts` + * imports it. Nothing re-exports this module, so the published surface does + * not move. + */ + +import type { ObjectStackProtocolImplementation } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the functions below read — + * every one of them, and no other; each function's `this` is the `Pick` of + * the members it reads. Each is typed AS the class member (an indexed access + * on the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface RegistryViewPredicatesHost { + readonly engine: ObjectStackProtocolImplementation['engine']; + readonly environmentId: ObjectStackProtocolImplementation['environmentId']; + readonly expandRuntimeViewContainer: ObjectStackProtocolImplementation['expandRuntimeViewContainer']; + readonly runtimeViewContainerObject: ObjectStackProtocolImplementation['runtimeViewContainerObject']; + readonly runtimeViewContainerPackage: ObjectStackProtocolImplementation['runtimeViewContainerPackage']; + readonly shippedViewContainerOf: ObjectStackProtocolImplementation['shippedViewContainerOf']; + readonly storeHydratedIntoRegistry: ObjectStackProtocolImplementation['storeHydratedIntoRegistry']; +} + +/** The body of `ObjectStackProtocolImplementation#expandStoredViewContainers`; the method's docblock there is its contract. */ +export function expandStoredViewContainers( + this: Pick, + type: string, + overlays: readonly E[], +): Array<{ item: Record; container: E }> { + const out: Array<{ item: Record; container: E }> = []; + for (const container of overlays) { + for (const item of this.expandRuntimeViewContainer(type, container.data, { packageId: container.packageId })) { + out.push({ item, container }); + } + } + return out; +} + +/** The body of `ObjectStackProtocolImplementation#runtimeViewContainerObject`; the method's docblock there is its contract. */ +export function runtimeViewContainerObject(container: Record): string | undefined { + return (typeof container.object === 'string' && container.object ? container.object : undefined) + ?? container?.list?.data?.object + ?? container?.form?.data?.object + ?? (typeof container.name === 'string' ? container.name : undefined); +} + +/** The body of `ObjectStackProtocolImplementation#copiesOwnShippedViewContainer`; the method's docblock there is its contract. */ +export function copiesOwnShippedViewContainer( + this: Pick, + type: string, + object: string, + container: Record, + ownPackageId: string | undefined, +): boolean { + const shipped = this.shippedViewContainerOf(type, container.name, ownPackageId); + return shipped !== undefined && this.runtimeViewContainerObject(shipped) === object; +} + +/** The body of `ObjectStackProtocolImplementation#isAnotherPackagesObject`; the method's docblock there is its contract. */ +export function isAnotherPackagesObject(this: Pick, object: string, ownPackageId: string | undefined): boolean { + const registry: any = (this.engine as any)?.registry; + const owner = typeof registry?.getPackagedObjectOwner === 'function' + ? registry.getPackagedObjectOwner(object) + : undefined; + const ownerPackageId: unknown = owner?.packageId; + return typeof ownerPackageId === 'string' && ownerPackageId !== '' && ownerPackageId !== ownPackageId; +} + +/** The body of `ObjectStackProtocolImplementation#registryTakesStoredRows`; the method's docblock there is its contract. */ +export function registryTakesStoredRows(this: Pick): boolean { + return this.environmentId === undefined || this.storeHydratedIntoRegistry; +} + +/** The body of `ObjectStackProtocolImplementation#overlaidShippedContainerViewNames`; the method's docblock there is its contract. */ +export function overlaidShippedContainerViewNames( + this: Pick, + type: string, + container: Record, + packageId: string | null | undefined, +): ReadonlySet { + const ownPackageId = this.runtimeViewContainerPackage(type, container, { packageId }); + const shipped = this.shippedViewContainerOf(type, container.name, ownPackageId); + if (shipped === undefined) return new Set(); + return new Set( + this.expandRuntimeViewContainer(type, shipped, { packageId: ownPackageId }).map((view) => String(view.name)), + ); +} diff --git a/packages/metadata-protocol/src/protocol-sdui-manifest.ts b/packages/metadata-protocol/src/protocol-sdui-manifest.ts new file mode 100644 index 00000000000..de4050f1347 --- /dev/null +++ b/packages/metadata-protocol/src/protocol-sdui-manifest.ts @@ -0,0 +1,77 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 33] The deployment's ADR-0080 SDUI component manifest, from + * `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole: + * `resolveSduiManifest`, read per publish, and + * `reportPageRequiresAbsentAtLoad`, the load half of the plugin-presence + * check that reads it. + * + * A MOVE, not a redesign. Each function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited. + * + * ## How the moved bodies reach the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps each method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link SduiManifestHost} members it reads, plus a `.call`; a body that + * reads no member is called as a plain function. The services registry and + * the warned-once flag are handed over as getters (the flag also as a + * setter), so each read and write reaches the class field as it stands at + * call time. Every caller keeps calling the class methods. + * + * What the bodies read from elsewhere, they import from where `protocol.ts` + * imports it. Nothing re-exports this module, so the published surface does + * not move. + */ + +import type { ObjectStackProtocolImplementation } from './protocol.js'; +import { SDUI_MANIFEST_SERVICE, findPageRequiresAbsentFromManifest, isUsableSduiManifest } from './runtime-authoring-gate.js'; + +/** + * The `ObjectStackProtocolImplementation` members the functions below read — + * every one of them, and no other; each function's `this` is the `Pick` of + * the members it reads. Each is typed AS the class member (an indexed access + * on the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface SduiManifestHost { + readonly getServicesRegistry: ObjectStackProtocolImplementation['getServicesRegistry']; + unusableSduiManifestWarned: ObjectStackProtocolImplementation['unusableSduiManifestWarned']; +} + +/** The body of `ObjectStackProtocolImplementation#resolveSduiManifest`; the method's docblock there is its contract. */ +export function resolveSduiManifest(this: Pick): unknown { + const value = this.getServicesRegistry?.().get(SDUI_MANIFEST_SERVICE); + if (value === undefined || isUsableSduiManifest(value)) return value; + if (!this.unusableSduiManifestWarned) { + this.unusableSduiManifestWarned = true; + console.warn( + `[Protocol] the '${SDUI_MANIFEST_SERVICE}' service is not an SDUI component manifest ` + + `(a JSON object with a \`components\` map) — html page sources are saved without being ` + + `compiled against it, and a page's \`requires\` is not validated. Register the parsed ` + + `sdui.manifest.json of the console this deployment serves.`, + ); + } + return undefined; +} + +/** The body of `ObjectStackProtocolImplementation#reportPageRequiresAbsentAtLoad`; the method's docblock there is its contract. */ +export function reportPageRequiresAbsentAtLoad(name: string, body: unknown, sduiManifest: unknown): void { + const absent = findPageRequiresAbsentFromManifest('page', body, sduiManifest); + if (!absent || absent.length === 0) return; + const one = absent.length === 1; + const named = absent.map((ns) => `'${ns}'`).join(', '); + console.warn( + `[Protocol] [page_requires_plugin_absent] stored page/${name} requires ${named}, ` + + `${one ? 'a namespace' : 'namespaces'} no component in this deployment's SDUI component manifest ` + + `carries: the plugin${one ? ' that provides it is' : 's that provide them are'} not loaded in the ` + + `console this deployment serves. The page is loaded and served anyway, and what its source draws ` + + `from ${one ? 'that plugin' : 'those plugins'} will not render. Install ${one ? 'it' : 'them'} in ` + + `that console, or take ${one ? 'its' : 'their'} components out of the page's source and save the ` + + `page without \`requires\` — it is derived from the source at save.`, + ); +} diff --git a/packages/metadata-protocol/src/protocol-stored-row-bindings.ts b/packages/metadata-protocol/src/protocol-stored-row-bindings.ts new file mode 100644 index 00000000000..ba66651e58e --- /dev/null +++ b/packages/metadata-protocol/src/protocol-stored-row-bindings.ts @@ -0,0 +1,113 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 33] What a stored `sys_metadata` overlay row binds, read off the + * stored rows, from `ObjectStackProtocolImplementation`, moved out of + * `protocol.ts` whole: + * + * - `overlayLockLayerAt` — the item-lock resolution's `overlay` layer for an + * item's address, over its stored ACTIVE rows; + * - `resolveOverlayPackageBinding` — the package binding of a persisted + * overlay row, read from the row itself; + * - `storedRowKnownDatasources` — the datasource a stored `object` row's + * `external` binding names, for the stored-migration refused-shape listing. + * + * A MOVE, not a redesign. Each function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited. + * + * ## How the moved bodies reach the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps each method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link StoredRowBindingsHost} members it reads, plus a `.call`; a body + * that reads no member is called as a plain function. Each call to another + * member is handed over as a closure, so it still reaches the class method. + * Every caller keeps calling the class methods. + * + * What the bodies read from elsewhere, they import from where `protocol.ts` + * imports it; `rowMatchesOverlayWhere` is `protocol.ts`'s own and is imported from there. Nothing re-exports this module, so the published surface does + * not move. + */ + +import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from '@objectstack/spec/shared'; +import type { ItemAddress, StoredOverlayRow } from './item-lock.js'; +import { resolveOverlayLockLayer } from './item-lock.js'; +import { overlayMemberWhere } from './migrations/overlay-index.js'; +import type { ObjectStackProtocolImplementation } from './protocol.js'; +import { rowMatchesOverlayWhere } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the functions below read — + * every one of them, and no other; each function's `this` is the `Pick` of + * the members it reads. Each is typed AS the class member (an indexed access + * on the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface StoredRowBindingsHost { + readonly externalBindingDatasourceDefinition: ObjectStackProtocolImplementation['externalBindingDatasourceDefinition']; + readonly findEnvironmentRow: ObjectStackProtocolImplementation['findEnvironmentRow']; + readonly findEnvironmentRows: ObjectStackProtocolImplementation['findEnvironmentRows']; +} + +/** The body of `ObjectStackProtocolImplementation#storedRowKnownDatasources`; the method's docblock there is its contract. */ +export async function storedRowKnownDatasources( + this: Pick, + singularType: string, + body: unknown, +): Promise | undefined> { + if (singularType !== 'object' || !body || typeof body !== 'object' || Array.isArray(body)) return undefined; + const { external, datasource } = body as { external?: unknown; datasource?: unknown }; + if (external === undefined || external === null) return undefined; + if (typeof datasource !== 'string' || datasource === '') return undefined; + const definition = await this.externalBindingDatasourceDefinition(datasource); + return definition ? new Map([[datasource, definition]]) : undefined; +} + +/** The body of `ObjectStackProtocolImplementation#overlayLockLayerAt`; the method's docblock there is its contract. */ +export function overlayLockLayerAt( + this: Pick, + address: ItemAddress, + options: { readonly otherSpelling: boolean }, + activeRows?: readonly StoredOverlayRow[], +): Promise { + const other = PLURAL_TO_SINGULAR[address.type] ?? SINGULAR_TO_PLURAL[address.type]; + return resolveOverlayLockLayer(address, async (spelling) => { + const type = spelling === 'canonical' ? address.type : other; + if (type === undefined) return []; + const where = { type, name: address.name, state: 'active' }; + if (activeRows !== undefined) return activeRows.filter((row) => rowMatchesOverlayWhere(row, where)); + // [#21911] The explicit system opt-in — see findServedOverlayRow. + const rows: StoredOverlayRow[] | null | undefined = await this.findEnvironmentRows('sys_metadata', { + where, + context: { isSystem: true }, + }); + return rows ?? []; + }, options); +} + +/** The body of `ObjectStackProtocolImplementation#resolveOverlayPackageBinding`; the method's docblock there is its contract. */ +export async function resolveOverlayPackageBinding( + this: Pick, + type: string, + name: string, + /** + * [#22877] The bundle member whose row's binding to read; omitted, the + * canonical member. Nothing for a type with no members. + */ + member?: string, +): Promise { + // [#21911] The explicit system opt-in — see findServedOverlayRow. + const row = await this.findEnvironmentRow('sys_metadata', { + where: { + type, + name, + state: 'active', + ...overlayMemberWhere(type, member), + }, + context: { isSystem: true }, + }); + return (row as { package_id?: string | null } | null)?.package_id ?? null; +} diff --git a/packages/metadata-protocol/src/protocol-unmintable-meta-type.ts b/packages/metadata-protocol/src/protocol-unmintable-meta-type.ts new file mode 100644 index 00000000000..16d625d3e93 --- /dev/null +++ b/packages/metadata-protocol/src/protocol-unmintable-meta-type.ts @@ -0,0 +1,62 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 33] The `/meta` save door's refusal of a `:type` segment that is + * not a metadata type at all, on the one entry point that mints a + * `sys_metadata` namespace, from `ObjectStackProtocolImplementation`, moved + * out of `protocol.ts` whole: `refuseUnmintableMetaType`. + * + * A MOVE, not a redesign. Each function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited. + * + * ## How the moved bodies reach the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps each method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link UnmintableMetaTypeHost} members it reads, plus a `.call`; a body + * that reads no member is called as a plain function. Its call to + * `metaTypeNamespaceExists` is handed over as a closure, so it still reaches + * the class method, which stays. Every caller keeps calling the class + * methods. + * + * What the bodies read from elsewhere, they import from where `protocol.ts` + * imports it. Nothing re-exports this module, so the published surface does + * not move. + */ + +import { unrecognisedMetaTypeRefusal } from '@objectstack/spec/shared'; +import type { ObjectStackProtocolImplementation } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the function below reads — + * every one of them, and no other; the function's `this` is the `Pick` of them. Each is typed AS the class member (an indexed access + * on the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface UnmintableMetaTypeHost { + readonly metaTypeNamespaceExists: ObjectStackProtocolImplementation['metaTypeNamespaceExists']; +} + +/** The body of `ObjectStackProtocolImplementation#refuseUnmintableMetaType`; the method's docblock there is its contract. */ +export async function refuseUnmintableMetaType(this: Pick, request: { type: string, name: string }): Promise { + const unrecognised = unrecognisedMetaTypeRefusal(request.type); + if (!unrecognised) return; + // The old exemption 1 (skip when the name contains a slash) was + // removed by commit 311433f6b — the grammar verdict upstream refuses every + // slash-bearing name before this probe runs. See the header. + // Exemption 2 — the namespace predates this write. + if (await this.metaTypeNamespaceExists(unrecognised.type)) return; + const err = new Error( + `'${unrecognised.type}' is not a metadata type. The platform declares ` + + `no such type, and a plugin cannot declare one either: 'additionalTypes' was retired because ` + + `nothing ever read it — so this write would mint a sys_metadata namespace under ` + + `type='${unrecognised.type}' that nothing reads and nothing serves. Address a real ` + + `metadata type; GET /api/v1/meta/types lists the ones this deployment carries.`, + ); + (err as any).code = 'INVALID_REQUEST'; + (err as any).status = 400; + throw err; +} diff --git a/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts b/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts index bb14aa75562..a1dbbb098fa 100644 --- a/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts +++ b/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts @@ -93,6 +93,13 @@ const PRODUCERS: Readonly> = { // The package door's save refusal, moved out of `protocol.ts` whole on the // same terms. 'protocol-packaged-base-refusal.ts': 1, + // [#25467 cut 33] The named-base rule and the app package-id key, the `/meta` + // mint door's refusal, the MetadataService 503 and the overlay-index DDL + // probe, moved out of `protocol.ts` whole on the same terms. + 'protocol-package-scope.ts': 2, + 'protocol-unmintable-meta-type.ts': 1, + 'protocol-loud-degradations.ts': 1, + 'protocol-overlay-index.ts': 1, 'sys-metadata-repository.ts': 5, 'runtime-authoring-gate.ts': 1, }; diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 18b1a6d90eb..9eac8c081df 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -24,25 +24,18 @@ import { stripUnauthorableProperties } from './unauthorable-nodes.js'; import { markErasedAuthoringInput } from './erased-authoring-mark.js'; import { SDUI_MANIFEST_SERVICE, - isUsableSduiManifest, stampHtmlPageRequires, - findPageRequiresAbsentFromManifest, type RuntimePendingDeclarations, } from './runtime-authoring-gate.js'; -// [#7560] ADR-0070's read-only-package rule, shared with the `/packages` -// lifecycle gate in `@objectstack/runtime` — see `./package-writability.js`. -import { isWritablePackage as isWritablePackageShared } from './package-writability.js'; import type { RuntimeAuthoringIssue } from './runtime-authoring-gate.js'; // [#6418] `sys_metadata`'s overlay-uniqueness indexes: probe-first DDL plus the // ADR-0120 D4 reporting that replaced this file's empty `catch` blocks. import { backfillOverlayBundleMembers, - ensureMetadataOverlayIndexes, overlayMemberOfBody, overlayMemberValue, overlayMemberWhere, } from './migrations/overlay-index.js'; -import { driverCanRunSql, resolveDriverExec } from './migrations/driver-exec.js'; import { DraftConflictError, SysMetadataRepository, @@ -132,9 +125,6 @@ import { planUnmanagedImport, unmanagedInstallPostureRefusal } from './package-u import type { MetadataCacheRequest, MetadataCacheResponse, ServiceInfo, ApiRoutes, WellKnownCapabilities, CapabilityDescriptor } from '@objectstack/spec/api'; import type { ApiError, BatchOperationResult } from '@objectstack/spec/api'; import { readServiceSelfInfo, readChannelRoute, readAuthFamilies, isSubscribableChannel, CHANNEL_SURFACE_SLOTS, ErrorCode, standardErrorCodeForHttpStatus, resolveDiscoveryEnvironment } from '@objectstack/spec/api'; -// [#23183] The app item read's package-id key — the one spelling of the arm -// `GetMetaItemRequestSchema.name` declares ({@link resolveAppPackageIdKey}). -import { isAppPackageIdKey } from '@objectstack/spec/api'; // [#24395] The save's authoring surface, and the draft row's per-key // provenance read back for the strict draft read. import { MetadataAuthoringSurfaceSchema, type MetadataAuthoringClient, type MetadataAuthoringSurface } from '@objectstack/spec/api'; @@ -429,6 +419,22 @@ import { foldObjectExtendersFromRegistry } from './protocol-object-extender-fold import { readMetaItemFace } from './protocol-meta-item-face.js'; // [#25467 cut 32] A stored commit row's `items` column read back as its revert plan. import { parseCommitItems } from './protocol-commit-items.js'; +// [#25467 cut 33] What a stored overlay row binds: the lock layer, the package binding, the external datasource. +import { overlayLockLayerAt, resolveOverlayPackageBinding, storedRowKnownDatasources } from './protocol-stored-row-bindings.js'; +// [#25467 cut 33] Two degradations said out loud: the MetadataService 503 and the batch closure's once-per-process warn. +import { throwMetadataServiceUnavailable, warnClosureReadUnavailable } from './protocol-loud-degradations.js'; +// [#25467 cut 33] The overlay-uniqueness indexes and the bundle-member backfill, once per process. +import { ensureOverlayIndex, ensureOverlayMembersBackfilled } from './protocol-overlay-index.js'; +// [#25467 cut 33] The registry and view-container predicates: stored container expansion, binding, ownership, registry scope, overlaid views. +import { copiesOwnShippedViewContainer, expandStoredViewContainers, isAnotherPackagesObject, overlaidShippedContainerViewNames, registryTakesStoredRows, runtimeViewContainerObject } from './protocol-registry-view-predicates.js'; +// [#25467 cut 33] The ADR-0067 commit row of a publish or revert turn, and its named INSERT. +import { persistPackageCommitRow, recordPackageCommit } from './protocol-package-commit-record.js'; +// [#25467 cut 33] Package scope: the writable base, the named-base rule, storage teardown, the app package-id key. +import { isWritablePackage, resolveAppPackageIdKey, shouldDropStorage, uninstalledNamedBaseRefusal } from './protocol-package-scope.js'; +// [#25467 cut 33] The /meta save door's refusal of a `:type` that is no metadata type at all. +import { refuseUnmintableMetaType } from './protocol-unmintable-meta-type.js'; +// [#25467 cut 33] The deployment's SDUI component manifest, and the load-time page `requires` report against it. +import { reportPageRequiresAbsentAtLoad, resolveSduiManifest } from './protocol-sdui-manifest.js'; /** * Canonical Zod schema per metadata type lives in @@ -2849,7 +2855,7 @@ function servedOverlayRowCandidates(address: { * candidates, the package-less binding and the lock's scope): every key equal, * and `null` matching an absent value as the store's IS NULL does. */ -function rowMatchesOverlayWhere(row: object, where: Readonly>): boolean { +export function rowMatchesOverlayWhere(row: object, where: Readonly>): boolean { const stored = row as Readonly>; return Object.entries(where).every(([key, value]) => (stored[key] ?? null) === value); } @@ -4169,7 +4175,7 @@ const METADATA_STORE_UNAVAILABLE_MESSAGE = * invented string, and no new ledger vocabulary for a distinction no consumer * measures today. */ -function metadataStoreUnavailableError(cause: unknown): Error { +export function metadataStoreUnavailableError(cause: unknown): Error { const err = new Error(METADATA_STORE_UNAVAILABLE_MESSAGE) as Error & { code?: string; status?: number; @@ -6556,12 +6562,11 @@ export class ObjectStackProtocolImplementation implements singularType: string, body: unknown, ): Promise | undefined> { - if (singularType !== 'object' || !body || typeof body !== 'object' || Array.isArray(body)) return undefined; - const { external, datasource } = body as { external?: unknown; datasource?: unknown }; - if (external === undefined || external === null) return undefined; - if (typeof datasource !== 'string' || datasource === '') return undefined; - const definition = await this.externalBindingDatasourceDefinition(datasource); - return definition ? new Map([[datasource, definition]]) : undefined; + // [#25467 cut 33] The body moved to `protocol-stored-row-bindings.ts` (see `StoredRowBindingsHost`). + const host: ThisParameterType = { + externalBindingDatasourceDefinition: (...a) => this.externalBindingDatasourceDefinition(...a), + }; + return storedRowKnownDatasources.call(host, singularType, body); } /** @@ -6748,18 +6753,16 @@ export class ObjectStackProtocolImplementation implements * warned about once and read as nothing, never compiled against. */ private resolveSduiManifest(): unknown { - const value = this.getServicesRegistry?.().get(SDUI_MANIFEST_SERVICE); - if (value === undefined || isUsableSduiManifest(value)) return value; - if (!this.unusableSduiManifestWarned) { - this.unusableSduiManifestWarned = true; - console.warn( - `[Protocol] the '${SDUI_MANIFEST_SERVICE}' service is not an SDUI component manifest ` - + `(a JSON object with a \`components\` map) — html page sources are saved without being ` - + `compiled against it, and a page's \`requires\` is not validated. Register the parsed ` - + `sdui.manifest.json of the console this deployment serves.`, - ); - } - return undefined; + // [#25467 cut 33] The body moved to `protocol-sdui-manifest.ts` (see `SduiManifestHost`). + const readGetServicesRegistry = () => this.getServicesRegistry; + const readUnusableSduiManifestWarned = () => this.unusableSduiManifestWarned; + const writeUnusableSduiManifestWarned = (value: ObjectStackProtocolImplementation['unusableSduiManifestWarned']) => { this.unusableSduiManifestWarned = value; }; + const host: ThisParameterType = { + get getServicesRegistry() { return readGetServicesRegistry(); }, + get unusableSduiManifestWarned() { return readUnusableSduiManifestWarned(); }, + set unusableSduiManifestWarned(value) { writeUnusableSduiManifestWarned(value); }, + }; + return resolveSduiManifest.call(host); } /** [#20312] One warn per process for an unusable registered manifest — see {@link resolveSduiManifest}. */ @@ -8015,43 +8018,17 @@ export class ObjectStackProtocolImplementation implements */ private overlayIndexEnsured = false; private async ensureOverlayIndex(): Promise { - if (this.overlayIndexEnsured) return; - this.overlayIndexEnsured = true; - // [#22877] Rows stored before the member column existed get their - // body's member before the first write can address them. - await this.ensureOverlayMembersBackfilled(); - try { - const engineAny = this.engine as any; - let driver: any = engineAny?.driver ?? engineAny?.getDriver?.(); - if (!driver && engineAny?.drivers instanceof Map) { - for (const candidate of engineAny.drivers.values()) { - if (driverCanRunSql(candidate)) { - driver = candidate; - break; - } - } - } - if (!driver) return; - const resolved = resolveDriverExec(driver); - // The refusal stays INSIDE the seam rather than becoming an early - // return: a driver taken from `engineAny?.driver` is not - // capability-checked above, and when it offers neither surface it is - // the migration that classifies and reports the failure — an early - // return here would put that back into the silent `catch` below. - const exec = async (sql: string): Promise => { - if (!resolved) throw new Error('driver has neither raw nor execute'); - await resolved(sql); - }; - // `console` satisfies the logger surface structurally; this class - // carries no injected logger, and its own diagnostics go to - // `console.warn` throughout (see `emitMetadataMutation`). - await ensureMetadataOverlayIndexes(exec, console); - } catch { - // A boot must never fail over an index. Note this arm is now only - // reachable for driver RESOLUTION failures: every DDL failure past - // this point is classified and reported by the migration itself, - // instead of vanishing into an empty catch (#6418). - } + // [#25467 cut 33] The body moved to `protocol-overlay-index.ts` (see `OverlayIndexHost`). + const readEngine = () => this.engine; + const readOverlayIndexEnsured = () => this.overlayIndexEnsured; + const writeOverlayIndexEnsured = (value: ObjectStackProtocolImplementation['overlayIndexEnsured']) => { this.overlayIndexEnsured = value; }; + const host: ThisParameterType = { + get engine() { return readEngine(); }, + ensureOverlayMembersBackfilled: (...a) => this.ensureOverlayMembersBackfilled(...a), + get overlayIndexEnsured() { return readOverlayIndexEnsured(); }, + set overlayIndexEnsured(value) { writeOverlayIndexEnsured(value); }, + }; + return ensureOverlayIndex.call(host); } /** @@ -8070,9 +8047,16 @@ export class ObjectStackProtocolImplementation implements */ private overlayMembersBackfilled = false; private async ensureOverlayMembersBackfilled(): Promise { - if (this.overlayMembersBackfilled) return; - this.overlayMembersBackfilled = true; - await backfillOverlayBundleMembers(this.engine as any, console); + // [#25467 cut 33] The body moved to `protocol-overlay-index.ts` (see `OverlayIndexHost`). + const readEngine = () => this.engine; + const readOverlayMembersBackfilled = () => this.overlayMembersBackfilled; + const writeOverlayMembersBackfilled = (value: ObjectStackProtocolImplementation['overlayMembersBackfilled']) => { this.overlayMembersBackfilled = value; }; + const host: ThisParameterType = { + get engine() { return readEngine(); }, + get overlayMembersBackfilled() { return readOverlayMembersBackfilled(); }, + set overlayMembersBackfilled(value) { writeOverlayMembersBackfilled(value); }, + }; + return ensureOverlayMembersBackfilled.call(host); } /** @@ -9405,13 +9389,8 @@ export class ObjectStackProtocolImplementation implements * `handleRouteError` / `logWithheldServerFault`. */ private throwMetadataServiceUnavailable(errors: string[]): never { - throw metadataStoreUnavailableError( - new Error( - `The metadata service could not read every loader: ${ - errors.length > 0 ? errors.join('; ') : 'no loader detail reported' - }`, - ), - ); + // [#25467 cut 33] The body moved to `protocol-loud-degradations.ts`; it reads no member. + return throwMetadataServiceUnavailable(errors); } async getMetaItems(request: { type: string; packageId?: string; previewDrafts?: boolean }) { @@ -10278,13 +10257,11 @@ export class ObjectStackProtocolImplementation implements type: string, overlays: readonly E[], ): Array<{ item: Record; container: E }> { - const out: Array<{ item: Record; container: E }> = []; - for (const container of overlays) { - for (const item of this.expandRuntimeViewContainer(type, container.data, { packageId: container.packageId })) { - out.push({ item, container }); - } - } - return out; + // [#25467 cut 33] The body moved to `protocol-registry-view-predicates.ts` (see `RegistryViewPredicatesHost`). + const host: ThisParameterType = { + expandRuntimeViewContainer: (...a) => this.expandRuntimeViewContainer(...a), + }; + return (expandStoredViewContainers).call(host, type, overlays); } /** @@ -10480,19 +10457,11 @@ export class ObjectStackProtocolImplementation implements options: { readonly otherSpelling: boolean }, activeRows?: readonly StoredOverlayRow[], ): Promise { - const other = PLURAL_TO_SINGULAR[address.type] ?? SINGULAR_TO_PLURAL[address.type]; - return resolveOverlayLockLayer(address, async (spelling) => { - const type = spelling === 'canonical' ? address.type : other; - if (type === undefined) return []; - const where = { type, name: address.name, state: 'active' }; - if (activeRows !== undefined) return activeRows.filter((row) => rowMatchesOverlayWhere(row, where)); - // [#21911] The explicit system opt-in — see findServedOverlayRow. - const rows: StoredOverlayRow[] | null | undefined = await this.findEnvironmentRows('sys_metadata', { - where, - context: { isSystem: true }, - }); - return rows ?? []; - }, options); + // [#25467 cut 33] The body moved to `protocol-stored-row-bindings.ts` (see `StoredRowBindingsHost`). + const host: ThisParameterType = { + findEnvironmentRows: (...a) => this.findEnvironmentRows(...a), + }; + return overlayLockLayerAt.call(host, address, options, activeRows); } /** @@ -10582,44 +10551,13 @@ export class ObjectStackProtocolImplementation implements private async resolveAppPackageIdKey(request: R): Promise { - if (!isAppPackageIdKey(request.type, request.name)) return request; - const packageId = request.name; - if (request.packageId !== undefined && request.packageId !== packageId) { - const err: any = new Error( - `The app read's key '${packageId}' is a package id, and it names a different package ` - + `than packageId '${request.packageId}'. Name the package once: read the app by its ` - + `package id alone, or by its own app name with packageId as its scope.`, - ); - err.code = 'INVALID_REQUEST'; - err.status = 400; - throw err; - } - const readsDrafts = request.state === 'draft' || request.previewDrafts === true; - const listed = await this.getMetaItems({ - type: 'app', - packageId, - ...(readsDrafts ? { previewDrafts: true } : {}), - }); - const names = [...new Set( - ((listed as { items?: unknown[] } | undefined)?.items ?? []) - .map((app) => (app as { name?: unknown } | null)?.name) - .filter((name): name is string => typeof name === 'string' && name.length > 0), - )]; - if (names.length === 1) return { ...request, name: names[0], packageId }; - if (names.length > 1) { - const key = `${packageId}|${[...names].sort().join(',')}`; - if (!this.reportedMultiAppPackages.has(key)) { - this.reportedMultiAppPackages.add(key); - console.warn( - `[metadata-protocol] Package '${packageId}' holds ${names.length} apps ` - + `(${[...names].sort().join(', ')}), so reading the app by its package id answers ` - + `"not found": with one app per package the app is the package container ` - + `(ADR-0048 §3.1), and a package may define at most one app (ADR-0019 D3). ` - + `Fold them into one app, or move all but one into packages of their own.`, - ); - } - } - return request; + // [#25467 cut 33] The body moved to `protocol-package-scope.ts` (see `PackageScopeHost`). + const readReportedMultiAppPackages = () => this.reportedMultiAppPackages; + const host: ThisParameterType = { + getMetaItems: (...a) => this.getMetaItems(...a), + get reportedMultiAppPackages() { return readReportedMultiAppPackages(); }, + }; + return (resolveAppPackageIdKey).call(host, request); } /** @@ -16237,34 +16175,11 @@ export class ObjectStackProtocolImplementation implements request: { type: string; name: string; packageId?: string | null }, how: { verb?: 'save' | 'publish'; inherited?: boolean } = {}, ): Error | null { - const namedBase = request.packageId; - if (!namedBase || namedBase === 'sys_metadata') return null; - if (this.resolveWritePackageScope(namedBase) !== undefined) return null; - const type = PLURAL_TO_SINGULAR[request.type] ?? request.type; - // Kept under the REST door's client-message bound (`truncateClientMessage`) - // for ordinary names, so the last sentence reaches the caller. - const storedWayOut = `Create a package with that id first, or remove the rows bound to it (a delete naming ` - + `that package; discard-drafts for drafts) and save again, into an installed writable package or with ` - + `no package.`; - const err = new Error( - how.verb === 'publish' - ? `Cannot publish ${type} '${request.name}' into package '${namedBase}': no package with that id ` - + `is installed in this deployment, so the ${type} would go live bound to a package that does ` - + `not exist. ${storedWayOut} Nothing was published.` - : how.inherited - ? `Cannot save ${type} '${request.name}' without naming a package: its stored rows are bound to ` - + `package '${namedBase}', which is not installed in this deployment, and this save would ` - + `extend that binding. ${storedWayOut} Nothing was written.` - : `Cannot save ${type} '${request.name}' into package '${namedBase}': no package with that id is ` - + `installed in this deployment, so the ${type} would be stored bound to a package that does not ` - + `exist. Name an installed writable package as the ${type}'s base (or create one first), or save ` - + `the ${type} without naming a package. Nothing was written.`, - ); - (err as any).code = 'WRITABLE_PACKAGE_REQUIRED'; - (err as any).status = 422; - (err as any).packageId = namedBase; - (err as any).docs = 'docs/adr/0070-package-first-authoring.md'; - return err; + // [#25467 cut 33] The body moved to `protocol-package-scope.ts` (see `PackageScopeHost`). + const host: ThisParameterType = { + resolveWritePackageScope: (...a) => this.resolveWritePackageScope(...a), + }; + return uninstalledNamedBaseRefusal.call(host, request, how); } /** @@ -16575,7 +16490,12 @@ export class ObjectStackProtocolImplementation implements * definition, and its doc comment carries the reasoning. */ private isWritablePackage(packageId: string | null | undefined): boolean { - return isWritablePackageShared(this.engine, packageId); + // [#25467 cut 33] The body moved to `protocol-package-scope.ts` (see `PackageScopeHost`). + const readEngine = () => this.engine; + const host: ThisParameterType = { + get engine() { return readEngine(); }, + }; + return isWritablePackage.call(host, packageId); } /** @@ -18207,10 +18127,8 @@ export class ObjectStackProtocolImplementation implements * derivation". */ private runtimeViewContainerObject(container: Record): string | undefined { - return (typeof container.object === 'string' && container.object ? container.object : undefined) - ?? container?.list?.data?.object - ?? container?.form?.data?.object - ?? (typeof container.name === 'string' ? container.name : undefined); + // [#25467 cut 33] The body moved to `protocol-registry-view-predicates.ts`; it reads no member. + return runtimeViewContainerObject(container); } /** @@ -18244,8 +18162,12 @@ export class ObjectStackProtocolImplementation implements container: Record, ownPackageId: string | undefined, ): boolean { - const shipped = this.shippedViewContainerOf(type, container.name, ownPackageId); - return shipped !== undefined && this.runtimeViewContainerObject(shipped) === object; + // [#25467 cut 33] The body moved to `protocol-registry-view-predicates.ts` (see `RegistryViewPredicatesHost`). + const host: ThisParameterType = { + runtimeViewContainerObject: (...a) => this.runtimeViewContainerObject(...a), + shippedViewContainerOf: (...a) => this.shippedViewContainerOf(...a), + }; + return copiesOwnShippedViewContainer.call(host, type, object, container, ownPackageId); } /** @@ -18293,12 +18215,12 @@ export class ObjectStackProtocolImplementation implements * answer. */ private isAnotherPackagesObject(object: string, ownPackageId: string | undefined): boolean { - const registry: any = (this.engine as any)?.registry; - const owner = typeof registry?.getPackagedObjectOwner === 'function' - ? registry.getPackagedObjectOwner(object) - : undefined; - const ownerPackageId: unknown = owner?.packageId; - return typeof ownerPackageId === 'string' && ownerPackageId !== '' && ownerPackageId !== ownPackageId; + // [#25467 cut 33] The body moved to `protocol-registry-view-predicates.ts` (see `RegistryViewPredicatesHost`). + const readEngine = () => this.engine; + const host: ThisParameterType = { + get engine() { return readEngine(); }, + }; + return isAnotherPackagesObject.call(host, object, ownPackageId); } /** @@ -18645,7 +18567,14 @@ export class ObjectStackProtocolImplementation implements * set's wider grant included. */ private registryTakesStoredRows(): boolean { - return this.environmentId === undefined || this.storeHydratedIntoRegistry; + // [#25467 cut 33] The body moved to `protocol-registry-view-predicates.ts` (see `RegistryViewPredicatesHost`). + const readEnvironmentId = () => this.environmentId; + const readStoreHydratedIntoRegistry = () => this.storeHydratedIntoRegistry; + const host: ThisParameterType = { + get environmentId() { return readEnvironmentId(); }, + get storeHydratedIntoRegistry() { return readStoreHydratedIntoRegistry(); }, + }; + return registryTakesStoredRows.call(host); } /** @@ -19278,17 +19207,11 @@ export class ObjectStackProtocolImplementation implements */ member?: string, ): Promise { - // [#21911] The explicit system opt-in — see findServedOverlayRow. - const row = await this.findEnvironmentRow('sys_metadata', { - where: { - type, - name, - state: 'active', - ...overlayMemberWhere(type, member), - }, - context: { isSystem: true }, - }); - return (row as { package_id?: string | null } | null)?.package_id ?? null; + // [#25467 cut 33] The body moved to `protocol-stored-row-bindings.ts` (see `StoredRowBindingsHost`). + const host: ThisParameterType = { + findEnvironmentRow: (...a) => this.findEnvironmentRow(...a), + }; + return resolveOverlayPackageBinding.call(host, type, name, member); } /** @@ -19323,12 +19246,8 @@ export class ObjectStackProtocolImplementation implements * `sys_`-prefixed platform table. */ private shouldDropStorage(type: string, name: string, dropStorage: boolean | undefined, state: 'active' | 'draft'): boolean { - if (!dropStorage) return false; - const singular = PLURAL_TO_SINGULAR[type] ?? type; - if (singular !== 'object') return false; - if (state !== 'active') return false; - if (name.startsWith('sys_')) return false; - return true; + // [#25467 cut 33] The body moved to `protocol-package-scope.ts`; it reads no member. + return shouldDropStorage(type, name, dropStorage, state); } /** @@ -19493,23 +19412,11 @@ export class ObjectStackProtocolImplementation implements * existence claim (see {@link metaTypeNamespaceExists}). */ private async refuseUnmintableMetaType(request: { type: string, name: string }): Promise { - const unrecognised = unrecognisedMetaTypeRefusal(request.type); - if (!unrecognised) return; - // The old exemption 1 (skip when the name contains a slash) was - // removed by commit 311433f6b — the grammar verdict upstream refuses every - // slash-bearing name before this probe runs. See the header. - // Exemption 2 — the namespace predates this write. - if (await this.metaTypeNamespaceExists(unrecognised.type)) return; - const err = new Error( - `'${unrecognised.type}' is not a metadata type. The platform declares ` - + `no such type, and a plugin cannot declare one either: 'additionalTypes' was retired because ` - + `nothing ever read it — so this write would mint a sys_metadata namespace under ` - + `type='${unrecognised.type}' that nothing reads and nothing serves. Address a real ` - + `metadata type; GET /api/v1/meta/types lists the ones this deployment carries.`, - ); - (err as any).code = 'INVALID_REQUEST'; - (err as any).status = 400; - throw err; + // [#25467 cut 33] The body moved to `protocol-unmintable-meta-type.ts` (see `UnmintableMetaTypeHost`). + const host: ThisParameterType = { + metaTypeNamespaceExists: (...a) => this.metaTypeNamespaceExists(...a), + }; + return refuseUnmintableMetaType.call(host, request); } /** @@ -20020,12 +19927,13 @@ export class ObjectStackProtocolImplementation implements container: Record, packageId: string | null | undefined, ): ReadonlySet { - const ownPackageId = this.runtimeViewContainerPackage(type, container, { packageId }); - const shipped = this.shippedViewContainerOf(type, container.name, ownPackageId); - if (shipped === undefined) return new Set(); - return new Set( - this.expandRuntimeViewContainer(type, shipped, { packageId: ownPackageId }).map((view) => String(view.name)), - ); + // [#25467 cut 33] The body moved to `protocol-registry-view-predicates.ts` (see `RegistryViewPredicatesHost`). + const host: ThisParameterType = { + expandRuntimeViewContainer: (...a) => this.expandRuntimeViewContainer(...a), + runtimeViewContainerPackage: (...a) => this.runtimeViewContainerPackage(...a), + shippedViewContainerOf: (...a) => this.shippedViewContainerOf(...a), + }; + return overlaidShippedContainerViewNames.call(host, type, container, packageId); } /** @@ -23512,15 +23420,14 @@ export class ObjectStackProtocolImplementation implements * carries the whole fact, and the hundredth adds nothing. */ private warnClosureReadUnavailable(type: string, name: string): void { - if (this.closureReadWarned) return; - this.closureReadWarned = true; - console.warn( - `[Protocol] publishPackageDrafts: this overlay repository declares no 'get', so the batch's own ` - + `pending drafts cannot be read (first reached at ${type}/${name}). Author-time validation falls ` - + `back to the LIVE declarations only, without this batch's own drafts in the closure, so a draft that references a ` - + `sibling drafted in the SAME batch may be refused as unresolved. Nothing is published unchecked: ` - + `the gate still runs, with strictly less resolution context.`, - ); + // [#25467 cut 33] The body moved to `protocol-loud-degradations.ts` (see `LoudDegradationsHost`). + const readClosureReadWarned = () => this.closureReadWarned; + const writeClosureReadWarned = (value: ObjectStackProtocolImplementation['closureReadWarned']) => { this.closureReadWarned = value; }; + const host: ThisParameterType = { + get closureReadWarned() { return readClosureReadWarned(); }, + set closureReadWarned(value) { writeClosureReadWarned(value); }, + }; + return warnClosureReadUnavailable.call(host, type, name); } /** One warn per process for the {@link warnClosureReadUnavailable} degrade. */ @@ -26367,8 +26274,12 @@ export class ObjectStackProtocolImplementation implements * again — which is precisely how the seam this repairs was born. */ private async persistPackageCommitRow(row: Record): Promise { - // [#21911] The explicit system opt-in — see recordMetadataAudit. - await this.engine.insert('sys_metadata_commit', row, { context: { isSystem: true } }); + // [#25467 cut 33] The body moved to `protocol-package-commit-record.ts` (see `PackageCommitRecordHost`). + const readEngine = () => this.engine; + const host: ThisParameterType = { + get engine() { return readEngine(); }, + }; + return persistPackageCommitRow.call(host, row); } /** @@ -26405,77 +26316,15 @@ export class ObjectStackProtocolImplementation implements eventSeqStart?: number; eventSeqEnd?: number; }): Promise<{ commitId: string } | null> { - try { - const commitId = 'cmt_' + (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' - ? crypto.randomUUID() - : `${args.eventSeqEnd ?? 0}-${args.items.length}-${args.packageId}`); - await this.persistPackageCommitRow({ - id: commitId, - package_id: args.packageId, - operation: args.operation, - ...(args.message ? { message: args.message } : {}), - ...(args.actor ? { actor: args.actor } : {}), - ...(args.aiModel ? { ai_model: args.aiModel } : {}), - ...(args.parentCommitId ? { parent_commit_id: args.parentCommitId } : {}), - ...(args.eventSeqStart !== undefined ? { event_seq_start: args.eventSeqStart } : {}), - ...(args.eventSeqEnd !== undefined ? { event_seq_end: args.eventSeqEnd } : {}), - items: JSON.stringify(args.items), - item_count: args.items.length, - created_at: new Date().toISOString(), - }); - return { commitId }; - } catch (error) { - // [#9066] The publish keeps its outcome — `null` still comes back - // and the caller still reports success — but the silence is over. - // - // BENIGN: `sys_metadata_commit` was never provisioned. That is a - // real deployment state (a first boot, or an env kernel composed - // without the commit log — the exact case - // `@objectstack/metadata`'s plugin note records), and it is a - // CONFIGURATION fact, not a store that broke: it is the same on - // every publish and it is fixed in one place. Stated once per - // instance, at `info`, per this card's ruling that an unprovisioned - // commit store stays silent or informational. - // - // EVERYTHING ELSE — a connection drop, a timeout, a permission - // denial, schema drift on that one table — is a write that was - // supposed to land and did not, while the publish it describes DID. - // `error`, per AGENTS.md "Degradation log levels", and the line - // owes the two things that rule requires: the CONSEQUENCE (this - // turn is not revertible, and the system will keep looking healthy) - // and the FIX (repair write access to the commit store). - // - // NOT rethrown, and not surfaced to the caller: the artifacts are - // already live and unwinding them over a missing history row would - // be strictly worse than losing the row. Telling the CALLER that - // the turn is unrevertible is a separate question (a response-field - // change the #8896 ruling forbids for this family) and deliberately - // NOT decided here. - if (isMissingTableError(error, 'sys_metadata_commit')) { - if (!this.commitStoreUnprovisionedNoted) { - this.commitStoreUnprovisionedNoted = true; - console.info( - '[Protocol] sys_metadata_commit is not provisioned — publishes and reverts ' - + 'succeed but record no ADR-0067 commit row, so no turn can be reverted ' - + '(the revert plan of every turn is dropped). Fix: provision the commit ' - + 'store — it is registered alongside sys_metadata_history by ' - + "@objectstack/metadata's plugin; run schema sync. Said once per protocol instance.", - ); - } - return null; - } - const reason = (error as { message?: string } | undefined)?.message ?? String(error); - console.error( - `[Protocol] sys_metadata_commit write FAILED for package '${args.packageId}' ` - + `(${args.operation}, ${args.items.length} item(s)): ${reason}. ` - + 'The publish itself SUCCEEDED and reports success, so nothing looks broken — but ' - + "this turn's revert plan was not persisted: the turn is absent from the package " - + 'commit timeline and can never be reverted, and every further publish loses its ' - + 'own the same way until this is repaired. Fix: restore write access to ' - + 'sys_metadata_commit (connectivity, permissions, or schema drift on that table).', - ); - return null; - } + // [#25467 cut 33] The body moved to `protocol-package-commit-record.ts` (see `PackageCommitRecordHost`). + const readCommitStoreUnprovisionedNoted = () => this.commitStoreUnprovisionedNoted; + const writeCommitStoreUnprovisionedNoted = (value: ObjectStackProtocolImplementation['commitStoreUnprovisionedNoted']) => { this.commitStoreUnprovisionedNoted = value; }; + const host: ThisParameterType = { + get commitStoreUnprovisionedNoted() { return readCommitStoreUnprovisionedNoted(); }, + set commitStoreUnprovisionedNoted(value) { writeCommitStoreUnprovisionedNoted(value); }, + persistPackageCommitRow: (...a) => this.persistPackageCommitRow(...a), + }; + return recordPackageCommit.call(host, args); } private parseCommitItems( @@ -28935,19 +28784,8 @@ export class ObjectStackProtocolImplementation implements * that registered no manifest. */ private reportPageRequiresAbsentAtLoad(name: string, body: unknown, sduiManifest: unknown): void { - const absent = findPageRequiresAbsentFromManifest('page', body, sduiManifest); - if (!absent || absent.length === 0) return; - const one = absent.length === 1; - const named = absent.map((ns) => `'${ns}'`).join(', '); - console.warn( - `[Protocol] [page_requires_plugin_absent] stored page/${name} requires ${named}, ` - + `${one ? 'a namespace' : 'namespaces'} no component in this deployment's SDUI component manifest ` - + `carries: the plugin${one ? ' that provides it is' : 's that provide them are'} not loaded in the ` - + `console this deployment serves. The page is loaded and served anyway, and what its source draws ` - + `from ${one ? 'that plugin' : 'those plugins'} will not render. Install ${one ? 'it' : 'them'} in ` - + `that console, or take ${one ? 'its' : 'their'} components out of the page's source and save the ` - + `page without \`requires\` — it is derived from the source at save.`, - ); + // [#25467 cut 33] The body moved to `protocol-sdui-manifest.ts`; it reads no member. + return reportPageRequiresAbsentAtLoad(name, body, sduiManifest); }