Skip to content

Commit d8ed2e1

Browse files
feat(scripts): codemod-split-protocol --members moves a member of the landed protocol.ts facade, and moves six #30054 door bodies (#30687)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DEaLDouphrUMvBWbK52b3uh)_ Refs #30518 Refs #30054 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:给 `scripts/codemod-split-protocol.mjs` 加了 `--members` 通道,能在已落地的 protocol.ts 门面上把单个方法体搬进它所在的 facet;并用它搬走了 #30054 的六个门(getData、cloneData 与四个 *MetaItem)。 - 为什么改:原 codemod 拒绝在自己的输出上运行,`--verify` 也因 facet 模块被手改过而无法再证明,八个门的方法体只能留在类上当例外。 - 风险与回滚:纯搬移,方法体逐字节不变,公开 `index.d.ts` / `index.d.cts` 前后字节相等;回滚即 revert 本 PR。 - 先看:`scripts/codemod-split-protocol.mjs` 的 members 一节(生成与逆向证明);`protocol-data-crud.ts` 新增的 import 块;`scripts/facade-delegation-registry.json`。 - 两分钟验证:`node scripts/codemod-split-protocol.mjs --members --verify --base f4d568b`(本 PR 的基点),预期输出 `undoing the moves does rebuild … byte-for-byte`、退出码 0。 - 需要你做的:无,席位落地。 ## What changes **The tool.** `scripts/codemod-split-protocol.mjs --members A,B [--write]` moves named instance methods of the split `ObjectStackProtocolImplementation` into the facet of their section, exactly as the first pass would have taken them: the same stay rules (`plan`, any reason refuses), `host-split.mjs`'s `planMove` and `insertHostEntries`, the first pass's lift and docblock-ownership rule, and the lifted one-line stub. `--members --verify --base REF` re-proves the moves on disk. The first pass is unchanged and still refuses its own output. It accepts the hand edits `main`'s facet modules carry (header paragraphs after the generated first one, imports, top-level declarations, facet members nobody moved). It grows only: the generated first header paragraph (the name, re-wrapped), the facet class (the member in class order), and the import block (a binding joins a declaration of its specifier and kind in that declaration's own layout, or becomes a new declaration, in `protocol.ts`'s import order). It refuses a facet `protocol.ts` does not already import, a body reading a top-level name the generated export list does not export, a first paragraph that is not the generated one, and a binding the module holds under another source or kind. This is the same acceptance rule #30572 asks of the `engine.ts` `--members` pass (hand-edited headers and import blocks), kept consistent here; the engine codemod is not touched. **The proof** is the undo against the pre-move tree, not the split's #29985 base: from the head alone, each moved body (rewrites and re-indentation undone) and any owned docblock goes back on its stub; each receiving module loses the moved members, their header names and the import bindings nothing reads any more; the factory and the interface lose the entries the base host did not carry. The result must be the base `protocol.ts` (its import bindings only moved bodies read pruned, as the first pass prunes them), `protocol-host.ts` and receiving modules byte for byte, every other section module unchanged, each facet member its base method's text as the first pass lifts it, and `host-split.mjs`'s `checkMove` green on the unlifted pair. **The moves.** `getData`, `cloneData`, `saveMetaItem`, `publishMetaItem`, `rollbackMetaItem` and `deleteMetaItem` now delegate to `ProtocolDataCrudFacet`, `ProtocolSaveFacet`, `ProtocolHistoryAndPublishFacet`, `ProtocolPackageCommitsFacet` and `ProtocolDiffDeleteLoadFacet`; `protocol-host.ts` gains the eight entries those bodies read. Their six rows leave `scripts/facade-delegation-registry.json` (50 named exceptions remain). Written by `node scripts/codemod-split-protocol.mjs --members getData,cloneData,saveMetaItem,publishMetaItem,rollbackMetaItem,deleteMetaItem --write`, no hand edit after it. `getDiscovery` and `findData` stay for the second PR of #30518: all eight together come to 3,775 changed lines, over the 3,000-line budget. Their two rows now name #30518. ## Why #30054 spelled the eight doors' return types (PR #30516) so their bodies could leave the facade, but the codemod could not move a member of a facade that had already landed. #30518 asked for that pass. ## Tests - `node scripts/codemod-split-protocol.mjs --self-test`: 85 cases in 10 batteries pass. Three new batteries: `members pass` (8: the move proves; stub; class-order placement before the next member and last; header paragraph with a hand-written paragraph kept; import joins its declaration; new declaration ahead of `ProtocolHost`'s with a hand-written import kept; `protocol.ts` prune; determinism), `members proof` (12 negative controls, each must change the head and must fail: an edited moved body, an edited hand-written facet member, a second blank line, a header missing a name, a changed untouched module, an edited interface entry, an entry no `Pick` names, a stub calling another facet, an edited public docblock, an edit elsewhere in `protocol.ts`, a dropped import no moved body reads, a proof over nothing) and `members refusals` (10). - The pass on `main` (`f4d568ba`): `--members` (six names) exits 0 and `--members --verify --base HEAD~1` at head exits 0: 6 moved, `checkMove` 6/6, `protocol.ts` 13,927 -> 13,743 lines, 9 import bindings pruned. Measured also with all eight names: exit 0, `checkMove` 8/8, the built declarations byte-equal too. - `@objectstack/metadata-protocol` at `c2e3fa7f`: vitest 317 files passed / 4 skipped, 22,454 tests passed / 26 skipped (exit 0); `typecheck` exit 0. - Facade gate negative control: the head registry over `main`'s `src/`: exit 1, six problems, one per moved door; at head exit 0 (50 named exceptions). ## Acceptance notes - Premises re-measured on `main` `f4d568ba`: the first pass still refuses its own output (exit 2), and `--verify --base 48914fd^` is now 77/85 bodies and 69/78 `checkMove` (the card read 79/85, 71/78; more hand edits since). - The second PR (same card thread, `Fixes #30518`) moves `getDiscovery` and `findData` with this pass. `findData` has a path reader, `protocol.query-transport-dialect.test.ts` (`the fold names the spec export at the call site` reads `protocol.ts`), which goes red when only the body moves (measured); that PR gives the reader the facet module, as ruling 6597555122 item 3 does. `scripts/check-filter-alias-parity.mjs` also reads `protocol.ts` and is re-run there. - `PATH_READER_NEEDLES` is empty in this codemod, so the first pass would not have kept `findData` for that reader either; not changed here (it is the first pass's table). - A lifted public member's return type keeps the method's own line breaks re-indented one level, as the first pass's lift writes it (the undo depends on it). - The `--self-test` of this codemod is not run by a CI workflow (true on `main` too); run locally above. ## Verification <details><summary>Declaration files</summary> `pnpm --filter @objectstack/metadata-protocol build` at base `f4d568ba` and at head: `dist/index.d.ts` and `dist/index.d.cts` `cmp` byte-equal (sha256 `726c856a6a748865…` on both sides). </details> <details><summary>dispatch-gates</summary> `node scripts/pm/dispatch-gates.mjs --commands` at `c2e3fa7f`: 78 families derived, each run with its exit code captured before any pipe. - 77/78 exit 0 (two after their prerequisite: `check-plugin-teardown-shape.mjs --self-test` exit 3 in the shallow clone, exit 0 after fetching its pinned commit; `check:lean-entry-closure` exit 3, exit 0 after `pnpm turbo run build --filter=@objectstack/objectql`). - NOT MEASURED: `pnpm check:dual-build-cjs-loads` (exit 3, needs a whole-tree build; CI runs it). - `dispatch-gates --ran`: 78 derived, 77 run, 1 NOT-MEASURED, 0 UNRUN. - `node scripts/check-facade-delegation-only.mjs` exit 0 (50 named exceptions); `eslint --no-inline-config` over the 9 changed source files exit 0; `node scripts/codemod-split-protocol.mjs --self-test` exit 0. </details> <details><summary>Changed lines</summary> 1,353 changed lines (+1,098 / -255) in 11 files, under 3,000: `scripts/codemod-split-protocol.mjs` +711/-1, `protocol-data-crud.ts` +220/-2, `protocol.ts` +19/-203, `protocol-history-and-publish.ts` +59/-1, `scripts/facade-delegation-registry.json` +4/-40, `protocol-save.ts` +28/-2, `protocol-diff-delete-load.ts` +19/-2, `protocol-package-commits.ts` +18/-2, the changeset +10, `protocol-host.ts` +7, the return-type pin's docblock +3/-2. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cweKLi1i8z12wPbsnw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7da99b7 commit d8ed2e1

