Skip to content

Commit 82142ed

Browse files
fix(metadata-protocol,rest,runtime): serve the pending draft as the /layers draft layer, untranslated and ungoverned (#30713)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D6zjFMV3ohoQ82rQMxswmcE)_ domain:server#1 · os-elon-musk · session_016WV4cxJ8w29kmRj5j2Ex2D Fixes #29814 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`GET /meta/:type/:name/layers`(以及已弃用的 `?layers=`)现在按 spec 已声明的 `draft` 成员返回待发布草稿,REST 与 runtime 两个入口一致:原样存储的草稿,不翻译、不注入平台列,没有草稿时为 `null`。 - 为什么改:Studio / Setup 编辑对象时以 `?state=draft` 的结果作为编辑缓冲,那份结果已被翻译并注入了 `owner_id` 等平台列,存回去就把译文和平台列写进了作者的草稿;有了 `draft` 层,#29793 才能把编辑缓冲改为 `draft ?? overlay ?? code`。 - 风险与回滚:只对既有草稿读准入(`mayReadPendingDrafts`)放行的调用方返回 `draft`,其余调用方的响应里没有这个键,什么都没放宽;单一提交可整体回滚。唯一可见的行为变化:只有草稿、从未发布的条目,对已准入的作者由 404 变为 200(只带 `draft` 层)。 - 先看:`packages/rest/src/meta-item-read-gate.ts`(共用分层链把 `draft` 纳入逐调用方门控与掩码)、`packages/metadata-protocol/src/protocol-meta-layered-reads.ts`(草稿层的读取地址与原样性)、`packages/rest/src/rest-server.ts`(REST 入口的准入)。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-layers-draft-layer.test.ts`,预期 16/16 通过(两个入口 × 两种拼写)。 - 需要你做的:无,席位落地。 ## What changes - **Protocol.** `getMetaItemLayered(request, { withDraft: true })` adds the `draft` layer: the row the strict `state: 'draft'` read serves, at that read's address (the named package's draft; naming none, the draft the strict draft address names, with that read's best-effort first match), read as the `overlay` layer is read (the stored body through the conversion chain), credential-redacted like every layer, and never governed, translated or stamped. `null` when no draft is pending. Without the option the answer carries no `draft` key, so the method's in-process readers (security catalog echo, packaged permission-set lock gate, `/published`) read no draft. - **Both transports** (`RestServer.serveMetaItemLayered`, the runtime dispatcher's `answerMetaLayered`) ask for the layer only for a caller the existing draft-read admission admits (`mayReadPendingDrafts`), declared in its own `const` as every draft switch is. This reuses that predicate unchanged and relaxes nothing: a caller it does not admit is answered the layers with no `draft` key, which says nothing about whether a draft exists, and a name with only a draft stays that caller's 404. - **The shared layered chain** (`createMetaLayeredAnswer`) treats `draft` as a layer for every step: presence, the per-caller gate under the stored-version policy (whole for whoever may write the item, pruned as `?state=draft` prunes it otherwise), and the ADR-0106 mask. The mask never writes an absent `draft` key in. - **Draft-read censuses** (`meta-draft-read-door-census.test.ts`, the runtime twin in `meta-draft-read-builder-gate.test.ts`) now see the `withDraft` option and ledger it as a `read` switch, so a later copy of it without the admission is red. The spec member was declared by #29855 (PR #29863); `packages/spec` is untouched. ## Tests `packages/runtime/src/domains/meta-layers-draft-layer.test.ts`: a real store per door (better-sqlite3, the real `sys_metadata*` objects, a real protocol), `RestServer`'s handlers and the real `HttpDispatcher`, an i18n service with a zh-CN catalog; each case on both spellings. - a zh-CN read serves the draft byte for byte as stored (compared with the row read back from `sys_metadata`): source labels, no injected column; - `?state=draft` is unchanged: the same draft translated and governed, and none of its injected columns is on the draft layer; - `draft: null` for an item with no pending draft; `?package=` picks that package's draft; - a never-published name is served to an author as its draft layer; - a stored cleartext credential in a draft is redacted, the rest served as stored; - the per-caller gate: an app draft is whole for its author and pruned for a `studio.access` reader, exactly as `?state=draft` answers each; - a caller not admitted gets no `draft` key on either door, and a draft-only name stays 404. <details><summary>Ablations (each restored, blob equal to HEAD, `git diff HEAD` empty)</summary> - REST admission forced to `true`: the not-admitted case reds on the `RestServer` door only (1 failed / 15 passed). Re-run on the final spelling of that line: same result. - runtime admission forced to `true`: the not-admitted case reds on the catch-all door only (1 failed / 15 passed). - `draft` taken off the chain's gated layer list: 8 red on both doors (the draft-only presence, `?package=`, redaction and per-caller gate cases). - REST admission forced to `true` against the REST census: "every read switch carries its admission in its own declaration" reds. </details> ## Acceptance notes - The draft layer is converted through the stored-item chain, as the `overlay` layer is. For a current-spec body this is the identity (the byte-for-byte pin holds through it); a row carrying a retired spelling is served converted, which is what its publish would store. - Redaction applies to `draft` for the same reason it applies to `code` and `overlay`: it subtracts a credential and nothing else. ## Verification Local runs, final readings to be quoted with the head they ran on in the report comment on #29814: `typecheck` for `@objectstack/metadata-protocol`, `@objectstack/rest`, `@objectstack/runtime`; their `pnpm test`; every family `node scripts/pm/dispatch-gates.mjs --commands` derives from this diff, reconciled with `--ran`; `pnpm lint`. CI's required checks are the authority for everything else. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cxJ8w29kmRj5j2Ex2D)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 447226b commit 82142ed

10 files changed

Lines changed: 554 additions & 16 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/rest': patch
4+
'@objectstack/runtime': patch
5+
---
6+
7+
fix(metadata-protocol,rest,runtime): `GET /meta/:type/:name/layers` serves the pending draft as stored, so an editor re-opens an author's draft without translations or injected columns (#29814)
8+
9+
Clause-②: no
10+
Metadata-protocol: none
11+
12+
The layered view now serves the `draft` layer that `GetMetaItemLayeredResponseSchema` declares, on `RestServer` and on the runtime dispatcher, and on the deprecated `?layers=` spelling too. Before, the only read of a pending draft was `?state=draft`, which serves it translated for the caller's locale and governed (the platform's injected columns added), so an editor that built its buffer from it and saved it back wrote translations and platform columns into the author's draft.
13+
14+
- **What `draft` is.** The row the `?state=draft` read serves, at the same address (`?package=` picks that package's draft), exactly as stored: converted like the `overlay` layer and credential-redacted like every layer, never translated, governed or stamped. `null` when no draft is pending. An edit buffer reads `draft ?? overlay ?? code`.
15+
- **Who is served it.** Only a caller the draft-read admission already admits (the predicate every draft read asks: a system caller, `studio.access`, `setup.access` or `manage_metadata`). The per-caller gate judges it like the other layers: whole for whoever may write the item, pruned as `?state=draft` prunes it otherwise. Any other caller is answered the layers with no `draft` key, which says nothing about whether a draft exists, and a name with only a draft stays a `404` for them.
16+
- **Unchanged.** `?state=draft` and every other read answer as before. A never-published name an admitted author holds a draft of is now answered `200` with only its `draft` layer, where it was a `404`.
17+
- **Protocol.** `getMetaItemLayered(request, { withDraft: true })` reads the layer; without the option the answer carries no `draft` key, so its in-process readers read no draft.

‎packages/metadata-protocol/src/protocol-meta-layered-reads.ts‎

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,25 @@ export async function packagelessLayeredReadRefusal(host: Pick<ProtocolHost, 'pa
7676
}
7777

7878
/** The body of `ObjectStackProtocolImplementation#getMetaItemLayered`; the method's docblock there is its contract. */
79-
export async function getMetaItemLayered(host: Pick<ProtocolHost, 'artifactLockLayerAt' | 'convertStoredItem' | 'declinesStoredRow' | 'engine' | 'findPairAddedMemberRow' | 'findServedOverlayRow' | 'foldObjectExtendersFromRegistry' | 'governServedObject' | 'isArtifactBacked' | 'lookupArtifactItem' | 'overlayLockLayerAt' | 'packagelessReadAddress' | 'readItemActiveRows' | 'readItemFromMetadataService' | 'resolveRowlessExpandedView' | 'resolveWritePackageScope' | 'rethrowUnlessMetadataStoreUnprovisioned' | 'servedLockState' | 'throwMetadataServiceUnavailable'>, request: {
79+
export async function getMetaItemLayered(host: Pick<ProtocolHost, 'artifactLockLayerAt' | 'convertStoredItem' | 'declinesStoredRow' | 'engine' | 'findPairAddedMemberRow' | 'findServedOverlayRow' | 'foldObjectExtendersFromRegistry' | 'governServedObject' | 'isArtifactBacked' | 'lookupArtifactItem' | 'overlayLockLayerAt' | 'packagelessReadAddress' | 'readItemActiveRows' | 'readItemFromMetadataService' | 'resolveRowlessExpandedView' | 'resolveWritePackageScope' | 'rethrowUnlessMetadataStoreUnprovisioned' | 'servedLockState' | 'strictDraftReadAddress' | 'throwMetadataServiceUnavailable'>, request: {
8080
type: string;
8181
name: string;
8282
packageId?: string;
8383
/** [#26699] The bundle member whose layers to read — {@link resolveBundleMemberSelector}; omitted, the canonical one. */
8484
member?: string;
85-
}): Promise<{
85+
}, opts: { withDraft?: boolean } = {}): Promise<{
8686
type: string;
8787
name: string;
8888
code: unknown | null;
8989
overlay: unknown | null;
9090
overlayScope: 'env' | null;
9191
effective: unknown | null;
92+
/**
93+
* [#29814] The pending draft row as stored — present only when the
94+
* caller asked for it (`opts.withDraft`); `null` when no draft is
95+
* pending. See the class method's docblock.
96+
*/
97+
draft?: unknown | null;
9298
/**
9399
* Load-time validation result for the effective payload — same
94100
* shape attached to getMetaItems/getMetaItem by
@@ -418,6 +424,60 @@ export async function getMetaItemLayered(host: Pick<ProtocolHost, 'artifactLockL
418424
}
419425
}
420426

427+
// ── [#29814] The pending DRAFT layer, only when the caller asked ──
428+
//
429+
// The row the strict `state: 'draft'` read of {@link getMetaItem} serves,
430+
// at the same address: the requested package's draft when one is named,
431+
// else the draft {@link strictDraftReadAddress} names (the address the
432+
// draft save, the per-item publish and the discard naming none act on),
433+
// with that read's best-effort first match when two or more packages hold
434+
// drafts and none is package-unbound. Read as the `overlay` layer is read:
435+
// the stored body through the conversion chain and nothing else. ⛔ Not
436+
// governed (no injected columns), not translated, no `_packageId` stamp:
437+
// an edit buffer built from it and saved back writes what the author
438+
// wrote. The strict read decorates and governs; this layer is the draft
439+
// as stored, which is the whole reason it exists.
440+
//
441+
// Asked for by the DOOR, never by default: a door asks only for a caller
442+
// its draft-read admission admits (`mayReadPendingDrafts`), so no
443+
// in-process resolution reader of this method (the security catalog echo,
444+
// the packaged-permission-set lock gate, `/published`) reads drafts.
445+
let draft: unknown | null | undefined;
446+
if (opts.withDraft === true) {
447+
draft = null;
448+
const draftAddress = request.packageId
449+
? undefined
450+
: await host.strictDraftReadAddress(request.type, request.name, member);
451+
try {
452+
let draftRow = (await host.findServedOverlayRow({
453+
type: request.type,
454+
name: request.name,
455+
state: 'draft',
456+
...(request.packageId
457+
? { packageId: request.packageId }
458+
: draftAddress ? { packageId: draftAddress.packageId } : {}),
459+
...memberOption(member),
460+
}))?.row;
461+
if (!draftRow && draftAddress?.bestEffort) {
462+
draftRow = (await host.findServedOverlayRow({
463+
type: request.type,
464+
name: request.name,
465+
state: 'draft',
466+
...memberOption(member),
467+
}))?.row;
468+
}
469+
if (draftRow) {
470+
draft = host.convertStoredItem(
471+
String(draftRow.type ?? request.type),
472+
storedRowDocument(draftRow),
473+
);
474+
}
475+
} catch (error) {
476+
// [#5707] A read that did not happen is not "no draft pending".
477+
host.rethrowUnlessMetadataStoreUnprovisioned(error, 'sys_metadata');
478+
}
479+
}
480+
421481
// [#4513] `effective` is documented above as "what `getMetaItem` would
422482
// return", and the response's `_diagnostics` is computed from it — so it
423483
// carries the same audit-family governance that read now applies, or the
@@ -565,6 +625,9 @@ export async function getMetaItemLayered(host: Pick<ProtocolHost, 'artifactLockL
565625
const servedCode = redactMetadataItem(request.type, code);
566626
const servedOverlay = redactMetadataItem(request.type, overlay);
567627
const servedEffective = redactMetadataItem(request.type, effective);
628+
// [#29814] …and the draft layer, by the same reasoning: redaction
629+
// subtracts a credential and nothing else.
630+
const servedDraft = draft === undefined ? undefined : redactMetadataItem(request.type, draft);
568631

569632
return {
570633
type: request.type,
@@ -573,6 +636,7 @@ export async function getMetaItemLayered(host: Pick<ProtocolHost, 'artifactLockL
573636
overlay: servedOverlay,
574637
overlayScope,
575638
effective: servedEffective,
639+
...(servedDraft !== undefined ? { draft: servedDraft } : {}),
576640
...(_diagnostics ? { _diagnostics } : {}),
577641
lock: lockState.lock,
578642
...(lockState.lockReason !== undefined ? { lockReason: lockState.lockReason } : {}),

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4613,20 +4613,34 @@ export class ObjectStackProtocolImplementation implements
46134613
* layer's MetadataService read reporting `degraded` with nothing
46144614
* in the registry to answer instead, carrying the failing loaders'
46154615
* messages on `cause`.
4616+
*
4617+
* [#29814] `opts.withDraft` adds the `draft` layer
4618+
* (`GetMetaItemLayeredResponseSchema.draft`): the pending draft row the
4619+
* strict `state: 'draft'` read of {@link getMetaItem} serves, at that
4620+
* read's address, AS STORED — converted like the `overlay` layer and
4621+
* credential-redacted like every layer, never governed, translated or
4622+
* stamped — or `null` when no draft is pending. Without the option the
4623+
* answer carries no `draft` key ("not served"). Only a door that has
4624+
* admitted its caller to pending drafts (`mayReadPendingDrafts` in
4625+
* `@objectstack/rest`, and its runtime twin) asks for it; every other
4626+
* reader of this method reads no draft. A failed draft read throws, as
4627+
* the overlay read does, for the same [#5707] reason.
46164628
*/
46174629
async getMetaItemLayered(request: {
46184630
type: string;
46194631
name: string;
46204632
packageId?: string;
46214633
/** [#26699] The bundle member whose layers to read — {@link resolveBundleMemberSelector}; omitted, the canonical one. */
46224634
member?: string;
4623-
}): Promise<{
4635+
}, opts: { withDraft?: boolean } = {}): Promise<{
46244636
type: string;
46254637
name: string;
46264638
code: unknown | null;
46274639
overlay: unknown | null;
46284640
overlayScope: 'env' | null;
46294641
effective: unknown | null;
4642+
/** [#29814] The pending draft row as stored, only when `opts.withDraft` asked; `null` for none. */
4643+
draft?: unknown | null;
46304644
/**
46314645
* Load-time validation result for the effective payload — same
46324646
* shape attached to getMetaItems/getMetaItem by
@@ -4657,7 +4671,7 @@ export class ObjectStackProtocolImplementation implements
46574671
resettable: boolean;
46584672
}> {
46594673
// [#25467 E1] The body moved to `protocol-meta-layered-reads.ts`; it reaches this class through `protocol-host.ts`.
4660-
return getMetaItemLayered(protocolHost(this), request);
4674+
return getMetaItemLayered(protocolHost(this), request, opts);
46614675
}
46624676

46634677
/**

‎packages/rest/src/meta-draft-read-door-census.test.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
*
1919
* It walks the file's AST for every draft switch: a comparison against the
2020
* literal `'draft'`, a `.previewDrafts` read, and a `.listDrafts` reference.
21-
* Each is keyed `<route> » <text>` (the registration's `method` and `path`,
21+
* [#29814] It also counts the layered read's `withDraft` option, keyed by its
22+
* name. Each is keyed `<route> » <text>` (the registration's `method` and `path`,
2223
* else the named function around it — never a line number, this file is edited
2324
* several times a day) and must be LEDGERED below as one of:
2425
*
@@ -98,7 +99,9 @@ const isDraftSite = (n: ts.Node): boolean =>
9899
(ts.isBinaryExpression(n)
99100
&& (n.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken || n.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken)
100101
&& [n.left, n.right].some((side) => ts.isStringLiteral(side) && side.text === 'draft'))
101-
|| (ts.isPropertyAccessExpression(n) && ['previewDrafts', 'listDrafts'].includes(n.name.text));
102+
|| (ts.isPropertyAccessExpression(n) && ['previewDrafts', 'listDrafts'].includes(n.name.text))
103+
// [#29814] The layered read's draft option, `{ withDraft: … }`, keyed by its name.
104+
|| (ts.isIdentifier(n) && n.text === 'withDraft' && ts.isPropertyAssignment(n.parent) && n.parent.name === n);
102105

103106
const isAdmission = (n: ts.Node): boolean =>
104107
ts.isCallExpression(n) && n.expression.getText() === 'mayReadPendingDrafts';
@@ -186,6 +189,11 @@ const LEDGER: readonly LedgerRow[] = [
186189
disposition: 'write',
187190
door: 'DELETE /meta/:type/:name?state=draft — DISCARDS a draft; admitted by the write-capability gate first',
188191
},
192+
{
193+
site: 'method serveMetaItemLayered » withDraft',
194+
disposition: 'read',
195+
door: 'GET /meta/:type/:name/layers (and the deprecated ?layers=) — the pending draft layer',
196+
},
189197
{
190198
site: 'POST ${basePath}/analytics/dataset/query » body.previewDrafts',
191199
disposition: 'read',

‎packages/rest/src/meta-item-layered-route.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ describe('#5882 GET /meta/:type/:name/layers — the declared layered resource',
179179
await dispatch(protocol, LAYERS_PATH, { type: 'object', name: 'customer' }, { package: 'com.acme.crm' });
180180
expect(protocol.getMetaItemLayered).toHaveBeenCalledWith(
181181
expect.objectContaining({ packageId: 'com.acme.crm' }),
182+
// [#29814] The draft layer's option: this caller is not admitted to pending drafts.
183+
{ withDraft: false },
182184
);
183185
});
184186

‎packages/rest/src/meta-item-read-gate.ts‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4147,11 +4147,20 @@ export function metaItemLayersDeprecationHeaders(
41474147
: { Deprecation: 'true', Link: `<${itemPath}/layers>; rel="successor-version"` };
41484148
}
41494149

4150-
/** The layers a layered answer carries, in the order the gate judges them: `effective` first — it is what the plain read serves, so its refusal is the plain read's own. */
4151-
const META_ITEM_LAYERS = ['effective', 'code', 'overlay'] as const;
4150+
/**
4151+
* The layers a layered answer carries, in the order the gate judges them: `effective` first — it is what the plain read serves, so its refusal is the plain read's own.
4152+
* [#29814] `draft` last: the pending draft row as stored, present only when the transport admitted its caller to pending drafts and asked for it.
4153+
*/
4154+
const META_ITEM_LAYERS = ['effective', 'code', 'overlay', 'draft'] as const;
41524155

41534156
/** The layers in the order the ADR-0106 mask projects them. */
4154-
const META_ITEM_MASKED_LAYERS = ['code', 'overlay', 'effective'] as const;
4157+
const META_ITEM_MASKED_LAYERS = ['code', 'overlay', 'effective', 'draft'] as const;
4158+
4159+
/**
4160+
* [#29814] The layers an answer may leave out: absent means "not served", so the
4161+
* mask never writes the key in. The other three are always on the answer.
4162+
*/
4163+
const META_ITEM_OPTIONAL_LAYERS: ReadonlySet<string> = new Set(['draft']);
41554164

41564165
/** The request facts the layered chain reads — no transport shape. */
41574166
export interface MetaLayeredRequest {
@@ -4279,6 +4288,16 @@ export type MetaLayeredAnswer =
42794288
* through {@link projectMetaObjectSchema} under the posture resolved before
42804289
* the read, and the `private, no-store` an undetermined posture owes.
42814290
*
4291+
* [#29814] `draft` — the pending draft row as stored
4292+
* (`GetMetaItemLayeredResponseSchema.draft`) — is a layer like the other three
4293+
* for every step above: it counts for step 1's presence (an admitted author's
4294+
* never-published item is its draft), the gate judges it under the same
4295+
* stored-version policy, and the mask projects it. It reaches this chain only
4296+
* when the transport asked the protocol for it, which each transport does only
4297+
* for a caller its draft-read admission (`mayReadPendingDrafts`) admits; for
4298+
* any other caller the answer carries no `draft` key, so it says nothing about
4299+
* whether a draft exists. The mask never writes an absent `draft` key in.
4300+
*
42824301
* The protocol's answer is never mutated. A layer the gate or the mask leaves as
42834302
* it was is served as it was, and every other key of the answer
42844303
* (`overlayScope`, `_diagnostics`, the ADR-0010 protection envelope) rides
@@ -4330,10 +4349,13 @@ export function createMetaLayeredAnswer(
43304349
// 3. [ADR-0106 D5(4)] The mask, on every layer — under one posture
43314350
// related [#21884] over every layer, so each object an `objectOverride`
43324351
// param names is asked about once.
4333-
const layerDocuments = META_ITEM_MASKED_LAYERS.map((layer) => (served.has(layer) ? served.get(layer) : layered?.[layer]));
4352+
const maskedLayers = META_ITEM_MASKED_LAYERS.filter(
4353+
(layer) => !META_ITEM_OPTIONAL_LAYERS.has(layer) || (layered != null && layer in layered),
4354+
);
4355+
const layerDocuments = maskedLayers.map((layer) => (served.has(layer) ? served.get(layer) : layered?.[layer]));
43344356
const layerPosture = await relateObjectSchemaMaskPosture(maskPosture, ...layerDocuments);
43354357
let cacheControl: typeof META_UNDETERMINED_CACHE_CONTROL | undefined;
4336-
for (const layer of META_ITEM_MASKED_LAYERS) {
4358+
for (const layer of maskedLayers) {
43374359
const document = served.has(layer) ? served.get(layer) : layered?.[layer];
43384360
const masked = projectMetaObjectSchema(layerPosture, document);
43394361
if (!masked.ok) return { kind: 'mask-fault', object: name };

‎packages/rest/src/rest-server.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4352,7 +4352,15 @@ export class RestServer {
43524352
...metaReadGate.metaMemberSelector(req.query),
43534353
...(environmentId ? { environmentId } : {}),
43544354
};
4355-
const layered = await p.getMetaItemLayered(layeredRequest);
4355+
// [#29814] The pending draft layer, asked for only when this caller may
4356+
// read pending drafts — the admission every draft switch in this file
4357+
// carries in its own declaration ({@link mayReadPendingDrafts}). A
4358+
// caller it does not admit is answered the layers without a `draft`
4359+
// key, byte-identical to a read that never asked for one, so the answer
4360+
// says nothing about whether a draft exists.
4361+
const layeredCaller = await this.resolveExecCtx(environmentId, req).catch(rethrowAuthzStoreUnavailable);
4362+
const layeredOptions = { withDraft: mayReadPendingDrafts(layeredCaller) };
4363+
const layered = await p.getMetaItemLayered(layeredRequest, layeredOptions);
43564364
// [#20156 · #20478] THE per-caller gate on every layer, then the mask —
43574365
// the shared chain. The stored-version doors honour the author
43584366
// exemption, so the caller carries this transport's save-door

0 commit comments

Comments
 (0)