Skip to content

Commit 5782008

Browse files
fix(metadata-protocol): a draft's _diagnostics reports what its publish will refuse (#26856)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XUBqAQc8raXt7Kqi3d7pnyt)_ Fixes #26707 Clause-②: no (the draft verdict reports more; no accepted value is refused; the dev re-measures) Metadata-protocol: none ## 维护者速读 - 改了什么:草稿读(`?state=draft` 与 previewDrafts)的 `_diagnostics` 现在跑发布时那一套运行时规则,发布会拒的草稿读出来就是 `valid:false`,带同一个规则 id 和同一句话;Studio 编辑页和画布 Problems 有草稿时显示草稿的服务端裁决。 - 为什么改:之前编辑器、`_diagnostics`、Problems 都说「有效」,到点发布才 422,而且一个这样的草稿会让整包发布全部回滚。 - 风险与回滚:草稿照常保存,发布拒什么不变,整包原子回滚不变;草稿读每次多付一次发布规则的耗时(showcase 实测 p50 约 16ms → 145ms,只在有待发布草稿的读上)。回滚即 revert 本 PR。 - 先看:`packages/metadata-protocol/src/protocol-authoring-gate.ts`(judge/assert 拆分与 `draftPublishVerdict`)、`packages/metadata-protocol/src/protocol.ts` 的 `getMetaItem` 中 `serveDraft`、`ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx` 的 `serverDiagnostics`。 - 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.draft-publish-verdict.test.ts`,预期 12 条全绿(每条规则:草稿读 `valid:false` 且与发布 422 的规则 id、消息一一相同)。 - 需要你做的:无,席位落地。 ## What changes **Server (`@objectstack/metadata-protocol`).** The publish's runtime authoring gate is split into `judgeRuntimeAuthoringRules` (returns the verdict) and the existing `assertRuntimeAuthoringRules` (throws it), so there is still one rule set and one gathering of its host facts. A new `draftPublishVerdict` runs that judge on a stored draft exactly as `promoteDraftForPublish` does: the raw stored draft body, `state: 'active'`, the row's package scope, the package's pending drafts as the batch closure (so a reference to a sibling draft that the same Publish-all lands is not an error), and the credential paths the publish would restore. `getMetaItem` serves its issues in the draft's `_diagnostics` (`withPublishVerdict`, metadata-diagnostics.ts): `valid: false`, and one error per issue with `code` = the gate's rule id and `message` = the publish's own message. Paths inside the item are served item-relative (`objects.invoice.fields.total.summaryOperations` becomes `fields.total.summaryOperations`, `flows[0].nodes[0].config.secret` becomes `nodes.0.config.secret`); a where-style path (a flow edge's condition) is served as the gate wrote it. The arms that do this are the strict draft read (`?state=draft`) and the `previewDrafts` read of an item with a pending draft. ⛔ No draft save is refused, what publish refuses is unchanged, and package publish keeps its atomic rollback. **Studio (`@object-ui/app-shell`).** The edit page read `_diagnostics` from the layered read alone, which judges the published body. It now keeps the draft read's `_diagnostics` (`draftVerdict.ts`) and shows it whenever a draft is pending: on load, after each autosave, after publish and after discard. The canvas Problems panel and the diagnostics banner get it. Where a live client parse exists, the server errors that parse did not raise are shown beside it, each once. No verdict is computed on the client. ## Tests - `protocol.draft-publish-verdict.test.ts` (new, 12 tests): for each of the six rules the card names (`expression-invalid` on a flow edge, `flow-api-trigger-secret-missing`, `rls-predicate-unenforceable`, `field/summary-without-operations`, `dashboard-filter-field-unknown`, `dataset-include-unknown`) the draft save answers 200, the draft read answers `valid: false`, and the `(code, message)` pairs equal the publish refusal's `(rule, message)` pairs; package publish refuses and the first failed draft reads `valid: false`; a clean draft reads `{ valid: true }`; an in-package forward reference to a pending dataset reads valid and publishes; a dangling one reads `widget-dataset-unknown`; the preview read equals the strict read; item-relative paths are pinned. Red on base (three source files at `8b276385e6`): 9 failed, 3 passed (the clean, closure and preview controls). - `protocol.item-execution-read.test.ts`: the served/execution parity for the strict draft and preview arms now includes the draft's publish verdict (the fixture draft declares no `sharingModel`, which its publish refuses as `security-owd-unset`). - `ResourceEditPage.draftPublishVerdict-26707.test.tsx` (new, 3 tests): the canvas receives the draft read's refusal while the layered verdict says valid; an autosave of a fixed draft clears it; a control with no draft. Red on base `ResourceEditPage.tsx`: 2 failed, 1 passed (the control). `draftVerdict.test.ts` (new, 3 tests). ## Acceptance notes - **Rules the draft verdict does not carry.** Refusals made by the save door itself (they refuse the save, so no stored draft carries them), the plugin authoring gates (`runAuthoringGate`) and the package build probes that `publish-drafts` runs. The list reads (`getMetaItems` with `previewDrafts`) and the `getMetaDiagnostics` sweep keep their read-time verdicts. - **Cost.** Measured on the showcase dev server (111 objects), 20 timed reads of one drafted flow: draft read p50 15.8 ms on base, 144.7 ms on this branch. Instrumented: gathering the collections costs about 5 ms and the gate's own evaluation about 145 ms, the same evaluation the publish of that item runs. It is paid only by a read that serves a pending draft. I did not store the verdict at save time: it depends on other items (objects, datasets, the package's other drafts), so a stored verdict could disagree with the publish, which the order rules out. - **Editors that print their own verdict (not in this PR).** `CelPredicateField` still says "Valid CEL" from its client lint for an RLS predicate over a formula field and for a flow edge condition, while the banner and Problems now show the server refusal. Making those fields read the server verdict for their own path needs the verdict plumbed to the field editors, and the flow edge refusal has a where-style path, not a key path. Handed back as a decision with a proposed child card. - **Dashboard global filter.** The Studio "Add item" for a global filter emits `{}`, which fails the spec schema even as a draft, so it never reaches the runtime gate. The pinned dashboard case is `dashboard-filter-field-unknown`. - **Overlap.** Open PRs touching `protocol.ts` (#26811, #26807) merge cleanly with this branch (`git merge-tree`), and their hunks are not adjacent. #26799 touches `packages/lint/src/authoring-rules.ts`, which this PR does not change. #26708's branch merges cleanly too. ## Verification <details><summary>Live REST red/green on the showcase dev server</summary> Branch (`pnpm dev -- --fresh`, writable package `app.probe26707`): ``` [green] PUT flow/edge_cel 200 [green] PUT flow/inbound_hook 200 GET flow/edge_cel?state=draft {"valid":false,"errors":[{"path":"flow 'edge_cel' · edge 'e2' (decide→end) condition","message":"invalid CEL predicate: Unexpected token: EOF …","code":"expression-invalid"}]} GET flow/inbound_hook?state=draft {"valid":false,"errors":[{"path":"nodes.0.config.secret","message":"binds the inbound api trigger … so the engine refuses to register the flow","code":"flow-api-trigger-secret-missing"}]} [green] item publish 422 (INVALID_METADATA, expression-invalid, same message) [green] publish-drafts: both failed, BATCH_ABORTED ``` Base (`metadata-protocol` at `8b276385e6`, rebuilt): ``` [red] PUT flow/edge_cel 200 [red] PUT flow/inbound_hook 200 GET flow/edge_cel?state=draft {"valid":true} GET flow/inbound_hook?state=draft {"valid":true} [red] item publish 422 (INVALID_METADATA, expression-invalid) ``` </details> <details><summary>Gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the final diff: 74 commands, each run with its exit code captured before any pipe; `--ran` reconciliation: 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN. The first pass had two reds, both fixed in d996462: `check:objectql-double-limit` (the new harness's `find` double ignored `limit`; it now applies it after the filter) and `check:engine-double-contract` (the new test's pinned doubles registered with `--write`). Scoped lint: root eslint on the five metadata-protocol files, 0 problems; app-shell eslint on the four touched files, 0 errors (51 warnings, all in `ResourceEditPage.tsx`, which has 54 on base). `@objectstack/metadata-protocol` full suite: 272 of 273 files passed on the first run; the one red file, `protocol.item-execution-read.test.ts` (two parity rows on the draft arms), was updated in 6f54ceb and passes. `@object-ui/app-shell` `metadata-admin/`: 611 files, 6646 tests pass; `type-check` exit 0. Downstream draft-read suites (`objectql` protocol-meta, `rest` rest.test, `runtime` seed read decorations) pass. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FEkQ5oVR19x7NPLf4YzCzj --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 94e29cd commit 5782008

