Repository navigation
Commit 5782008
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
File tree
- .changeset
- packages/metadata-protocol/src
- scripts
- ui
- .changeset
- packages/app-shell/src/views/metadata-admin
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
40 | 45 | | |
41 | 46 | | |
42 | 47 | | |
| |||
316 | 321 | | |
317 | 322 | | |
318 | 323 | | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
319 | 382 | | |
320 | 383 | | |
321 | 384 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
| 52 | + | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| |||
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
| 70 | + | |
70 | 71 | | |
71 | 72 | | |
72 | 73 | | |
73 | 74 | | |
74 | 75 | | |
| 76 | + | |
75 | 77 | | |
76 | 78 | | |
77 | 79 | | |
| |||
167 | 169 | | |
168 | 170 | | |
169 | 171 | | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
170 | 178 | | |
171 | 179 | | |
172 | | - | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
173 | 199 | | |
174 | 200 | | |
175 | 201 | | |
| |||
211 | 237 | | |
212 | 238 | | |
213 | 239 | | |
214 | | - | |
| 240 | + | |
| 241 | + | |
215 | 242 | | |
216 | 243 | | |
217 | 244 | | |
| |||
250 | 277 | | |
251 | 278 | | |
252 | 279 | | |
253 | | - | |
| 280 | + | |
254 | 281 | | |
255 | 282 | | |
256 | 283 | | |
257 | 284 | | |
258 | 285 | | |
259 | | - | |
| 286 | + | |
260 | 287 | | |
261 | | - | |
262 | | - | |
263 | | - | |
264 | | - | |
265 | | - | |
266 | | - | |
267 | | - | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
268 | 298 | | |
269 | 299 | | |
270 | 300 | | |
| |||
280 | 310 | | |
281 | 311 | | |
282 | 312 | | |
283 | | - | |
| 313 | + | |
284 | 314 | | |
285 | 315 | | |
286 | 316 | | |
| |||
420 | 450 | | |
421 | 451 | | |
422 | 452 | | |
423 | | - | |
424 | | - | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
425 | 488 | | |
0 commit comments