Skip to content

Commit 9cc2c79

Browse files
fix(metadata-protocol)!: the metadata door refuses an edit of a code-defined datasource, and removes only a stored row left under one (#21942)
Fixes #21899 Clause-②: no (narrowing) ## What changes The metadata door now answers a code-defined datasource the way the published contract (`DatasourceSchema.origin`: "code — authored as `*.datasource.ts`, GitOps-owned, read-only in the UI") and the datasource-admin door already did: read-only. Triage ruled Q1-A and Q2-B in 6006929054; this PR implements both, in `packages/metadata-protocol` only. - **The resolver (Q1-A).** `isArtifactBacked` (`packages/metadata-protocol/src/protocol.ts`) gains a second non-standalone-artifact resolver, `isDeclaredCodeDatasource`, in the `isNestedArtifactField` shape from #7743. It reads the installed packages' declared `datasources` (`registry.getAllPackages()`, each record's `manifest.datasources`, in the canonical array form `defineStack` leaves). `datasource` is added to that docblock's census. It never reads a MetadataService slot's `origin` or a request body's `origin`; a unit case pins a body asserting `origin: 'runtime'` on a code datasource as still refused, and one asserting `origin: 'code'` on a runtime name as still saved. - **The package door's answer.** With the resolver in place, the existing door refuses the save on both kernel shapes: `refusePackagedBaseOverride` on an environment kernel, and the repository write intent (`override-artifact` into `SysMetadataRepository.assertAllowed`) on a host-config kernel, which is the showcase's shape. The answer is `NOT_OVERRIDABLE` / 403. The sentence comes from the packaged-base sentence table (`packaged-base-regime.ts`), which gains one `origin-gated` row for `datasource`. ADR-0126 §3 records `datasource` outside the three regimes, as "origin-gated: code-defined read-only, runtime-created free", so the row is not a Regime C row. It carries no routes, only the owning source. - **DELETE as repair (Q2-B).** A `DELETE` that would remove nothing is refused with the same verdict. A `DELETE` of an existing stored row answers 200. Where the carve-out lives: - `ObjectStackProtocolImplementation.originGatedRemovalRefusal` (new, beside `refusePackagedBaseRemoval`) holds the package door's removal verdict. `deleteMetaItem` answers it at its row probe: it throws when no stored row exists and lifts it when one does. - `SysMetadataRepository.assertDeleteAllowed` mirrors the lift for the same type through `isOriginGatedType`. This is the topology-independent gate a host-config kernel asks. - The reasoning is reused, not invented. It is `saveMetaItem`'s #5086 record ("removing a code-only row that predates this refusal is repair, and must stay possible") and the #6960 ruling (removal restores the code-declared state, which is the narrowing direction). #6960's own `supportsOverlay` boundary is not widened: `object`, which shares `datasource`'s registry flags, keeps refusing both verbs, and a guard pins that. - **The read envelope.** `servedLockState` reports what the doors do: `editable: false`, and `deletable` true only while the read found a stored row to remove. ## The two doors' codes differ, by ruling Triage's answer 6006929054: "The two doors' codes differ, and that is accepted. Each door speaks its own vocabulary; the verdict and the remedy agree." Measured on a real showcase boot at this branch: - metadata door `PUT`: `403 NOT_OVERRIDABLE` — `Datasource 'showcase_external' is code-defined and cannot be edited at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy. See docs/adr/0062-external-datasource-runtime.md.` - admin door `PATCH`: `400 DATASOURCE_ADMIN_ERROR` — `Datasource 'showcase_external' is code-defined and cannot be edited at runtime.` - metadata door `DELETE` with no stored row: `403 NOT_OVERRIDABLE` — `Datasource 'showcase_external' is code-defined and cannot be removed at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy. See docs/adr/0062-external-datasource-runtime.md.` - admin door `DELETE`: `400 DATASOURCE_ADMIN_ERROR` — `Datasource 'showcase_external' is code-defined and cannot be removed at runtime.` ## The host's `default` datasource (H4): the admin door treats it as code-defined; covering it here is a named gap - **Measured** on a real showcase boot (`bootStack`, admin routes mounted as `serve.ts` mounts them): - `PATCH /api/v1/datasources/default` answers `400 DATASOURCE_ADMIN_ERROR` "Datasource 'default' is code-defined and cannot be edited at runtime.", and `DELETE` answers "… cannot be removed at runtime.". - On the metadata door, `PUT /api/v1/meta/datasource/default` answers 200 "Saved datasource 'default' (env-wide, state=active)" and the read then serves the edit. `DELETE` answers 200. - This PR leaves `default` unchanged: `PUT` answers 200 on this branch too, which is measured. - **Why it is not covered here.** The host's code datasource set is not readable from `metadata-protocol` without a `runtime` or `service-datasource` change: - `DefaultDatasourcePlugin` registers `default` only through `MetadataService.registerInMemory`. That is the slot whose `origin` triage ruled unsound, because a stored row overwrites it. - The connection service's retained state carries no origin (`ConnectResult`: name, status, reason, ownership). - The engine's `listDatasourceDefs()` mixes code and runtime definitions. - No package declares `default`. - **What follows.** Under the claim's stop condition, no `runtime` or `service-datasource` file is edited. This is reported for a follow-up card, and the changeset names it. ## Pins, before and after (real showcase boot, `showcase_external`) "Before" is the reverse-verification leg below (the base `isArtifactBacked` on committed HEAD) and the first run's measurements at `54fb60ac3f` (6006105473). "After" is this branch. | Pin | Before | After | |:--|:--|:--| | `PUT /meta/datasource/showcase_external` | 200 "Saved datasource 'showcase_external' (env-wide, state=active)", a row persisted, the read served the edit | 403 `NOT_OVERRIDABLE` with the verdict and remedy above; no `sys_metadata` row; the read serves the code label | | `DELETE`, no stored row | 200 "No datasource 'showcase_external' found - nothing to delete." | 403 `NOT_OVERRIDABLE`, "cannot be removed at runtime" | | `DELETE`, a pre-existing stored row (seeded as a pre-fix save wrote it, across a restart) | 200, reset true | 200, reset true, row gone; a second `DELETE` answers 403 | | after the repair and one more restart | — | both doors serve "External Analytics (SQLite)", origin `code`, `_packageId` `com.example.showcase`; no row | | runtime datasource | admin door: `POST` 201, `PATCH` 200, `DELETE` 204; metadata door: `PUT` 200, `PUT` 200, `DELETE` 200 | the same | ## Reverse verification Run on committed HEAD `8413b4622d`, through `scripts/ablation-replace.mjs` (WRAP mode, its own restore trap, plus a `git checkout HEAD` trap): - **The mutation** restores the base form of `isArtifactBacked`'s last line, dropping `|| this.isDeclaredCodeDatasource(type, name)`. On disk the anchor went 1 to 0 and the replacement 0 to 1. The blob went `8e2d759618ba` to `51f36f712468`. - **Unit suite (src):** `protocol.code-defined-datasource-door.test.ts` showed 14 failed and 11 passed. The red cases are the resolver, `PUT` refused (both kernels), `DELETE` with no row refused, the repair's second `DELETE`, the read envelope, and the 500-character bound. The green cases are the runtime controls, the hatch guard and the repository gate cases, which do not route through `isArtifactBacked`. - **Dogfood (dist):** - `pnpm --filter @objectstack/metadata-protocol build` emitted ESM/CJS and failed only DTS on TS6133, because the mutation leaves the new method unused. - `node scripts/ablation-dist-preflight.mjs @objectstack/metadata-protocol '...' --absent` confirmed the marker absent from all 22 built files. - `meta-door-code-datasource.dogfood.test.ts` showed 4 failed and 2 passed. The `PUT` pin read `expected { status: 200, code: undefined } to deeply equal { status: 403, code: 'NOT_OVERRIDABLE' }`. - **Restore:** the blob after restore equals HEAD (`8e2d759618ba`), `git diff HEAD` is empty, and `git status --porcelain` is empty. A rebuild put the marker back in both built entry files (preflight: present). The unit file then passed 25/25 and the dogfood file 6/6. ## Tests (HEAD `dd81fb50d5`) - `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2`: 217 files passed (3 skipped), 27941 tests passed. - `pnpm --filter @objectstack/metadata-protocol typecheck` and `pnpm --filter @objectstack/dogfood typecheck` are green. `tsc --listFiles` includes every touched test file. - Dogfood: `meta-door-code-datasource.dogfood.test.ts` (6/6) and `external-import-code-datasource-namespace.dogfood.test.ts`. - Consumer files touching the datasource `/meta` door were run, all green: - `runtime`: `datasource-visibility`, `meta-type-write-capability-parity`, `stored-metadata-reader-contexts.pin`, `standalone-stack-hydrate-metadata`, `meta-write-org-scope`, `dispatcher-plugin.declared-5xx-prose-withhold`. - `rest`: `meta-type-read-capability`, `meta-type-write-capability`, `rest-server-meta-write-org-scope`, `meta-unknown-type-read-refusal`, `rest-server-meta-org-scope-url-spelling`, `rest`. - `service-datasource`: `datasource-admin-record-judgement`. - `objectql`: `overlay-precedence`. - Three existing sweeps had pinned the old `datasource` delete refusal and were triaged. `protocol.delete-rewrap-envelope`, `protocol.legacy-overlay-delete` and `protocol.read-lock-flags-write-door` exclude the origin-gated type from the derived refusal sweeps or measure it at the protocol's delete door. Each change points at the new pin file. ## Gates (HEAD `dd81fb50d5`) - `node scripts/pm/dispatch-gates.mjs --commands` derived 76 commands, and all 76 were run with exit 0. `--ran` reconciliation reports "76 derived famil(ies) accounted for — 76 run, 0 NOT-MEASURED". - `check:engine-double-contract` asked for its ledger to learn the new file's pinned doubles (`--write`, +3 rows, committed). - The artifact-roster block (53) ran: 50 exited 0. `check-closing-target-claim`, `check-partof-closing-keyword` and `check-single-claim-paths` exited 2 with no PR context (NOT MEASURED locally); they run on this PR in CI. - The four symbol-anchor sweeps (`check:adr-symbol-anchors`, `check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors`, `check:adr-anchors`) exited 0. - `check:adr-0087-registration --base origin/main`: one declared-breaking changeset, `not-required (no-migration-prescription)`. `check-changeset-no-major` reports no major. ## Acceptance notes - **Same boot after the repair `DELETE`.** The read keeps serving the stored copy until the next restart, because the datasource-admin plugin's boot restore registered it in the MetadataService. The receipt still reads "reset to artifact default". That is #21922's in-memory half, so the repair pin holds across a restart. Measured: in the same boot, `GET` after the repair served "Shadow 21899"; after the restart it served the code label. - **An admin-created runtime datasource cannot be edited or deleted through the metadata door.** Both answer `409 METADATA_CONFLICT`, whether or not the version token is sent, because the admin door's `sys_metadata` row carries a null checksum. This is pre-existing and untouched here (runtime names are not artifact-backed). It is reported for filing in the dev report. The PM's hypothesis that sending the version makes it pass was measured false. - **H2's "packaged-base sentence table".** The table gains an `origin-gated` row rather than a Regime C row, per ADR-0126 §3. Its module header now scopes the "no redeploy prescription" rule to Regime C sentences. - **The operator hatch.** `OS_METADATA_WRITABLE=datasource` still opens the lock exactly as before; a guard case pins it. - **Not measured.** An artifact whose top-level `datasources` no package body declares (`AppPlugin` warns about this composition at boot) registers code datasources the resolver does not see, because no package record carries them. ## Changeset `.changeset/21899-meta-door-code-datasource-read-only.md`: `@objectstack/metadata-protocol` minor, BREAKING, `Clause-②: no (narrowing)`. It states the remedy: edit the `*.datasource.ts` source, and delete a stored row through the metadata door to repair. It carries one ADR-0087 marker. --- _Generated by [Claude Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1f04696 commit 9cc2c79

10 files changed

Lines changed: 1080 additions & 55 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/metadata-protocol": minor
3+
---
4+
5+
fix(metadata-protocol)!: the metadata door refuses an edit of a code-defined datasource, and removes only a stored row left under one (#21899)
6+
7+
Clause-②: no (narrowing)
8+
9+
A datasource an installed package declares in `*.datasource.ts` is code-defined: `DatasourceSchema.origin` publishes it as "GitOps-owned, read-only in the UI", and the datasource-admin door already refused to edit or remove one. The metadata door did not. The runtime registers a code-defined datasource in memory only, never as a registry item, so the door's artifact check missed it and the write took the runtime-create tier: `PUT /api/v1/meta/datasource/:name` answered 200, persisted a row, and the metadata read then served that row in place of the code definition.
10+
11+
The door's artifact check now reads the datasources the installed packages declare, so the package door that refuses every other code-shipped item of a type with no overlay channel refuses this one too.
12+
13+
**BREAKING — what moves for consumers.**
14+
15+
- `PUT /api/v1/meta/datasource/:name` on a code-defined datasource answered 200 and now answers `403 NOT_OVERRIDABLE`: "Datasource ':name' is code-defined and cannot be edited at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy."
16+
- `DELETE /api/v1/meta/datasource/:name` on a code-defined datasource with no stored row answered 200 ("nothing to delete") and now answers the same `403 NOT_OVERRIDABLE`, saying "cannot be removed at runtime".
17+
- The admin door keeps its own `400 DATASOURCE_ADMIN_ERROR`. The two doors' codes differ; the verdict and the remedy are the same.
18+
- The metadata read envelope reports the same answers: `editable: false`, and `deletable` true only while a stored row exists under the name.
19+
20+
**Remedy.**
21+
22+
- To change a code-defined datasource, edit the `*.datasource.ts` source that declares it and redeploy.
23+
- A row an earlier `PUT` stored under a code-defined datasource's name is still removable, and removing it is the repair: `DELETE /api/v1/meta/datasource/:name` answers 200 and deletes it, once per name. After the next restart both doors serve the code definition again. Until that restart the datasource-admin service keeps the stored copy it restored at boot (tracked in #21922).
24+
25+
**Unchanged.** A runtime datasource, one no package declares, saves and deletes through the metadata door as before. `OS_METADATA_WRITABLE=datasource` opens the lock exactly as it did. The host's `default` datasource is declared by no package, so the metadata door still accepts edits to it as before; the admin door refuses them.
26+
27+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of metadata-door writes the published contract already forbids, on datasources an installed package declares: no authorable key, spelling, export or stored shape moves, and no stored row is read, rewritten or converted. A row an earlier save left under a code-defined name stays readable and is removed by the operator with the door's own DELETE; which stored edit an operator meant to keep is not something a ledger entry can rewrite. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this door (not already-registered); and the change is a door verdict, not a declaration (not runtime-interface-only or type-surface-only). -->

‎packages/metadata-protocol/src/packaged-base-regime.ts‎

Lines changed: 121 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,44 @@
6363
* `skill` / `position` — no sanctioned path is built for them, and a row with no
6464
* route does not compile ({@link PackagedBaseRegimeCRoutes}).
6565
*
66+
* ## The one origin-gated row
67+
*
68+
* [#21899] ADR-0126 §3 places `datasource` OUTSIDE the three regimes, and says
69+
* how: "origin-gated: code-defined read-only, runtime-created free". A
70+
* code-defined datasource has no runtime route at all — no overlay, no clone,
71+
* no switch: `DatasourceSchema.origin` (`@objectstack/spec`) declares it
72+
* "authored as `*.datasource.ts`, GitOps-owned, read-only in the UI", ADR-0062
73+
* ratifies it read-only, and the datasource-admin service refuses to edit or
74+
* remove one ("… is code-defined and cannot be edited at runtime."). So its row
75+
* carries no routes; it names the source that owns the datasource, and its
76+
* sentence states the admin door's verdict in the admin door's words, then that
77+
* remedy. The two doors keep their own codes (`NOT_OVERRIDABLE` / 403 here,
78+
* `DATASOURCE_ADMIN_ERROR` / 400 there); the verdict and the remedy agree.
79+
*
80+
* The row is also what {@link isOriginGatedType} answers from, for the one
81+
* removal both the protocol's delete door and the repository's delete gate
82+
* allow on such a type: deleting a STORED row under a code-defined name. The
83+
* runtime registers a code-defined datasource in memory only and never
84+
* persists it, so a stored row under its name is never a layer of it — it is
85+
* residue a runtime write left — and removing it restores the code definition.
86+
*
6687
* ⛔ No sentence built here names the `OS_METADATA_WRITABLE` hatch. The hatch
6788
* still opens these locks exactly as before, so which writes are refused does
68-
* not move — only what the refusal prescribes. ⛔ Nor does any prescribe editing
69-
* the source and redeploying: the administrator of an installed package cannot
70-
* do that.
89+
* not move — only what the refusal prescribes. ⛔ Nor does a Regime C sentence
90+
* prescribe editing the source and redeploying: the administrator of an
91+
* installed package cannot do that, and a Regime C type has a runtime route
92+
* instead. The origin-gated row has none, so the source is the only remedy
93+
* there is to name.
7194
*/
7295

7396
import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared';
7497

75-
/** An ADR-0126 customization regime a packaged-base refusal speaks for — only the ones it needs today. */
76-
export type PackagedBaseRegime = 'C';
98+
/**
99+
* An ADR-0126 customization regime a packaged-base refusal speaks for — only the
100+
* ones it needs today: Regime C, and the origin-gated posture §3 records for a
101+
* type outside the regimes.
102+
*/
103+
export type PackagedBaseRegime = 'C' | 'origin-gated';
77104

78105
/**
79106
* The sanctioned routes a Regime C type's row supplies to its refusal: how to
@@ -86,11 +113,21 @@ export type PackagedBaseRegimeCRoutes =
86113
| { readonly clone: string; readonly switchOff?: string }
87114
| { readonly clone?: string; readonly switchOff: string };
88115

89-
/** One type's row: its regime, and the routes that regime names for it. */
90-
export interface PackagedBaseRegimeRow {
91-
readonly regime: PackagedBaseRegime;
92-
readonly routes: PackagedBaseRegimeCRoutes;
93-
}
116+
/**
117+
* One type's row: its regime, and what that regime names for it — a Regime C
118+
* type's sanctioned routes, or an origin-gated type's owning source.
119+
*/
120+
export type PackagedBaseRegimeRow =
121+
| { readonly regime: 'C'; readonly routes: PackagedBaseRegimeCRoutes }
122+
| {
123+
readonly regime: 'origin-gated';
124+
/** The type's noun, opening the sentence the way the type's own admin door opens it. */
125+
readonly noun: string;
126+
/** The source pattern a code-defined item of the type is authored in. */
127+
readonly source: string;
128+
/** The decision record the sentence cites. */
129+
readonly docs: string;
130+
};
94131

95132
/** The table. Keyed by the canonical (singular) metadata type. */
96133
export const PACKAGED_BASE_REGIME: Readonly<Record<string, PackagedBaseRegimeRow>> = {
@@ -117,6 +154,12 @@ export const PACKAGED_BASE_REGIME: Readonly<Record<string, PackagedBaseRegimeRow
117154
+ 'or POST /api/v1/data/sys_permission_set with a new name',
118155
},
119156
},
157+
datasource: {
158+
regime: 'origin-gated',
159+
noun: 'Datasource',
160+
source: '*.datasource.ts',
161+
docs: 'docs/adr/0062-external-datasource-runtime.md',
162+
},
120163
};
121164

122165
/** The type's row, read on the canonical type, or `undefined` when it declares no regime. */
@@ -128,37 +171,62 @@ export function packagedBaseRegimeRow(type: string): PackagedBaseRegimeRow | und
128171
}
129172