12 files changed

Lines changed: 1070 additions & 51 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): a draft's `_diagnostics` reports what its publish will refuse (#26707)
6+
7+
Clause-②: no (the draft verdict reports more; no accepted value is refused)
8+
Metadata-protocol: none
9+
10+
A draft the runtime authoring gate refuses at publish (a flow edge whose condition does not parse, an API-triggered flow with no secret, an RLS predicate over a field the database cannot filter, a summary field with no operations, a dashboard global filter on an unknown field, a dataset `include` that names an unknown relationship) used to read `_diagnostics.valid: true` on the draft read, so the editors and the Problems tab called it valid and the refusal arrived only at Publish, where one such draft rolls back the whole package release.
11+
12+
- **One rule set.** The draft read (`GET /api/v1/meta/:type/:name?state=draft`, and the `previewDrafts` read of a pending draft) now runs the same runtime authoring gate the publish runs, on the stored draft body, with the same package scope, the same pending in-package drafts (so a reference to a sibling draft the same Publish-all lands is not an error) and the same restored credential paths. Each refusal is added to `_diagnostics.errors` with the gate's rule id as `code` and the publish's own `message`, and `valid` becomes `false`. ⛔ No rule is restated.
13+
- **The draft still saves.** No draft save is refused; publish refuses exactly what it refused before; package publish keeps its atomic rollback.
14+
- **Paths.** A gate path inside the item (`objects.invoice.fields.total.summaryOperations`) is served item-relative (`fields.total.summaryOperations`); a gate path that names a place rather than a key (a flow edge) is served as the gate wrote it.
15+
- **Not covered.** The save-door refusals (they refuse the save itself, so no stored draft carries them), the plugin authoring gates and the package build probes that `publish-drafts` runs, and the list reads and the diagnostics sweep, which keep their read-time verdicts.

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ import { zodIssuesToMetadataIssues } from './protocol.js';
3737
// landed by #8300). Composed into `decorateMetadataItem` below — see the
3838
// ordering note there for why it is not applied per read exit.
3939
import { redactMetadataItem } from './metadata-redaction.js';
40+
// [#26707] The snapshot key a write type's findings are spelled under — the
41+
// runtime gate's own answer, so the publish verdict below is re-spelled
42+
// against the same prefix the gate wrote it with.
43+
import { stackKeyForType } from '@objectstack/lint/runtime';
44+
import type { RuntimeAuthoringIssue } from '@objectstack/spec/api';
4045

