From c5f135197870e98344541e62c862ea22ec979d1d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 21:37:54 +0000 Subject: [PATCH] refactor(metadata-protocol): move the write package scope resolution out of protocol.ts (#25467 cut 19) Move the body of resolveWritePackageScope unedited into protocol-write-package-scope.ts, behind a WritePackageScopeHost that hands over the engine as a getter. The class keeps the method, its docblock and declared signature; its body becomes a host literal plus a .call. The authoring gate's host closure keeps calling the class method. Claude-Session: https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude --- .../src/protocol-write-package-scope.ts | 88 +++++++++++++++++++ packages/metadata-protocol/src/protocol.ts | 46 ++-------- 2 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 packages/metadata-protocol/src/protocol-write-package-scope.ts diff --git a/packages/metadata-protocol/src/protocol-write-package-scope.ts b/packages/metadata-protocol/src/protocol-write-package-scope.ts new file mode 100644 index 00000000000..7cfb6bfa1fe --- /dev/null +++ b/packages/metadata-protocol/src/protocol-write-package-scope.ts @@ -0,0 +1,88 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 19] A package write's validation scope, as + * `ObjectStackProtocolImplementation` resolves it + * (`resolveWritePackageScope`): the written package and the closure of the + * dependencies its manifest declares, read from the registry, or `undefined` + * wherever that cannot be read. Moved out of `protocol.ts` whole. + * + * A MOVE, not a redesign. The function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited: the same guards, the same + * dependency walk, and the same silent `catch` answering `undefined`. + * + * ## How the moved body reaches 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 the method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link WritePackageScopeHost} members it reads, plus a `.call`. The + * engine is handed over as a getter, so the body reads the protocol's engine + * (and so its registry) at call time. Every other reader of the method keeps + * calling the class method: `tenantAuthoredWriteRefusal`, and the authoring + * gate's host (cut 8, `protocol-authoring-gate.ts`), whose closure calls it. + * + * The body reads no module-level declaration of `protocol.ts` and no import, + * so this module imports only the class type. Nothing re-exports this module, + * so the published surface does not move. + */ + +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 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 WritePackageScopeHost { + readonly engine: ObjectStackProtocolImplementation['engine']; +} + +/** The body of `ObjectStackProtocolImplementation#resolveWritePackageScope`; the method's docblock there is its contract. */ +export function resolveWritePackageScope( + this: Pick, + packageId: string | null | undefined, +): { packageId: string; dependencies: string[] } | undefined { + if (typeof packageId !== 'string' || packageId === '' || packageId === 'sys_metadata') return undefined; + try { + const registry = this.engine.registry as + | { getPackage?: (id: string) => { manifest?: unknown } | undefined } + | undefined; + if (typeof registry?.getPackage !== 'function') return undefined; + // The written package itself must be readable: its manifest is the + // ONLY declaration of what it may reference, and narrowing without + // it would be narrowing on nothing. + if (!registry.getPackage(packageId)) return undefined; + + const dependencies = new Set(); + const frontier = [packageId]; + while (frontier.length > 0) { + const current = frontier.pop() as string; + const manifest = registry.getPackage(current)?.manifest as + | { dependencies?: Record } + | undefined; + const declared = manifest?.dependencies; + if (!declared || typeof declared !== 'object') continue; + for (const dep of Object.keys(declared)) { + // A dependency the registry cannot resolve is still IN the + // closure: the package declared it, so an object stamped + // with it is reachable by declaration. Only the transitive + // walk stops there — `getPackage` returns undefined and the + // loop above simply contributes nothing further. + if (dep === packageId || dependencies.has(dep)) continue; + dependencies.add(dep); + frontier.push(dep); + } + } + return { packageId, dependencies: [...dependencies] }; + } catch { + // Context gathering must never fail a write (the same contract + // `listCollection` above holds). An unreadable registry means an + // unnarrowed gate, not a refused publish. + return undefined; + } +} diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 9e145503f72..8b656673718 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -402,6 +402,8 @@ import { namesWithOwnStoredRow, readActiveOverlayRows, readItemActiveRows, store import { classifyObjectContribution, releaseAdoptedObjectOwner } from './protocol-object-owner.js'; // [#25467 cut 18] The flow save's retired-spelling replay verdict (#23903 ruling B′): does a save newly introduce a retired spelling no stored row carries. import { flowSaveReplaysRetired } from './protocol-flow-save-replay.js'; +// [#25467 cut 19] A package write's validation scope: the written package and its declared dependency closure, from the registry. +import { resolveWritePackageScope } from './protocol-write-package-scope.js'; /** * Canonical Zod schema per metadata type lives in @@ -7361,44 +7363,12 @@ export class ObjectStackProtocolImplementation implements private resolveWritePackageScope( packageId: string | null | undefined, ): { packageId: string; dependencies: string[] } | undefined { - if (typeof packageId !== 'string' || packageId === '' || packageId === 'sys_metadata') return undefined; - try { - const registry = this.engine.registry as - | { getPackage?: (id: string) => { manifest?: unknown } | undefined } - | undefined; - if (typeof registry?.getPackage !== 'function') return undefined; - // The written package itself must be readable: its manifest is the - // ONLY declaration of what it may reference, and narrowing without - // it would be narrowing on nothing. - if (!registry.getPackage(packageId)) return undefined; - - const dependencies = new Set(); - const frontier = [packageId]; - while (frontier.length > 0) { - const current = frontier.pop() as string; - const manifest = registry.getPackage(current)?.manifest as - | { dependencies?: Record } - | undefined; - const declared = manifest?.dependencies; - if (!declared || typeof declared !== 'object') continue; - for (const dep of Object.keys(declared)) { - // A dependency the registry cannot resolve is still IN the - // closure: the package declared it, so an object stamped - // with it is reachable by declaration. Only the transitive - // walk stops there — `getPackage` returns undefined and the - // loop above simply contributes nothing further. - if (dep === packageId || dependencies.has(dep)) continue; - dependencies.add(dep); - frontier.push(dep); - } - } - return { packageId, dependencies: [...dependencies] }; - } catch { - // Context gathering must never fail a write (the same contract - // `listCollection` above holds). An unreadable registry means an - // unnarrowed gate, not a refused publish. - return undefined; - } + // [#25467 cut 19] The body moved to `protocol-write-package-scope.ts` (see `WritePackageScopeHost`). + const readEngine = () => this.engine; + const host: ThisParameterType = { + get engine() { return readEngine(); }, + }; + return resolveWritePackageScope.call(host, packageId); } /**