From ed0ca2d57f3a3c4da98a6475cabbb8b9d88c8974 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 05:55:55 +0000 Subject: [PATCH 1/2] refactor(metadata-protocol): move the package door's save refusal and the origin-gated removal verdict out of protocol.ts (#25467 cut 13) Move-only. The bodies of refusePackagedBaseOverride and originGatedRemovalRefusal move into the new protocol-packaged-base-refusal.ts (host shape B: protocol-bound closures for isSealedManagedItem, isWritablePackage and packagedBaseRefusal). Both members keep their name, docblock and signature in the class as stubs that delegate. refusePackagedBaseRemoval stays in the class: it reads the private static mergesOverlayAtRead. isOriginGatedType leaves protocol.ts' import with its last reader. Nothing re-exports the new module, so the published surface does not move. Claude-Session: https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude --- .../src/protocol-packaged-base-refusal.ts | 140 ++++++++++++++++++ packages/metadata-protocol/src/protocol.ts | 89 ++--------- 2 files changed, 154 insertions(+), 75 deletions(-) create mode 100644 packages/metadata-protocol/src/protocol-packaged-base-refusal.ts diff --git a/packages/metadata-protocol/src/protocol-packaged-base-refusal.ts b/packages/metadata-protocol/src/protocol-packaged-base-refusal.ts new file mode 100644 index 00000000000..4d649c5c862 --- /dev/null +++ b/packages/metadata-protocol/src/protocol-packaged-base-refusal.ts @@ -0,0 +1,140 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 13] Two packaged-base door verdicts of + * `ObjectStackProtocolImplementation` — the package door's refusal of a write + * onto an item a code package ships (`refusePackagedBaseOverride`) and the + * removal verdict on a code-defined item of an origin-gated type + * (`originGatedRemovalRefusal`) — moved out of `protocol.ts` whole. + * + * 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: the same predicates, in the + * same order, refusing with the same emitters, codes, statuses and sentences. + * + * ## 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 every method — the published `dist/*.d.ts` lists each one's + * name, docblock and declared signature — and each body is one literal of + * exactly the {@link PackagedBaseRefusalHost} members that method reads, plus + * a `.call`. A method is handed over as a closure that calls it on the + * protocol at call time, so a test double or spy set on the instance is still + * the one the body reaches. Every other reader of these two methods keeps + * calling the class method. + * + * The removal door's own refusal, `refusePackagedBaseRemoval`, stays in the + * class: it reads the private static `mergesOverlayAtRead`, and the class's + * private-static readers stay there. So does `packagedBaseRefusal`, the public + * verdict both doors' refusals answer through, which reads the private static + * `isPackagedBaseRefusal`. + * + * The bodies read no module-level declaration of `protocol.ts`; what they + * import, they import from where `protocol.ts` imported it. Nothing + * re-exports this module, so the published surface does not move. + */ + +import type { ObjectStackProtocolImplementation } from './protocol.js'; +import { isOriginGatedType, managedItemSealedSentence } from './packaged-base-regime.js'; +import { SysMetadataRepository } from './sys-metadata-repository.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 PackagedBaseRefusalHost { + isSealedManagedItem: ObjectStackProtocolImplementation['isSealedManagedItem']; + isWritablePackage: ObjectStackProtocolImplementation['isWritablePackage']; + packagedBaseRefusal: ObjectStackProtocolImplementation['packagedBaseRefusal']; +} + +/** The body of `ObjectStackProtocolImplementation#refusePackagedBaseOverride`; the method's docblock there is its contract. */ +export function refusePackagedBaseOverride( + this: Pick, + request: { type: string; name: string; packageId?: string | null }, +): void { + if (this.isSealedManagedItem(request.type, request.name)) { + // [#8184] THE PACKAGE DOOR — the SECOND refusal point for one + // condition, and the reason this card exists. + // + // `SysMetadataRepository.assertAllowed` reads the base the + // caller NAMED and answers `ITEM_LOCKED` (`lockSource: + // 'package'`) when it is read-only (#7682, then #8146's + // hatch ruling). That door is topology-INDEPENDENT — and it + // was unreachable here, because this branch throws first on + // every kernel with an `environmentId`. So one request + // answered `ITEM_LOCKED` on a host-config / CLI-assembled + // kernel and the undiscriminated `NOT_OVERRIDABLE` on a + // project/cloud per-env one: the refusal VOCABULARY keyed off + // a row-scoping key, which is the #5086 / #6710 finding + // (see the block comment above) arriving on the error codes. + // A client that learns to handle `ITEM_LOCKED` on one + // deployment never saw it on the other. + // + // ⚠️ MIRRORED, NOT RE-INVENTED. Same predicate + // ({@link isWritablePackage}, the ADR-0070 rule in one + // place), same emitter — `readOnlyBaseOverrideError` is + // called, not copied — so the code, the status, the + // `lockSource`, the `packageId` and the sentence cannot drift + // between the two doors. Two independently-authored refusals + // for one condition is how `NOT_OVERRIDABLE`-everywhere + // started. + // + // THE LIMB ORDERING IS THE RULE, and it is the same ordering + // the repository states: BELOW every registry limb, and the + // hatch is not a limb at all. + // • Below the registry limb — this whole branch is guarded + // by {@link isSealedManagedItem}, whose type half is the + // registry's `allowEnvironmentOverlay` alone, so a regime-O type + // never reaches the door. That is ADR-0005: an overlay of + // a code-shipped item ALWAYS names the read-only package + // it customizes, and a door one limb higher would close + // the overlay model outright. Pinned. + // • [ADR-0131 D6] No hatch limb. `OS_METADATA_WRITABLE` used + // to take an artifact-backed write past this branch, down + // to the repository door, where a package-less write landed + // an overlay of the managed item (#8146 had narrowed that to + // "never a NAMED read-only base"). Managed content is sealed + // now — the broad reading that comment reserved for a + // maintainer decision is the decision ADR-0131 D6 records — + // so the hatch is not consulted here, and the repository + // refuses the same write without it. + // + // The named-base limb below keeps its own code: a write that + // NAMES the read-only base answers `ITEM_LOCKED`, and a + // package-less one `NOT_OVERRIDABLE`, with or without the hatch. + // + // `runtime-only` needs no limb here: this branch is guarded by + // `artifactBacked`, so the intent is always + // `override-artifact`. The create side of the door is the + // ADR-0070 D1 gate further down this method, which is already + // topology-independent and already answers + // `WRITABLE_PACKAGE_REQUIRED` / 422 on every kernel. + const namedBase = typeof request.packageId === 'string' && request.packageId.length > 0; + if (namedBase && !this.isWritablePackage(request.packageId)) { + throw SysMetadataRepository.readOnlyBaseOverrideError( + request.type, request.packageId as string, + ); + } + // [#20819, ADR-0131 D6] The SENTENCE is chosen per ADR-0126 regime, + // and names the managed package for every other type; the code and + // the status are unchanged. [#23577] Every write reads it, whatever + // the write meant to do: the door cannot tell a create from an edit. + const err = new Error(managedItemSealedSentence(request.type, request.name, 'save')); + (err as any).code = 'NOT_OVERRIDABLE'; + (err as any).status = 403; + throw err; + } +} + +/** The body of `ObjectStackProtocolImplementation#originGatedRemovalRefusal`; the method's docblock there is its contract. */ +export function originGatedRemovalRefusal( + this: Pick, + type: string, name: string): Error | null { + if (!isOriginGatedType(type)) return null; + return this.packagedBaseRefusal({ type, name, operation: 'delete' }); +} diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 85ab9c4e118..c9d47e0e210 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -59,7 +59,7 @@ import { unboundItemDraftAddress, type SysMetadataEngine, } from './sys-metadata-repository.js'; -import { isOriginGatedType, managedItemSealedSentence, packagedBaseRegimeRow, packagedBaseRegimeSentence } from './packaged-base-regime.js'; +import { managedItemSealedSentence, packagedBaseRegimeRow, packagedBaseRegimeSentence } from './packaged-base-regime.js'; import { resolveArtifactLockLayer, resolveItemLock, @@ -396,6 +396,8 @@ import { import { applyPublishBackfill, planPublishBackfill } from './protocol-publish-backfill.js'; // [#25467 cut 12] The `_drafts` list read and the one-draft discard. import { discardDraftInItsScope, listDrafts } from './protocol-drafts.js'; +// [#25467 cut 13] The package door's save refusal and the origin-gated removal verdict. +import { originGatedRemovalRefusal, refusePackagedBaseOverride } from './protocol-packaged-base-refusal.js'; /** * Canonical Zod schema per metadata type lives in @@ -16370,78 +16372,12 @@ export class ObjectStackProtocolImplementation implements private refusePackagedBaseOverride( request: { type: string; name: string; packageId?: string | null }, ): void { - if (this.isSealedManagedItem(request.type, request.name)) { - // [#8184] THE PACKAGE DOOR — the SECOND refusal point for one - // condition, and the reason this card exists. - // - // `SysMetadataRepository.assertAllowed` reads the base the - // caller NAMED and answers `ITEM_LOCKED` (`lockSource: - // 'package'`) when it is read-only (#7682, then #8146's - // hatch ruling). That door is topology-INDEPENDENT — and it - // was unreachable here, because this branch throws first on - // every kernel with an `environmentId`. So one request - // answered `ITEM_LOCKED` on a host-config / CLI-assembled - // kernel and the undiscriminated `NOT_OVERRIDABLE` on a - // project/cloud per-env one: the refusal VOCABULARY keyed off - // a row-scoping key, which is the #5086 / #6710 finding - // (see the block comment above) arriving on the error codes. - // A client that learns to handle `ITEM_LOCKED` on one - // deployment never saw it on the other. - // - // ⚠️ MIRRORED, NOT RE-INVENTED. Same predicate - // ({@link isWritablePackage}, the ADR-0070 rule in one - // place), same emitter — `readOnlyBaseOverrideError` is - // called, not copied — so the code, the status, the - // `lockSource`, the `packageId` and the sentence cannot drift - // between the two doors. Two independently-authored refusals - // for one condition is how `NOT_OVERRIDABLE`-everywhere - // started. - // - // THE LIMB ORDERING IS THE RULE, and it is the same ordering - // the repository states: BELOW every registry limb, and the - // hatch is not a limb at all. - // • Below the registry limb — this whole branch is guarded - // by {@link isSealedManagedItem}, whose type half is the - // registry's `allowEnvironmentOverlay` alone, so a regime-O type - // never reaches the door. That is ADR-0005: an overlay of - // a code-shipped item ALWAYS names the read-only package - // it customizes, and a door one limb higher would close - // the overlay model outright. Pinned. - // • [ADR-0131 D6] No hatch limb. `OS_METADATA_WRITABLE` used - // to take an artifact-backed write past this branch, down - // to the repository door, where a package-less write landed - // an overlay of the managed item (#8146 had narrowed that to - // "never a NAMED read-only base"). Managed content is sealed - // now — the broad reading that comment reserved for a - // maintainer decision is the decision ADR-0131 D6 records — - // so the hatch is not consulted here, and the repository - // refuses the same write without it. - // - // The named-base limb below keeps its own code: a write that - // NAMES the read-only base answers `ITEM_LOCKED`, and a - // package-less one `NOT_OVERRIDABLE`, with or without the hatch. - // - // `runtime-only` needs no limb here: this branch is guarded by - // `artifactBacked`, so the intent is always - // `override-artifact`. The create side of the door is the - // ADR-0070 D1 gate further down this method, which is already - // topology-independent and already answers - // `WRITABLE_PACKAGE_REQUIRED` / 422 on every kernel. - const namedBase = typeof request.packageId === 'string' && request.packageId.length > 0; - if (namedBase && !this.isWritablePackage(request.packageId)) { - throw SysMetadataRepository.readOnlyBaseOverrideError( - request.type, request.packageId as string, - ); - } - // [#20819, ADR-0131 D6] The SENTENCE is chosen per ADR-0126 regime, - // and names the managed package for every other type; the code and - // the status are unchanged. [#23577] Every write reads it, whatever - // the write meant to do: the door cannot tell a create from an edit. - const err = new Error(managedItemSealedSentence(request.type, request.name, 'save')); - (err as any).code = 'NOT_OVERRIDABLE'; - (err as any).status = 403; - throw err; - } + // [#25467 cut 13] The body moved to `protocol-packaged-base-refusal.ts` (see `PackagedBaseRefusalHost`). + const host: ThisParameterType = { + isSealedManagedItem: (...a) => this.isSealedManagedItem(...a), + isWritablePackage: (...a) => this.isWritablePackage(...a), + }; + return refusePackagedBaseOverride.call(host, request); } /** @@ -16532,8 +16468,11 @@ export class ObjectStackProtocolImplementation implements * found a stored row for the delete to remove. */ private originGatedRemovalRefusal(type: string, name: string): Error | null { - if (!isOriginGatedType(type)) return null; - return this.packagedBaseRefusal({ type, name, operation: 'delete' }); + // [#25467 cut 13] The body moved to `protocol-packaged-base-refusal.ts` (see `PackagedBaseRefusalHost`). + const host: ThisParameterType = { + packagedBaseRefusal: (...a) => this.packagedBaseRefusal(...a), + }; + return originGatedRemovalRefusal.call(host, type, name); } // ─────────────────────────────────────────────────────────────────── From 1c09db4206be14a9f1ab5eddaf628430644f04e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 05:56:07 +0000 Subject: [PATCH 2/2] test(metadata-protocol): the bracketed-refusal pin follows the package door's throw site (#25467 cut 13) refusePackagedBaseOverride's one `new Error(` moved with its body into protocol-packaged-base-refusal.ts, so the scan gains that module's row with its own count as the floor, as cuts 1, 3, 5, 6, 7, 10 and 12 added theirs. Claude-Session: https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude --- .../src/protocol.bracketed-refusal-opener-absence.test.ts | 3 +++ 1 file changed, 3 insertions(+) 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 d76f3c98cf4..bb14aa75562 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 @@ -90,6 +90,9 @@ const PRODUCERS: Readonly> = { // The publish backfill pair, moved out of `protocol.ts` whole on the same // terms. 'protocol-publish-backfill.ts': 3, + // The package door's save refusal, moved out of `protocol.ts` whole on the + // same terms. + 'protocol-packaged-base-refusal.ts': 1, 'sys-metadata-repository.ts': 5, 'runtime-authoring-gate.ts': 1, };