130173
/**
131-
* The PRESCRIPTION half of a regime's refusal, built from the row: for Regime C
132-
* the row's sanctioned paths in one fixed order — clone first, then the switch —
133-
* and the citation of the ADR that decided them. Nothing in it reads the type:
134-
* a flow reads "Clone it …, or switch it off …" because its row has both, an
135-
* action reads only the switch and a permission set only the clone because
136-
* theirs have one.
174+
* A Regime C row's prescription: its sanctioned paths in one fixed order —
175+
* clone first, then the switch — and the citation of the ADR that decided them.
176+
* Nothing in it reads the type: a flow reads "Clone it …, or switch it off …"
177+
* because its row has both, an action reads only the switch and a permission set
178+
* only the clone because theirs have one.
137179
*/
138-
const PRESCRIPTION_BY_REGIME: Readonly<Record<PackagedBaseRegime, (routes: PackagedBaseRegimeCRoutes) => string>> = {
139-
C: (routes) => {
140-
// [verb opening the sentence, verb after "or", the rest of the path]
141-
const paths: Array<readonly [string, string, string]> = [
142-
...(routes.clone
143-
? [['Clone', 'clone', ` it under a new name to customize it (${routes.clone})`] as const]
144-
: []),
145-
...(routes.switchOff ? [['Switch', 'switch', ` it off (${routes.switchOff})`] as const] : []),
146-
];
147-
const prescription = paths
148-
.map(([opening, following, rest], i) => (i === 0 ? opening : following) + rest)
149-
.join(', or ');
150-
return `${prescription}. See docs/adr/0126-packaged-metadata-customization-model.md.`;
151-
},
152-
};
180+
function regimeCPrescription(routes: PackagedBaseRegimeCRoutes): string {
181+
// [verb opening the sentence, verb after "or", the rest of the path]
182+
const paths: Array<readonly [string, string, string]> = [
183+
...(routes.clone
184+
? [['Clone', 'clone', ` it under a new name to customize it (${routes.clone})`] as const]
185+
: []),
186+
...(routes.switchOff ? [['Switch', 'switch', ` it off (${routes.switchOff})`] as const] : []),
187+
];
188+
const prescription = paths
189+
.map(([opening, following, rest], i) => (i === 0 ? opening : following) + rest)
190+
.join(', or ');
191+
return `${prescription}. See docs/adr/0126-packaged-metadata-customization-model.md.`;
192+
}
193+
194+
/**
195+
* The PRESCRIPTION half of a regime's refusal, built from the row alone: a
196+
* Regime C row's sanctioned paths ({@link regimeCPrescription}), or an
197+
* origin-gated row's owning source — the only remedy such a type has — and the
198+
* row's citation.
199+
*/
200+
function rowPrescription(row: PackagedBaseRegimeRow): string {
201+
switch (row.regime) {
202+
case 'C':
203+
return regimeCPrescription(row.routes);
204+
case 'origin-gated':
205+
return `Edit the ${row.source} source that declares it and redeploy. See ${row.docs}.`;
206+
}
207+
}
153208