11 files changed

Lines changed: 1098 additions & 255 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
refactor(metadata-protocol): six `ObjectStackProtocolImplementation` doors hand their bodies to their facets
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- `getData`, `cloneData`, `saveMetaItem`, `publishMetaItem`, `rollbackMetaItem` and `deleteMetaItem` keep their signature and docblock on the class and delegate in one line to `ProtocolDataCrudFacet`, `ProtocolSaveFacet`, `ProtocolHistoryAndPublishFacet`, `ProtocolPackageCommitsFacet` and `ProtocolDiffDeleteLoadFacet`, which now hold the bodies, moved unchanged. The built `index.d.ts` and `index.d.cts` are byte-equal before and after, and no behaviour changes. Nothing to do on upgrade.

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

Lines changed: 220 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
/**
44
* [#29985 codemod] The "Data CRUD" facet of
5-
* `ObjectStackProtocolImplementation`: the bodies of `probeRecord` moved out of
6-
* `protocol.ts` whole by `scripts/codemod-split-protocol.mjs`.
5+
* `ObjectStackProtocolImplementation`: the bodies of `getData`, `cloneData`,
6+
* `probeRecord` moved out of `protocol.ts` whole by
7+
* `scripts/codemod-split-protocol.mjs`.
78
*
89
* A MOVE, not a redesign. `ProtocolDataCrudFacet` is a stateless facet over the host
910
* (ruling 6624345766): each static method below is the body of the class
@@ -21,9 +22,226 @@
2122
* not move.
2223
*/
2324