4146
/**
4247
* Re-export the canonical validation-result type so callers in this
@@ -316,6 +321,64 @@ export function decorateMetadataItems<T>(type: string, items: T[]): T[] {
316321
return items.map((item) => decorateMetadataItem(type, item));
317322
}
318323

324+
// ---------------------------------------------------------------------------
325+
// [#26707] The publish verdict on a draft's `_diagnostics`
326+
// ---------------------------------------------------------------------------
327+
328+
/**
329+
* A runtime-gate finding's path, re-spelled relative to the item it was found
330+
* on: the gate spells it inside its private per-write snapshot
331+
* (`flows[0].edges[1].condition`, `objects.invoice.fields.total`), and
332+
* `_diagnostics` paths are dotted and item-relative (`edges.1.condition`), the
333+
* spelling Studio's inline errors and canvas badges resolve. A path that does
334+
* not start at the written item (a finding located on a sibling, or a `where`-
335+
* style locator) is kept as the gate wrote it.
336+
*/
337+
function itemRelativeIssuePath(type: string, name: string, path: string): string {
338+
const stackKey = stackKeyForType(PLURAL_TO_SINGULAR[type] ?? type);
339+
if (!stackKey) return path;
340+
let rest: string | undefined;
341+
for (const prefix of [`${stackKey}[0]`, `${stackKey}.${name}`]) {
342+
if (path === prefix || path.startsWith(`${prefix}.`) || path.startsWith(`${prefix}[`)) {
343+
rest = path.slice(prefix.length);
344+
break;
345+
}
346+
}
347+
if (rest === undefined) return path;
348+
return rest.replace(/\[(\d+)\]/g, '.$1').replace(/^\./, '');
349+
}
350+
351+
/**
352+
* Fold what publishing a draft would refuse into the draft's `_diagnostics`.
353+
*
354+
* `issues` are the runtime authoring gate's refusal for the draft body —
355+
* `ObjectStackProtocolImplementation#draftPublishVerdict`, the judgement the
356+
* publish door enforces — and each becomes one `errors[]` entry carrying the
357+
* publish's own `message`, with its `rule` id as the entry's `code`. Any of
358+
* them makes the verdict `valid: false`: the draft is still stored, but it does
359+
* not say it is valid while the publish says it is not. No issues: the schema
360+
* verdict is returned as it was. A type with no schema verdict (`undefined`)
361+
* gets one only when there is something to report.
362+
*/
363+
export function withPublishVerdict(
364+
type: string,
365+
name: string,
366+
diagnostics: MetadataDiagnostics | undefined,
367+
issues: readonly RuntimeAuthoringIssue[],
368+
): MetadataDiagnostics | undefined {
369+
if (issues.length === 0) return diagnostics;
370+
const refusals = issues.map((issue) => ({
371+
path: itemRelativeIssuePath(type, name, issue.path),
372+
message: issue.message,
373+
code: issue.rule,
374+
}));
375+
return {
376+
...diagnostics,
377+
valid: false,
378+
errors: [...(diagnostics?.errors ?? []), ...refusals],
379+
};
380+
}
381+
319382
// ---------------------------------------------------------------------------
320383
// ADR-0047 — reference-integrity diagnostics for list views
321384
// ---------------------------------------------------------------------------

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

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
import type { ObjectStackProtocolImplementation } from './protocol.js';
5050
import { permissionSetOwnerBindingRefusal, runtimeHookWithoutBodyRefusal } from './protocol.js';
5151
import { evaluateRuntimeAuthoringGate } from './runtime-authoring-gate.js';
52-
import type { RuntimeAuthoringIssue, RuntimePendingDeclarations } from './runtime-authoring-gate.js';
52+
import type { RuntimeAuthoringIssue, RuntimeAuthoringVerdict, RuntimePendingDeclarations } from './runtime-authoring-gate.js';
5353
import { savedItemNameRefusal } from '@objectstack/metadata/view-container-name';
5454
import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from '@objectstack/spec/shared';
5555
import type { IObjectQLEngine } from '@objectstack/spec/contracts';
@@ -67,11 +67,13 @@ export interface AuthoringGateHost {
6767
assertRuntimeAuthoringRules: ObjectStackProtocolImplementation['assertRuntimeAuthoringRules'];
6868
readonly authoringChannel: ObjectStackProtocolImplementation['authoringChannel'];
6969
readonly authoringGates: ObjectStackProtocolImplementation['authoringGates'];
70+
collectBatchPendingDeclarations: ObjectStackProtocolImplementation['collectBatchPendingDeclarations'];
7071
credentialChannelFor: ObjectStackProtocolImplementation['credentialChannelFor'];
7172
defaultRecordPageSaveRefusal: ObjectStackProtocolImplementation['defaultRecordPageSaveRefusal'];
7273
readonly engine: ObjectStackProtocolImplementation['engine'];
7374
externalBindingSaveRefusal: ObjectStackProtocolImplementation['externalBindingSaveRefusal'];
7475
foldStoredCollection: ObjectStackProtocolImplementation['foldStoredCollection'];
76+
getOverlayRepo: ObjectStackProtocolImplementation['getOverlayRepo'];
7577
hierarchyResolverMounted: ObjectStackProtocolImplementation['hierarchyResolverMounted'];
7678
isArtifactBacked: ObjectStackProtocolImplementation['isArtifactBacked'];
7779
orgWallEnforced: ObjectStackProtocolImplementation['orgWallEnforced'];
@@ -167,9 +169,33 @@ export async function runAuthoringGate(
167169
});
168170
}
169171