154209
/**
155-
* The regime prescription for `type` — the row's sanctioned paths and the
156-
* ADR-0126 citation, one sentence pair, no opener — or `undefined` when the type
157-
* declares no regime and the emitter keeps its own remedy.
210+
* The regime prescription for `type` — the row's remedy and its citation, one
211+
* sentence pair, no opener — or `undefined` when the type declares no regime and
212+
* the emitter keeps its own remedy.
158213
*/
159214
export function packagedBaseRegimePrescription(type: string): string | undefined {
160215
const row = packagedBaseRegimeRow(type);
161-
return row ? PRESCRIPTION_BY_REGIME[row.regime](row.routes) : undefined;
216+
return row ? rowPrescription(row) : undefined;
217+
}
218+
219+
/**
220+
* [#21899] Is `type` origin-gated (ADR-0126 §3: code-defined read-only,
221+
* runtime-created free)? The one removal such a type allows on a code-defined
222+
* name is deleting a STORED row under it: the runtime never persists a
223+
* code-defined item of the type, so the row is residue a runtime write left,
224+
* never a layer of the item, and removing it restores the code definition. Read
225+
* by the protocol's delete door and by the repository's delete gate — one row,
226+
* so the two cannot disagree about which types this is.
227+
*/
228+
export function isOriginGatedType(type: string): boolean {
229+
return packagedBaseRegimeRow(type)?.regime === 'origin-gated';
162230
}
163231