25+
import { recordNotFoundError } from '@objectstack/core';
26+
import { staleSummaryRefusalOrThrow, withEarnedAnswer } from '@objectstack/types';
27+
import type { SummaryRecomputeRefusal } from '@objectstack/types';
28+
import { omitInternalFieldsFromWriteResponse } from './write-response-internal-fields.js';
29+
import type { DroppedFieldsEvent, EngineQueryOptions, ValidationAdvisoryEvent } from '@objectstack/spec/data';
30+
import {
31+
redactStoredMetadataRow,
32+
serveStoredMetadataHashColumns,
33+
storedMetadataBodyProjection,
34+
} from './metadata-redaction.js';
35+
import { CLONE_STRIP_FIELDS } from './protocol-write-constants.js';
36+
import { projectionFieldsValue } from './protocol-list-query-params.js';
2437
import type { ProtocolHost } from './protocol-host.js';
2538

2639
export class ProtocolDataCrudFacet {
40+
/** The body of `ObjectStackProtocolImplementation#getData`; the method's docblock there is its contract. */
41+
static async getData(host: Pick<ProtocolHost, 'assertExpandTargetsExist' | 'assertObjectRegistered' | 'assertProjectionFieldsExist' | 'engine' | 'servedExpand' | 'storedHashDigest'>, request: { object: string, id: string, expand?: string | string[], select?: string | string[], context?: any }): Promise<{
42+
object: string;
43+
id: string;
44+
record: Record<string, any>;
45+
}> {
46+
host.assertObjectRegistered(request.object); // [#3770]
47+
const queryOptions: any = {
48+
where: { id: request.id }
49+
};
50+
if (request.context !== undefined) {
51+
queryOptions.context = request.context;
52+
}
53+
54+
// Support fields for single-record retrieval
55+
//
56+
// [#4226] Gated exactly as on the list path. `GET /data/:object/:id` and
57+
// `GET /data/:object` read the same `select`/`expand` against the same
58+
// field map, so answering one with a 400 and the other with a silently
59+
// widened projection would recreate, one route over, the very split
60+
// ("two routes, opposite answers for one input") this issue was filed
61+
// about.
62+
//
63+
// [#26644] …including the empty projection: a blank `?select=` was
64+
// falsy and skipped, a comma-only one became `[]` — both answered every
65+
// column. Asked on any value present, as the list path asks it.
66+
if (request.select != null) {
67+
queryOptions.fields = typeof request.select === 'string' || Array.isArray(request.select)
68+
? projectionFieldsValue(request.select, 'select')
69+
: request.select;
70+
host.assertProjectionFieldsExist(request.object, queryOptions.fields, 'select');
71+
}
72+
// [#21086] Same read projection as the list path (`findData`): the row's
73+
// `type` is read even when `select` named only the body.
74+
const bodyProjection = storedMetadataBodyProjection(request.object, queryOptions.fields);
75+
if (bodyProjection.addedType) queryOptions.fields = bodyProjection.fields;
76+
77+
// Support expand for single-record retrieval
78+
if (request.expand) {
79+
const expandNames = typeof request.expand === 'string'
80+
? request.expand.split(',').map((s: string) => s.trim()).filter(Boolean)
81+
: request.expand;
82+
host.assertExpandTargetsExist(request.object, expandNames);
83+
const expand = {} as Record<string, any>;
84+
for (const rel of expandNames) {
85+
expand[rel] = { object: rel };
86+
}
87+
// [#22661] The same exposure judgement as the list path: an entry
88+
// whose target does not serve `get` is withheld ({@link servedExpand}).
89+
const served = host.servedExpand(request.object, expand, 0);
90+
if (Object.keys(served).length > 0) queryOptions.expand = served;
91+
}
92+
93+
// [#29764] The bag `engine.findOne` receives is typed as the engine
94+
// declares it. `queryOptions` stays the working bag: the projection
95+
// helpers above answer `unknown` (they serve the raw list query too), so
96+
// its `fields` has no declared type until it is handed on here. Keys are
97+
// copied only when present, so the engine sees the same bag as before.
98+
const findOneQuery: EngineQueryOptions = {
99+
where: queryOptions.where,
100+
...(queryOptions.context !== undefined ? { context: queryOptions.context } : {}),
101+
...(queryOptions.fields !== undefined ? { fields: queryOptions.fields } : {}),
102+
...(queryOptions.expand !== undefined ? { expand: queryOptions.expand } : {}),
103+
};
104+
const result = await host.engine.findOne(request.object, findOneQuery);
105+
if (result) {
106+
return {
107+
object: request.object,
108+
id: request.id,
109+
// [#21207] Same served form as the list path: the content-hash
110+
// columns keyed.
111+
record: await serveStoredMetadataHashColumns(
112+
request.object,
113+
redactStoredMetadataRow(request.object, result, { dropType: bodyProjection.addedType }),
114+
host.storedHashDigest(),
115+
),
116+
};
117+
}
118+
throw recordNotFoundError(request.object, request.id);
119+
}
120+
121+
/** The body of `ObjectStackProtocolImplementation#cloneData`; the method's docblock there is its contract. */
122+
static async cloneData(host: Pick<ProtocolHost, 'assertObjectRegistered' | 'engine'>, request: { object: string, id: string, overrides?: Record<string, any>, context?: any }): Promise<{
123+
warnings?: {
124+
rule: string;
125+
severity: "info" | "warning";
126+
field: string;
127+
code: string;
128+
message: string;
129+
}[] | undefined;
130+
droppedFields?: {
131+
object: string;
132+
fields: string[];
133+
reason: "readonly" | "computed" | "readonly_when" | "primary_key" | "stored_expression";
134+
}[] | undefined;
135+
object: string;
136+
id: any;
137+
sourceId: string;
138+
record: any;
139+
}> {
140+
// [#3770] This object-existence check used to be open-coded here and
141+
// was the ONLY one on the whole data plane; it is now the shared gate
142+
// every data entry point runs. Same error envelope as before.
143+
host.assertObjectRegistered(request.object);
144+
const schema: any = host.engine.registry?.getObject(request.object);
145+
// `enable.clone` defaults to true in the spec; treat an absent block /
146+
// absent flag as enabled and only block on an explicit `false`.
147+
if (schema?.enable?.clone === false) {
148+
const err: any = new Error(`Cloning is disabled for object '${request.object}'`);
149+
err.code = 'CLONE_DISABLED';
150+
err.status = 403;
151+
err.object = request.object;
152+
throw err;
153+
}
154+
155+
const ctx = request.context;
156+
const ctxOpt = ctx !== undefined ? { context: ctx } : undefined;
157+
158+
const source = await host.engine.findOne(
159+
request.object,
160+
{ where: { id: request.id }, ...ctxOpt },
161+
);
162+
if (!source) throw recordNotFoundError(request.object, request.id);
163+
164+
// Copy the source, then strip the columns the engine owns so the insert
165+
// path re-derives them rather than carrying the source's values over.
166+
const data: Record<string, any> = { ...source };
167+
for (const f of CLONE_STRIP_FIELDS) delete data[f];
168+
const fields: Record<string, any> = schema?.fields || {};
169+
for (const [name, def] of Object.entries(fields)) {
170+
if (!def) continue;
171+
// Engine-/automation-owned values: injected system/audit columns,
172+
// engine-generated autonumbers, and computed formula/summary fields.
173+
if ((def as any).system === true
174+
|| (def as any).type === 'autonumber'
175+
|| (def as any).type === 'formula'
176+
|| (def as any).type === 'summary') {
177+
delete data[name];
178+
continue;
179+
}
180+
// [#29200] A read-only column the source left EMPTY is not carried
181+
// into the insert. Carried, it reached `engine.insert`'s strip as a
182+
// present key and was reported in `droppedFields` (and the
183+
// `X-ObjectStack-Dropped-Fields` header) although neither the caller
184+
// nor the source supplied a value. Leaving it out stores the same
185+
// row: the strip would have taken it from a non-system caller, and
186+
// the insert's defaults treat an explicit `null` exactly as an
187+
// omitted key. A read-only column that HOLDS a value is still
188+
// carried, stripped and reported (#15703), and so is one the caller
189+
// sets through `overrides`, which are applied below.
190+
if ((def as any).readonly && data[name] == null) delete data[name];
191+
}
192+
// Caller overrides win (new name, cleared unique field, reset status…).
193+
if (request.overrides && typeof request.overrides === 'object') {
194+
Object.assign(data, request.overrides);
195+
}
196+
197+
// [#3043/#14147] A clone is a create: a non-system caller must not carry
198+
// over (or override in) a read-only column — copying the source's
199+
// `approval_status` or forging one via `overrides` would mint an approved
200+
// record. That strip is `engine.insert`'s since #14147, so the payload
201+
// goes over whole and the insert re-derives the field's `defaultValue`,
202+
// symmetric with createData. `overrides` are applied ABOVE this line, so
203+
// a readonly key smuggled through them is still judged by the strip.
204+
//
205+
// [#15703] And the verdict is REPORTED, the same listener `createData`
206+
// wires: a clone is the one create shape that carries a read-only column
207+
// without the caller typing it (the source's `approval_status` travels in
208+
// the copy), so the 201 body says which keys the engine dropped instead of
209+
// leaving the caller to diff `record` against the source. Maintainer
210+
// ruling 2026-09-08 (option 1); `CloneDataResponseSchema` declares the
211+
// member in the same change, because that schema is declared AS PRODUCED.
212+
const dropped: DroppedFieldsEvent[] = [];
213+
// [#22726] A clone is a create: its advisory rule hits are answered as
214+
// `warnings`, the same listener `createData` wires.
215+
const warnings: ValidationAdvisoryEvent[] = [];
216+
const opts: any = {
217+
onFieldsDropped: (e: DroppedFieldsEvent) => { dropped.push(e); },
218+
onValidationAdvisory: (e: ValidationAdvisoryEvent) => { warnings.push(e); },
219+
};
220+
if (ctx !== undefined) opts.context = ctx;
221+
// [#28782] A copy the engine stored but whose parent roll-up it could
222+
// not recompute carries this method's answer on the refusal, as
223+
// `createData` does since #28781 (`withEarnedAnswer`).
224+
let stale: SummaryRecomputeRefusal | undefined;
225+
const result = await host.engine.insert(request.object, data, opts)
226+
.catch((e: unknown) => (stale = staleSummaryRefusalOrThrow(e)).written as any);
227+
// [#7823] Same ingress strip as `createData` — a clone's 201 body is
228+
// the same generic-data-path surface. (The SOURCE row was read through
229+
// the engine's find path, which already omits internal fields, so the
230+
// copy never carried one in; this guards the INSERT RESULT, which the
231+
// engine returns whole by design.)
232+
omitInternalFieldsFromWriteResponse(schema, result);
233+
const answer = {
234+
object: request.object,
235+
id: result.id,
236+
sourceId: request.id,
237+
record: result,
238+
...(dropped.length > 0 ? { droppedFields: dropped } : {}),
239+
...(warnings.length > 0 ? { warnings } : {}),
240+
};
241+
if (stale) throw withEarnedAnswer(stale, answer);
242+
return answer;
243+
}
244+
27245
/**
28246
* [#4435] Does this row EXIST? A fact about the database — deliberately
29247
* NOT "may this caller see it".

‎packages/metadata-protocol/src/protocol-diff-delete-load.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
/**
44
* [#29985 codemod] The "Diff, deleteMetaItem, loadMetaFromDb, load reports,
55
* findReferencesToMeta" facet of `ObjectStackProtocolImplementation`: the
6-
* bodies of `diffMetaItem`, `deleteMetaItemUnaudited`, `loadMetaFromDb`,
7-
* `findReferencesToMeta` moved out of `protocol.ts` whole by
6+
* bodies of `diffMetaItem`, `deleteMetaItem`, `deleteMetaItemUnaudited`,
7+
* `loadMetaFromDb`, `findReferencesToMeta` moved out of `protocol.ts` whole by
88
* `scripts/codemod-split-protocol.mjs`.
99
*
1010
* A MOVE, not a redesign. `ProtocolDiffDeleteLoadFacet` is a stateless facet over the host
@@ -55,6 +55,7 @@ import { collectCompositeKeyHeadHits, collectReferenceHits } from './protocol-re
5555
import { diffShallow } from './protocol-write-face.js';
5656
import type { MutationProjectionOutcome } from './protocol-mutation-events.js';
5757
import type { DeleteMetaItemRequest } from './protocol-door-requests.js';
58+
import { withRefusalAudit } from './protocol-refusal-audit.js';
5859
import type { ProtocolHost } from './protocol-host.js';
5960

6061
export class ProtocolDiffDeleteLoadFacet {
@@ -370,6 +371,22 @@ export class ProtocolDiffDeleteLoadFacet {
370371
};
371372
}
372373

374+
/** The body of `ObjectStackProtocolImplementation#deleteMetaItem`; the method's docblock there is its contract. */
375+
static async deleteMetaItem(host: Pick<ProtocolHost, 'deleteMetaItemUnaudited' | 'recordMetadataAudit'>, request: DeleteMetaItemRequest): Promise<{
376+
success: boolean;
377+
message?: string;
378+
reset?: boolean;
379+
seq?: number;
380+
/** [ADR-0094] Outcome of the awaited mutation projector, when one is registered. */
381+
projectionApplied?: MutationProjectionOutcome;
382+
}> {
383+
// [#28355] A refusal that carries no row of its own leaves its `denied` row here.
384+
return withRefusalAudit((entry) => host.recordMetadataAudit(entry), {
385+
operation: 'delete', type: request.type, name: request.name, actor: request.actor,
386+
source: 'protocol.deleteMetaItem',
387+
}, () => host.deleteMetaItemUnaudited(request));
388+
}
389+
373390
/** [#28355] The body of {@link deleteMetaItem} before its refusal audit; that method's docblock is the contract. */
374391
static async deleteMetaItemUnaudited(host: Pick<ProtocolHost, 'assertLockAllowsDelete' | 'dropObjectStorage' | 'dropStorageRemainingRowsRefusal' | 'emitMetadataMutation' | 'engine' | 'environmentId' | 'findEnvironmentRow' | 'getOverlayRepo' | 'isArtifactBacked' | 'isOverlayAllowed' | 'isRuntimeCreateAllowed' | 'metadataConflictRefusal' | 'originGatedRemovalRefusal' | 'packagedArtifactOwner' | 'packagedBaseRefusal' | 'packagelessAddress' | 'packagelessDraftAddress' | 'packagelessOverlayWriteRefusal' | 'recordMetadataAudit' | 'recordOptimisticConflictAudit' | 'refuseOrganizationScopedWrite' | 'refusePackagedBaseRemoval' | 'registryAllowsOverlay' | 'restoreArtifactRegistryView' | 'runMutationProjector' | 'shouldDropStorage' | 'storedParentForToken'>, request: DeleteMetaItemRequest): Promise<{
375392
success: boolean;

0 commit comments

Comments
 (0)