Skip to content

Commit 0479cd6

Browse files
refactor(metadata-protocol): move the package door save refusal and the origin-gated removal verdict out of protocol.ts into protocol-packaged-base-refusal.ts (#25467 cut 13) (#25972)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X7JKgiM29GUcitRvoGGj5BU)_ Part of #25467 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Cut 13 of the `protocol.ts` split (#24586 ruling A, recorded in `6270097840`). Cuts 1–12 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642, #25719, #25731, #25740, #25797 and #25939. The claim is `6409240079`; the scope is the cut 13 proposal of the cut 12 report (`6408535757`), as the server seat approved it in its dispatch order, **narrowed by one member** (below). Two `ObjectStackProtocolImplementation` members move unedited into a new module, `packages/metadata-protocol/src/protocol-packaged-base-refusal.ts` (#23050 shape B): the package door's save refusal `refusePackagedBaseOverride` and the origin-gated removal verdict `originGatedRemovalRefusal`. `protocol.ts` loses 61 net lines (29,681 to 29,620). The cut is move-only, and the card stays open for later cuts. ## The scope narrowed: `refusePackagedBaseRemoval` stays in the class The proposal (my cut 12 report) named three members and said none reads a private-static member. That reading was wrong for one of them: it grepped the one-line spelling `ObjectStackProtocolImplementation.NAME`, and `refusePackagedBaseRemoval` breaks that spelling across two lines (`ObjectStackProtocolImplementation` then `.mergesOverlayAtRead(request.type)`). `mergesOverlayAtRead` is `private static`, and the Release's stay list keeps the private-static readers in the class, which this order's rulings hold. So `refusePackagedBaseRemoval` stays, unedited, and the re-measure below walks the AST (a property access on the class identifier), not the text. The two members that move read no static member at all. **Does `packagedBaseRefusal` ride along?** No. It is not a small private helper: it is a public member (the `/automation` domain of `@objectstack/runtime` calls it), it reads the private static `isPackagedBaseRefusal`, and it calls `refusePackagedBaseRemoval`, which stays. It stays in the class and is handed to the moved `originGatedRemovalRefusal` as a closure. Branch `claude/project-thread-ybjm3h` is the harness-pinned branch for this thread, restarted from `main` after cut 12 merged. It names no issue, so the claim comment is the identity record. It holds the move commit `ed0ca2d57` and the pin commit `1c09db420`, both on `main` `7a04ba288d` (cut 12's merge `efe35983f8` and #25935 are in its history), and the merge of `main` `59d9181ed` (`b1ac1d8a9`). That merge brought six commits, none of which touches `packages/metadata-protocol/` (the package's `src/` tree is `761912e9` at both ends of the range). **Serial with the file's in-flight PRs.** Measured just before this PR was opened: of the 14 open PRs, one touches `packages/metadata-protocol/src/`: #25941 edits `metadata-diagnostics.ts` and adds its memo test, disjoint from `protocol.ts`. #25826 is no longer open. The release PR #25640's file list could not be paged through this container's proxy; at cut 11 it touched only the package's `CHANGELOG.md` and `package.json`. No open PR edits `protocol.ts`. ## What moves | Member | Base line | Visibility | AST tokens (params + return + body) | |---|---|---|---| | `refusePackagedBaseOverride` | `:16370` | private | 136 | | `originGatedRemovalRefusal` | `:16534` | private | 43 | (`refusePackagedBaseOverride`'s 72 body lines are mostly its record comment; comments are not AST tokens, and the comparison below checks them as text.) ## The shape (#23050 shape B, as cuts 1–12 apply it) - **The class keeps every member** with its name, docblock, signature and position. Each body is now one host literal plus a `.call`, marked `[#25467 cut 13]`. - **Typing.** `PackagedBaseRefusalHost` types every member as the class member itself (an indexed access on `ObjectStackProtocolImplementation`). Each moved function types its `this` as the `Pick` of the host over exactly the members its body reads. Each stub annotates its literal as `ThisParameterType` of `typeof fn`. No cast, no generic member (so the plain `.call` spelling), and no member changes visibility. - **Members read, measured.** `refusePackagedBaseOverride` reads `isSealedManagedItem` and `isWritablePackage`; `originGatedRemovalRefusal` reads `packagedBaseRefusal`. Each goes as a closure on the protocol. No field is read, so there is no getter. - **Every other caller still calls the class method**: `saveMetaItem` and `packagedBaseRefusal` call `refusePackagedBaseOverride`; `deleteMetaItem` calls `originGatedRemovalRefusal`. ## Module-level declarations, by reader - **None gains `export`, none moves.** The bodies read no module-level declaration of `protocol.ts`. They read `SysMetadataRepository` (its static `readOnlyBaseOverrideError`), `managedItemSealedSentence` and `isOriginGatedType`, which the new module imports from where `protocol.ts` imports them (`./sys-metadata-repository.js`, `./packaged-base-regime.js`). The new module's only import from `protocol.js` is a type import, so it adds no runtime cycle. - **Imports in `protocol.ts`.** One name loses its last reader and is dropped: `isOriginGatedType` from `./packaged-base-regime.js` (at base: the import, the body of `originGatedRemovalRefusal`, and a `{@link}` in its docblock, which stays as prose). The one new statement is the import of the new module. ## The published `.d.ts` The series bar is ruling `6348891160`, sorted-union identity, measured first. Base `7a04ba288d` and head were built with `pnpm --filter @objectstack/metadata-protocol build` against one dependency tree (the closure built at head by a full `pnpm build`, 119/119, then the package's `src/` swapped tree-only to base for the base build and restored from `HEAD` under a trap; `git diff HEAD` empty, `git status` clean and all three touched blobs equal to `HEAD`'s after). Measured twice: on the pre-merge tree (base `7a04ba288d`, head `1c09db420`) and after the merge (merged base, head `b1ac1d8a9`, closure rebuilt by a full `pnpm build`, 119/119); the readings below hold for both. - **Raw sha256**: `08914e65f936a2b031c3f3f215481a97f53d132eefd869e4f42d928947ebe173` at base and at head, on both `dist/index.d.ts` and `dist/index.d.cts`, 775,874 bytes each. **Raw identity holds**, so sorted-union identity holds too. Both members stay `private NAME;` lines. - **Normalised sha256**: `46ccd7e5c4db673652513d1801ec9c9f06bf34a0a7fb6ba97fb2531ef0cc544b` at both ends (80 string-literal union runs sorted; this series' cut 12 normaliser). - **The two builds differ where they should**: `index.js` hashes `53d93de18be521db…` at base and `4d3c4c7364445db0…` at head; each of the two functions' `.call(host` sites occurs 0 times in the base bundles and once in the head bundles (ESM and CJS alike). - **Positive control** on the head tree (on disk, restored from `HEAD` under a trap, hash equal to `HEAD`'s after): `private` dropped from `originGatedRemovalRefusal` and the package rebuilt. Both `.d.ts` files move to `fec45eed6afc2fff…`, so the bar can see a change on these members. - **Export tables.** The ESM and CJS bundles, loaded at runtime from the package, each export the same 78 names at base and head; the sorted list hashes to `9f18a0177ac736ed…` on all four. Neither moved function nor the host type appears. No changeset: `skip-changeset`, per ruling `6348891160`. ## Proof of move-only - **Bodies, by AST token** (the TypeScript compiler's AST leaf tokens, JSDoc skipped, over parameters less the added `this`, return type and body): 179 tokens compared, **0 differ**. The parameter, return-type and body text, comments included, is also equal with whitespace collapsed, for both. Negative control: one changed token in `refusePackagedBaseOverride`'s body (`namedBase &&` to `namedBase ||`, on an in-memory copy) makes the check report `differ=1` on that member and 0 on the other. - **Class side.** 300 members at base and at head, in the same order. The 298 that did not move are byte-identical to base, leading comments included. Each of the 2 stubs sits at its base position and keeps its docblock and signature byte-identical (everything before the body's opening brace). - **Top level of `protocol.ts`.** 240 of the 241 base statements other than the class are byte-identical, leading comments included. The other one is the `./packaged-base-regime.js` import, which loses `isOriginGatedType`. Head adds one statement, the new module's import. - **Size.** +157 / −75 over 3 files, 232 changed lines (the order's estimate was 250–320 for three members; the narrowed cut is smaller). `protocol-packaged-base-refusal.ts` has 140 lines. ## Readers of the moved lines Every test and script that reads `protocol.ts` by path, and every test file in `packages/` and `scripts/`, was grepped for the three proposed member names and the host name; so was `docs/qa/`, which cites methods by name. - `protocol.packaged-base-refusal.test.ts` (lines 9, 225, 231), `protocol.package-door-before-gates.test.ts` (line 7) and objectql `engine-security-catalog-package-door.test.ts` (line 13) name `refusePackagedBaseOverride` / `refusePackagedBaseRemoval` **in comments only**; none reads `protocol.ts` by path or asserts on the bodies' text. Verdict: no edit owed; all three pass at head (see Verification). - `docs/qa/platform-checklist/areas/access-security.json` cites `packages/metadata-protocol/src/protocol.ts#refusePackagedBaseOverride` as an anchor (line 1517) and in prose (line 1459). The method keeps its name in `protocol.ts`, and `check-platform-checklist --changed-from origin/main` judges it: OK, 1 anchored file in the change set, 29 anchors judged, 0 lost. - No other hit outside `CHANGELOG.md` entries. - **Bracketed-refusal pin** (`PRODUCERS`): `refusePackagedBaseOverride`'s one `new Error(` moves with its body, so the new module gets a row, `'protocol-packaged-base-refusal.ts': 1`, the module's own count (the order's "two" counted the one in `refusePackagedBaseRemoval`, which stays). `protocol.ts` goes from 72 to 71 such sites, still above its floor of 5. Control: with that throw changed to `new TypeError(` (on disk, restored from `HEAD` under a trap, hash equal to `HEAD`'s after; on-disk counts read first: 0 `new Error(`, 1 `new TypeError(`), the new row fails with "protocol-packaged-base-refusal.ts no longer constructs errors here — this pin is scanning the wrong file: expected 0 to be greater than or equal to 1" (1 failed, 14 passed); restored, 15/15 pass. - **ADR anchors and decision letters.** `check-adr-anchors` exits 0 at both ends: 85 anchored files, 58,860 citations, files 8,259 → 8,260, and **1,847 → 1,843** decision-letter citations. The four that leave the anchored file are comments in `refusePackagedBaseOverride`'s body: three `ADR-0131 D6` and one `ADR-0070 D1`. The `protocol.ts` anchor entry's four paragraphs (ADR-0029 D9.8/D9.9 object hydration, ADR-0119 atomic batches, ADR-0005 appendix (c) view storage, ADR-0126 §2 / §3 flow regime) do not describe the package door, so the new module gets no entry (disclosure only, as cuts 8 and 12 disclosed theirs). `ADR-0005`, which the moved comments also cite, stays referenced from `protocol.ts` (the gate's "every governing ADR still referenced" holds). - **Durability checker**: `check-durability-degradation-log-level` exits 0 at both ends with identical output, and its `--list` is identical once `[@:][0-9]+` is stripped (159 lines): the moved bodies hold no `try`/`catch`. - **Engine-double contract** (`check-engine-double-contract`): exits 0 at both ends. The `updateData`/`deleteData` seams keep their entries at new line numbers (14083 → 14085, 14108 → 14110). `productionFiles` is 3,707 at base and 3,709 at head: +1 is the new file, +1 is the gitignored `packages/spec/src/migrations/registry.ts`, which the spec build writes in the head tree and the clean base tree had not built. - **Identical output at both ends:** `check-adr-symbol-anchors`, `check-system-context-census`, `check-tenant-audit-census`, `check-query-options-erasure-ratchet`, `check-filter-alias-parity`. `check-nul-bytes` differs only in the scanned file count, +1. The base-end readings were taken in a clean detached worktree at `7a04ba288d`. - **Governed surface.** `check-governed-merges --branch claude/project-thread-ybjm3h`: 0 of 3 paths hit the register, NOT governed; 232 changed lines, under the 3,000-line human-merge bar. ## Verification - `tsc --noEmit` in `packages/metadata-protocol` exits 0 before and after the merge. `pnpm exec eslint --no-inline-config packages/metadata-protocol` (the whole touched package; it has no `lint` script, these are the root `lint` script's flags) exits 0, no output. - **`packages/metadata-protocol` suite**, base and head, compared per test name and status (vitest `--maxWorkers=2`, JSON reporter, each run under the shared verify lock, `VERDICT command-exit 0` both legs), on the pre-merge tree (the merge changed no file of the package): - Base `7a04ba288d`: 21,537 tests (21,518 passed, 0 failed, 19 skipped), 267 files. - Head `1c09db420`: 21,538 tests (21,519 passed, 0 failed, 19 skipped), 267 files. - None lost, none changed status, one added: the new `PRODUCERS` row's case. The base leg is the package `src/` swapped tree-only from `7a04ba288d` (the new module removed from the tree) and restored from `HEAD` under a trap. - **Cross-package pin named in prose.** objectql `engine-security-catalog-package-door.test.ts` at head `b1ac1d8a9`, under the verify lock: 1 file, 18/18 passed, `VERDICT command-exit 0`. The two metadata-protocol pins named above ran inside the package suite and pass at both ends. - **Derived gates.** `dispatch-gates --commands --repo objectstack-ai/objectstack` derives 59 gates on 3 changed paths, the same list before and after the merge; all 59 exit 0 on the merged tree `b1ac1d8a9`, after the full `pnpm build` and with the plugin-teardown fixture commit fetched first. `dispatch-gates --ran`: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. ## Acceptance notes - `refusePackagedBaseRemoval` and `refusePackagedBaseOverride` now live in two files, though their docblocks describe them as twins (one predicate, `isSealedManagedItem`). The removal twin can follow only once private-static readers can move (the stay list's reason); until then the class docblocks, which did not move, still name each other with `{@link}`. - The proposal's static-read screen was a text grep and missed a two-line spelling; this cut's screen walks the AST. The cut 14 proposal in the report was screened the same way. ## Carry-forward Unchanged from cut 12: public members whose return type is inferred, the bodies kept by a reader (`probeRecord`, `getMetaItemCached`, `findReferencesToMeta`, `packagelessAddress`, `refuseUnmintableMetaType`, `metaTypeNamespaceExists`, `viewContainerNameCollisionRefusal` with `overlaidShippedContainerViewNames`, `reportPageRequiresAbsentAtLoad`, the 7 `applyRegistryWriteThrough` callers, `readCodeLayerForCarryForward`), and the private-static readers (now named among them: `refusePackagedBaseRemoval` and `packagedBaseRefusal`, besides `runPublishSideEffects`, `promoteDraftForPublish`, `publishPackageDrafts` and `discardPackageDrafts`) stay in `protocol.ts`. --- _Generated by [Claude Code](https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cdd4eea commit 0479cd6

3 files changed

Lines changed: 157 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.bracketed-refusal-opener-absence.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ const PRODUCERS: Readonly<Record<string, number>> = {
9090
// The publish backfill pair, moved out of `protocol.ts` whole on the same
9191
// terms.
9292
'protocol-publish-backfill.ts': 3,
93+
// The package door's save refusal, moved out of `protocol.ts` whole on the
94+
// same terms.
95+
'protocol-packaged-base-refusal.ts': 1,
9396
'sys-metadata-repository.ts': 5,
9497
'runtime-authoring-gate.ts': 1,
9598
};

‎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)