164232
/**
@@ -167,20 +235,30 @@ export function packagedBaseRegimePrescription(type: string): string | undefined
167235
* `undefined` when the type declares no regime and the emitter keeps its own
168236
* sentence. Read on the canonical type, and spoken with it.
169237
*
238+
* The lock is the regime's: a Regime C item "is provided by a code package, and
239+
* its packaged base is locked"; an origin-gated item "is code-defined and cannot
240+
* be edited (removed) at runtime: it is read-only" — the datasource-admin
241+
* service's own verdict on the same item, so the two doors onto one code-defined
242+
* datasource state one verdict and one remedy.
243+
*
170244
* Kept under the REST door's 500-character client-message bound
171245
* (`truncateClientMessage`, `packages/rest/src/error-response.ts`), past which
172246
* the tail is truncated. Characters before the item's name, save / removal:
173-
* `flow` 411 / 404, `action` 365 / 358, `permission` 317 / 310 — so a name of up
174-
* to 88 characters arrives whole for every row (pinned). A `flow`'s sentence is
175-
* byte-identical to the one the row table replaced (pinned literally).
247+
* `flow` 411 / 404, `action` 365 / 358, `permission` 317 / 310, `datasource`
248+
* 192 / 193 — so a name of up to 88 characters arrives whole for every row
249+
* (pinned). A `flow`'s sentence is byte-identical to the one the row table
250+
* replaced (pinned literally).
176251
*/
177252
export function packagedBaseRegimeSentence(
178253
type: string, name: string, operation: 'save' | 'delete',
179254
): string | undefined {
180255
const singular = PLURAL_TO_SINGULAR[type] ?? type;
181-
const prescription = packagedBaseRegimePrescription(singular);
182-
if (prescription === undefined) return undefined;
183-
return `Metadata item '${singular}/${name}' is provided by a code package, and its packaged base is locked `
184-
+ (operation === 'delete' ? `against removal. ` : `against in-place edits. `)
185-
+ prescription;
256+
const row = packagedBaseRegimeRow(singular);
257+
if (row === undefined) return undefined;
258+
const lock = row.regime === 'origin-gated'
259+
? `${row.noun} '${name}' is code-defined and cannot be `
260+
+ (operation === 'delete' ? 'removed' : 'edited') + ' at runtime: it is read-only. '
261+
: `Metadata item '${singular}/${name}' is provided by a code package, and its packaged base is locked `
262+
+ (operation === 'delete' ? `against removal. ` : `against in-place edits. `);
263+
return lock + rowPrescription(row);
186264
}

0 commit comments

Comments
 (0)