Skip to content

Commit ed0ca2d

Browse files
committed
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 <noreply@anthropic.com>
1 parent 7a04ba2 commit ed0ca2d

2 files changed

Lines changed: 154 additions & 75 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 13] Two packaged-base door verdicts of
5+
* `ObjectStackProtocolImplementation` — the package door's refusal of a write
6+
* onto an item a code package ships (`refusePackagedBaseOverride`) and the
7+
* removal verdict on a code-defined item of an origin-gated type
8+
* (`originGatedRemovalRefusal`) — moved out of `protocol.ts` whole.
9+
*
10+
* A MOVE, not a redesign. Each function below is the body of the
11+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
12+
* re-indented one level and otherwise unedited: the same predicates, in the
13+
* same order, refusing with the same emitters, codes, statuses and sentences.
14+
*
15+
* ## How the moved bodies reach the class
16+
*
17+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
18+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
19+
* The class keeps every method — the published `dist/*.d.ts` lists each one's
20+
* name, docblock and declared signature — and each body is one literal of
21+
* exactly the {@link PackagedBaseRefusalHost} members that method reads, plus
22+
* a `.call`. A method is handed over as a closure that calls it on the
23+
* protocol at call time, so a test double or spy set on the instance is still
24+
* the one the body reaches. Every other reader of these two methods keeps
25+
* calling the class method.
26+
*
27+
* The removal door's own refusal, `refusePackagedBaseRemoval`, stays in the
28+
* class: it reads the private static `mergesOverlayAtRead`, and the class's
29+
* private-static readers stay there. So does `packagedBaseRefusal`, the public
30+
* verdict both doors' refusals answer through, which reads the private static
31+
* `isPackagedBaseRefusal`.
32+
*
33+
* The bodies read no module-level declaration of `protocol.ts`; what they
34+
* import, they import from where `protocol.ts` imported it. Nothing
35+
* re-exports this module, so the published surface does not move.
36+
*/
37+
38+
import type { ObjectStackProtocolImplementation } from './protocol.js';
39+
import { isOriginGatedType, managedItemSealedSentence } from './packaged-base-regime.js';
40+
import { SysMetadataRepository } from './sys-metadata-repository.js';
41+
42+
/**
43+
* The `ObjectStackProtocolImplementation` members the functions below read —
44+
* every one of them, and no other; each function's `this` is the `Pick` of the
45+
* members IT reads. Each is typed AS the class member (an indexed access on
46+
* the class), so a change to a member's signature on the class is a change
47+
* here too, and naming a member the class does not have is a compile error.
48+
*/
49+
export interface PackagedBaseRefusalHost {
50+
isSealedManagedItem: ObjectStackProtocolImplementation['isSealedManagedItem'];
51+
isWritablePackage: ObjectStackProtocolImplementation['isWritablePackage'];
52+
packagedBaseRefusal: ObjectStackProtocolImplementation['packagedBaseRefusal'];
53+
}
54+
55+
/** The body of `ObjectStackProtocolImplementation#refusePackagedBaseOverride`; the method's docblock there is its contract. */
56+
export function refusePackagedBaseOverride(
57+
this: Pick<PackagedBaseRefusalHost, 'isSealedManagedItem' | 'isWritablePackage'>,
58+
request: { type: string; name: string; packageId?: string | null },
59+
): void {
60+
if (this.isSealedManagedItem(request.type, request.name)) {
61+
// [#8184] THE PACKAGE DOOR — the SECOND refusal point for one
62+
// condition, and the reason this card exists.
63+
//
64+
// `SysMetadataRepository.assertAllowed` reads the base the
65+
// caller NAMED and answers `ITEM_LOCKED` (`lockSource:
66+
// 'package'`) when it is read-only (#7682, then #8146's
67+
// hatch ruling). That door is topology-INDEPENDENT — and it
68+
// was unreachable here, because this branch throws first on
69+
// every kernel with an `environmentId`. So one request
70+
// answered `ITEM_LOCKED` on a host-config / CLI-assembled
71+
// kernel and the undiscriminated `NOT_OVERRIDABLE` on a
72+
// project/cloud per-env one: the refusal VOCABULARY keyed off
73+
// a row-scoping key, which is the #5086 / #6710 finding
74+
// (see the block comment above) arriving on the error codes.
75+
// A client that learns to handle `ITEM_LOCKED` on one
76+
// deployment never saw it on the other.
77+
//
78+
// ⚠️ MIRRORED, NOT RE-INVENTED. Same predicate
79+
// ({@link isWritablePackage}, the ADR-0070 rule in one
80+
// place), same emitter — `readOnlyBaseOverrideError` is
81+
// called, not copied — so the code, the status, the
82+
// `lockSource`, the `packageId` and the sentence cannot drift
83+
// between the two doors. Two independently-authored refusals
84+
// for one condition is how `NOT_OVERRIDABLE`-everywhere
85+
// started.
86+
//
87+
// THE LIMB ORDERING IS THE RULE, and it is the same ordering
88+
// the repository states: BELOW every registry limb, and the
89+
// hatch is not a limb at all.
90+
// • Below the registry limb — this whole branch is guarded
91+
// by {@link isSealedManagedItem}, whose type half is the
92+
// registry's `allowEnvironmentOverlay` alone, so a regime-O type
93+
// never reaches the door. That is ADR-0005: an overlay of
94+
// a code-shipped item ALWAYS names the read-only package
95+
// it customizes, and a door one limb higher would close
96+
// the overlay model outright. Pinned.
97+
// • [ADR-0131 D6] No hatch limb. `OS_METADATA_WRITABLE` used
98+
// to take an artifact-backed write past this branch, down
99+
// to the repository door, where a package-less write landed
100+
// an overlay of the managed item (#8146 had narrowed that to
101+
// "never a NAMED read-only base"). Managed content is sealed
102+
// now — the broad reading that comment reserved for a
103+
// maintainer decision is the decision ADR-0131 D6 records —
104+
// so the hatch is not consulted here, and the repository
105+
// refuses the same write without it.
106+
//
107+
// The named-base limb below keeps its own code: a write that
108+
// NAMES the read-only base answers `ITEM_LOCKED`, and a
109+
// package-less one `NOT_OVERRIDABLE`, with or without the hatch.
110+
//
111+
// `runtime-only` needs no limb here: this branch is guarded by
112+
// `artifactBacked`, so the intent is always
113+
// `override-artifact`. The create side of the door is the
114+
// ADR-0070 D1 gate further down this method, which is already
115+
// topology-independent and already answers
116+
// `WRITABLE_PACKAGE_REQUIRED` / 422 on every kernel.
117+
const namedBase = typeof request.packageId === 'string' && request.packageId.length > 0;
118+
if (namedBase && !this.isWritablePackage(request.packageId)) {
119+
throw SysMetadataRepository.readOnlyBaseOverrideError(
120+
request.type, request.packageId as string,
121+
);
122+
}
123+
// [#20819, ADR-0131 D6] The SENTENCE is chosen per ADR-0126 regime,
124+
// and names the managed package for every other type; the code and
125+
// the status are unchanged. [#23577] Every write reads it, whatever
126+
// the write meant to do: the door cannot tell a create from an edit.
127+
const err = new Error(managedItemSealedSentence(request.type, request.name, 'save'));
128+
(err as any).code = 'NOT_OVERRIDABLE';
129+
(err as any).status = 403;
130+
throw err;
131+
}
132+
}
133+
134+
/** The body of `ObjectStackProtocolImplementation#originGatedRemovalRefusal`; the method's docblock there is its contract. */
135+
export function originGatedRemovalRefusal(
136+
this: Pick<PackagedBaseRefusalHost, 'packagedBaseRefusal'>,
137+
type: string, name: string): Error | null {
138+
if (!isOriginGatedType(type)) return null;
139+
return this.packagedBaseRefusal({ type, name, operation: 'delete' });
140+
}

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 14 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import {
5959
unboundItemDraftAddress,
6060
type SysMetadataEngine,
6161
} from './sys-metadata-repository.js';
62-
import { isOriginGatedType, managedItemSealedSentence, packagedBaseRegimeRow, packagedBaseRegimeSentence } from './packaged-base-regime.js';
62+
import { managedItemSealedSentence, packagedBaseRegimeRow, packagedBaseRegimeSentence } from './packaged-base-regime.js';
6363
import {
6464
resolveArtifactLockLayer,
6565
resolveItemLock,
@@ -396,6 +396,8 @@ import {
396396
import { applyPublishBackfill, planPublishBackfill } from './protocol-publish-backfill.js';
397397
// [#25467 cut 12] The `_drafts` list read and the one-draft discard.
398398
import { discardDraftInItsScope, listDrafts } from './protocol-drafts.js';
399+
// [#25467 cut 13] The package door's save refusal and the origin-gated removal verdict.
400+
import { originGatedRemovalRefusal, refusePackagedBaseOverride } from './protocol-packaged-base-refusal.js';
399401

400402
/**
401403
* Canonical Zod schema per metadata type lives in
@@ -16370,78 +16372,12 @@ export class ObjectStackProtocolImplementation implements
1637016372
private refusePackagedBaseOverride(
1637116373
request: { type: string; name: string; packageId?: string | null },
1637216374
): void {
16373-
if (this.isSealedManagedItem(request.type, request.name)) {
16374-
// [#8184] THE PACKAGE DOOR — the SECOND refusal point for one
16375-
// condition, and the reason this card exists.
16376-
//
16377-
// `SysMetadataRepository.assertAllowed` reads the base the
16378-
// caller NAMED and answers `ITEM_LOCKED` (`lockSource:
16379-
// 'package'`) when it is read-only (#7682, then #8146's
16380-
// hatch ruling). That door is topology-INDEPENDENT — and it
16381-
// was unreachable here, because this branch throws first on
16382-
// every kernel with an `environmentId`. So one request
16383-
// answered `ITEM_LOCKED` on a host-config / CLI-assembled
16384-
// kernel and the undiscriminated `NOT_OVERRIDABLE` on a
16385-
// project/cloud per-env one: the refusal VOCABULARY keyed off
16386-
// a row-scoping key, which is the #5086 / #6710 finding
16387-
// (see the block comment above) arriving on the error codes.
16388-
// A client that learns to handle `ITEM_LOCKED` on one
16389-
// deployment never saw it on the other.
16390-
//
16391-
// ⚠️ MIRRORED, NOT RE-INVENTED. Same predicate
16392-
// ({@link isWritablePackage}, the ADR-0070 rule in one
16393-
// place), same emitter — `readOnlyBaseOverrideError` is
16394-
// called, not copied — so the code, the status, the
16395-
// `lockSource`, the `packageId` and the sentence cannot drift
16396-
// between the two doors. Two independently-authored refusals
16397-
// for one condition is how `NOT_OVERRIDABLE`-everywhere
16398-
// started.
16399-
//
16400-
// THE LIMB ORDERING IS THE RULE, and it is the same ordering
16401-
// the repository states: BELOW every registry limb, and the
16402-
// hatch is not a limb at all.
16403-
// • Below the registry limb — this whole branch is guarded
16404-
// by {@link isSealedManagedItem}, whose type half is the
16405-
// registry's `allowEnvironmentOverlay` alone, so a regime-O type
16406-
// never reaches the door. That is ADR-0005: an overlay of
16407-
// a code-shipped item ALWAYS names the read-only package
16408-
// it customizes, and a door one limb higher would close
16409-
// the overlay model outright. Pinned.
16410-
// • [ADR-0131 D6] No hatch limb. `OS_METADATA_WRITABLE` used
16411-
// to take an artifact-backed write past this branch, down
16412-
// to the repository door, where a package-less write landed
16413-
// an overlay of the managed item (#8146 had narrowed that to
16414-
// "never a NAMED read-only base"). Managed content is sealed
16415-
// now — the broad reading that comment reserved for a
16416-
// maintainer decision is the decision ADR-0131 D6 records —
16417-
// so the hatch is not consulted here, and the repository
16418-
// refuses the same write without it.
16419-
//
16420-
// The named-base limb below keeps its own code: a write that
16421-
// NAMES the read-only base answers `ITEM_LOCKED`, and a
16422-
// package-less one `NOT_OVERRIDABLE`, with or without the hatch.
16423-
//
16424-
// `runtime-only` needs no limb here: this branch is guarded by
16425-
// `artifactBacked`, so the intent is always
16426-
// `override-artifact`. The create side of the door is the
16427-
// ADR-0070 D1 gate further down this method, which is already
16428-
// topology-independent and already answers
16429-
// `WRITABLE_PACKAGE_REQUIRED` / 422 on every kernel.
16430-
const namedBase = typeof request.packageId === 'string' && request.packageId.length > 0;
16431-
if (namedBase && !this.isWritablePackage(request.packageId)) {
16432-
throw SysMetadataRepository.readOnlyBaseOverrideError(
16433-
request.type, request.packageId as string,
16434-
);
16435-
}
16436-
// [#20819, ADR-0131 D6] The SENTENCE is chosen per ADR-0126 regime,
16437-
// and names the managed package for every other type; the code and
16438-
// the status are unchanged. [#23577] Every write reads it, whatever
16439-
// the write meant to do: the door cannot tell a create from an edit.
16440-
const err = new Error(managedItemSealedSentence(request.type, request.name, 'save'));
16441-
(err as any).code = 'NOT_OVERRIDABLE';
16442-
(err as any).status = 403;
16443-
throw err;
16444-
}
16375+
// [#25467 cut 13] The body moved to `protocol-packaged-base-refusal.ts` (see `PackagedBaseRefusalHost`).
16376+
const host: ThisParameterType<typeof refusePackagedBaseOverride> = {
16377+
isSealedManagedItem: (...a) => this.isSealedManagedItem(...a),
16378+
isWritablePackage: (...a) => this.isWritablePackage(...a),
16379+
};
16380+
return refusePackagedBaseOverride.call(host, request);
1644516381
}
1644616382

1644716383
/**
@@ -16532,8 +16468,11 @@ export class ObjectStackProtocolImplementation implements
1653216468
* found a stored row for the delete to remove.
1653316469
*/
1653416470
private originGatedRemovalRefusal(type: string, name: string): Error | null {
16535-
if (!isOriginGatedType(type)) return null;
16536-
return this.packagedBaseRefusal({ type, name, operation: 'delete' });
16471+
// [#25467 cut 13] The body moved to `protocol-packaged-base-refusal.ts` (see `PackagedBaseRefusalHost`).
16472+
const host: ThisParameterType<typeof originGatedRemovalRefusal> = {
16473+
packagedBaseRefusal: (...a) => this.packagedBaseRefusal(...a),
16474+
};
16475+
return originGatedRemovalRefusal.call(host, type, name);
1653716476
}
1653816477

1653916478
// ───────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)