172+
/**
173+
* The members {@link judgeRuntimeAuthoringRules} reads: the host facts and
174+
* collections every judgement of the shared rules is made against.
175+
*/
176+
type RuntimeRulesHost = Pick<AuthoringGateHost, 'authoringChannel' | 'engine' | 'foldStoredCollection' | 'hierarchyResolverMounted' | 'orgWallEnforced' | 'resolveSduiManifest' | 'resolveWritePackageScope' | 'rlsMembershipResolverKeys' | 'tenancyPostureInForce'>;
177+
170178
/** The body of `ObjectStackProtocolImplementation#assertRuntimeAuthoringRules`; the method's docblock there is its contract. */
171179
export async function assertRuntimeAuthoringRules(
172-
this: Pick<AuthoringGateHost, 'authoringChannel' | 'engine' | 'foldStoredCollection' | 'hierarchyResolverMounted' | 'orgWallEnforced' | 'resolveSduiManifest' | 'resolveWritePackageScope' | 'rlsMembershipResolverKeys' | 'tenancyPostureInForce'>,
180+
this: RuntimeRulesHost,
181+
evt: Parameters<typeof judgeRuntimeAuthoringRules>[0],
182+
): Promise<RuntimeAuthoringIssue[]> {
183+
const verdict = await judgeRuntimeAuthoringRules.call(this, evt);
184+
if (verdict.error) throw verdict.error;
185+
return verdict.advisories;
186+
}
187+
188+
/**
189+
* [#26707] The judgement {@link assertRuntimeAuthoringRules} enforces, handed
190+
* back instead of thrown: the gate's {@link RuntimeAuthoringVerdict}, its
191+
* refusal (`error`) beside its advisories. The assert above is this function
192+
* plus the throw, so a caller that only needs to KNOW the verdict — the draft
193+
* read's publish verdict ({@link draftPublishVerdict}) — reads the same
194+
* collections, the same host facts and the same early returns as the publish
195+
* that enforces it, and there is no second account of either.
196+
*/
197+
export async function judgeRuntimeAuthoringRules(
198+
this: RuntimeRulesHost,
173199
evt: {
174200
type: string; name: string; state: 'draft' | 'active'; body: unknown; source?: string;
175201
/**
@@ -211,7 +237,8 @@ export async function assertRuntimeAuthoringRules(
211237
* round trip for a secret it still holds.
212238
*/
213239
restoredCredentialPaths?: () => Promise<readonly string[]>;
214-
}): Promise<RuntimeAuthoringIssue[]> {
240+
}): Promise<RuntimeAuthoringVerdict> {
241+
const clean: RuntimeAuthoringVerdict = { error: null, advisories: [] };
215242
// [#6710] The ADR-0005 carve-out, now DECLARED instead of inferred.
216243
//
217244
// This line used to read `if (this.environmentId === undefined)
@@ -250,21 +277,24 @@ export async function assertRuntimeAuthoringRules(
250277
// #6710 re-keyed this activation and left that one on the retired
251278
// proxy, which cost the ADR-0090 D11 object posture gate every
252279
// host-config deployment until #7674 finished the move.
253-
if (this.authoringChannel === 'package-author') return [];
280+
if (this.authoringChannel === 'package-author') return clean;
254281
if (evt.state !== 'active') {
255282
// [#26188] D1: no rule gates a draft. The one finding a draft does hear
256283
// is a hierarchy scope this runtime cannot enforce, as a warning on the
257284
// 2xx; it needs no collection read, so the draft keeps paying none.
258285
// A `migrate-stored` pass heals storage and hears nothing (see below).
259-
if (evt.source === 'migrate-stored') return [];
286+
if (evt.source === 'migrate-stored') return clean;
260287
const hierarchyResolverMounted = this.hierarchyResolverMounted();
261-
return evaluateRuntimeAuthoringGate({
262-
type: PLURAL_TO_SINGULAR[evt.type] ?? evt.type,
263-
name: evt.name,
264-
state: evt.state,
265-
body: evt.body,
266-
...(hierarchyResolverMounted !== undefined ? { hierarchyResolverMounted } : {}),
267-
}).advisories;
288+
return {
289+
error: null,
290+
advisories: evaluateRuntimeAuthoringGate({
291+
type: PLURAL_TO_SINGULAR[evt.type] ?? evt.type,
292+
name: evt.name,
293+
state: evt.state,
294+
body: evt.body,
295+
...(hierarchyResolverMounted !== undefined ? { hierarchyResolverMounted } : {}),
296+
}).advisories,
297+
};
268298
}
269299
// `os migrate meta --stored --apply` rewrites rows that ALREADY EXIST
270300
// into the current dialect. It is not an author publishing anything —
@@ -280,7 +310,7 @@ export async function assertRuntimeAuthoringRules(
280310
// no caller can spell its way past the gate. `duplicatePackage` is
281311
// deliberately NOT here — it mints brand-new rows under new names, and
282312
// a copy of a broken flow is a new broken flow.
283-
if (evt.source === 'migrate-stored') return [];
313+
if (evt.source === 'migrate-stored') return clean;
284314
const singular = PLURAL_TO_SINGULAR[evt.type] ?? evt.type;
285315

286316
// Resolution context. Best-effort: a host without a registry (a
@@ -420,6 +450,39 @@ export async function assertRuntimeAuthoringRules(
420450
// [#26188] With it the gate refuses a hierarchy scope no resolver enforces.
421451
...(hierarchyResolverMounted !== undefined ? { hierarchyResolverMounted } : {}),
422452
});
423-
if (verdict.error) throw verdict.error;
424-
return verdict.advisories;
453+
return verdict;
454+
}
455+
456+
/** The body of `ObjectStackProtocolImplementation#draftPublishVerdict`; the method's docblock there is its contract. */
457+
export async function draftPublishVerdict(
458+
this: RuntimeRulesHost & Pick<AuthoringGateHost, 'collectBatchPendingDeclarations' | 'credentialChannelFor' | 'getOverlayRepo'>,
459+
draft: { type: string; name: string; body: unknown; packageId: string | null },
460+
): Promise<RuntimeAuthoringIssue[]> {
461+
const singular = PLURAL_TO_SINGULAR[draft.type] ?? draft.type;
462+
// The closure "Publish all" judges this draft against: its package's other
463+
// drafts, collected by the batch door's own collector from the batch door's
464+
// own listing. A package-unbound draft has no batch; its publish is the
465+
// per-item door's, which states none.
466+
let pending: RuntimePendingDeclarations | undefined;
467+
if (draft.packageId) {
468+
const repo = this.getOverlayRepo();
469+
if (typeof repo.listDrafts === 'function') {
470+
pending = await this.collectBatchPendingDeclarations(await repo.listDrafts({ packageId: draft.packageId }));
471+
}
472+
}
473+
// The promotion's restored-credential read: the stored draft holds no
474+
// credential the write-only channel holds, so the gate is told where one is.
475+
const channel = this.credentialChannelFor(singular);
476+
const verdict = await judgeRuntimeAuthoringRules.call(this, {
477+
type: singular,
478+
name: draft.name,
479+
state: 'active',
480+
body: draft.body,
481+
packageId: draft.packageId,
482+
...(pending !== undefined ? { pending } : {}),
483+
...(channel
484+
? { restoredCredentialPaths: () => channel.heldPaths({ name: draft.name, state: 'draft', item: draft.body }) }
485+
: {}),
486+
});
487+
return (verdict.error as { issues?: RuntimeAuthoringIssue[] } | null)?.issues ?? [];
425488
}

0 commit comments

Comments
 (0)