You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0712470
Browse filesBrowse the repository at this point in the historyBrowse files
fix(metadata-protocol): a flow read's _diagnostics and the restore doors judge a stored flow in its D2-converted spelling (#29753)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ85ZKppiwDrpyUp1d2RvAzUX)_Fixes#29696
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:流程的读取 `_diagnostics`(单项、列表、`?layers=true`、草稿读取、诊断汇总)的 schema
判定,以及两道恢复门(`rollbackMetaItem`、`revertCommit`)的运行时规则,改为按 D2
转换后的拼写判定流程,与保存门、两道发布门同一条缝 `storedFlowVerdictBody`;返回与恢复的仍是存储原字节。
- 为什么改:旧拼写的流程行(如 get_record 的 `filters`)保存、发布、引擎注册都能过,读取却标 `valid:
false`,恢复历史版本则被 422 拒掉,同一行几处答案不一。
- 风险与回滚:只放宽(转换后仍不合格的行照旧 `valid: false`、恢复照旧被拒,错误与新拼写一致);回滚即 revert 本 PR。
- 先看:`metadata-diagnostics.ts` 的
`computeMetadataDiagnostics`(读取判定入参)、`protocol-authoring-gate.ts` 的
`refuseRestoredBodyTheSaveDoorRefuses`、`protocol-flow-save-replay.ts` 的
`storedFlowVerdictBody` 注释(转换链与引擎一致的实测说明)。
- 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2 src/protocol.draft-publish-verdict.test.ts
src/protocol.restore-doors-save-refusal-family.test.ts -t 29696`,预期 9
条全绿。
- 需要你做的:无,席位落地。
## What changes
- **The read's schema half.** `computeMetadataDiagnostics` judges a
`flow` through `storedFlowVerdictBody` (the seam the save door's
fallback verdict and both publish doors judge through, PR #29694). Every
read exit that decorates reaches it: `getMetaItem` (active and draft),
`getMetaItems`, `getMetaItemLayered` and the `getMetaDiagnostics` sweep.
The read serves the body it was handed; the converted body is the
verdict's only. Not a redaction, so the #8154 order in
`decorateMetadataItem` is untouched (the body judged is still the
unredacted one; no flow redactor or cleartext-credential refusal exists
in the flow schema).
- **The restore doors.** `refuseRestoredBodyTheSaveDoorRefuses` (both
`rollbackMetaItem` and `revertCommit`'s restore limb) hands the runtime
authoring rules `storedFlowVerdictBody(type, body)`, as
`promoteDraftForPublish` does for a flow draft. The version is restored
as stored; the credential read and every other refusal in the family
read the stored version. The restore doors still do not ask the schema
gate (its docblock's stored-shape reason stands).
- **The excluded conversion, settled in the docblock.** Measured: the
engine's rehydration (`canonicalizeFlow` in
`service-automation/src/flow-definitions.ts`) replays the full chain
with retired entries and excludes
`CONVERSIONS_NOT_REPLAYED_AT_REHYDRATION`, which names the same one id
as `FLOW_FALLBACK_VERDICT_NOT_REPLAYED`
(`flow-decision-mode-inclusive-explicit`). So `storedFlowVerdictBody`
already replays exactly the engine's chain. `storedFlowVerdictBody`'s
docblock now says so, says why the excluded entry never decides
acceptance (it is a default flip: an omitted decision `mode` parses and
means exclusive), cites the maintainer ruling (letter C on #15429) that
it is not replayed over stored flows, and drops the old claim that
`applyConversionsToStoredItem` replays this chain on read (no read
converts a flow: `convertStoredItem` skips `flow`). No converter added;
no conversion list changed.
- No `packages/spec` change.
## Why
A flow row at rest in a legacy ADR-0087 D2 spelling (`get_record`
`config.filters`) is accepted by the save door, both publish doors and
the engine, but its reads answered `valid: false` and both restore doors
refused a history version in that spelling. Measured red on
`origin/main` `0eb97fa4`, the pins committed first (`e905c02c`, fix not
yet applied):
<details><summary>Red, before the fix</summary>
```text
× the active item read reads valid: true and serves the stored bytes
× the list read reads valid: true and serves the stored bytes
× the layered read reads valid: true (first run: the double lacked registry.getItem; fixed in 3bf9be7, red for the right reason in the ablation below)
× the draft read of a re-save of it reads valid: true
× a row still refused once converted reads valid: false, with the errors its canonical spelling reads
× rollbackMetaItem: answers INVALID_METADATA / 422; nothing is stored
× rollbackMetaItem CONTROL: an acceptable version of the same item is restored
× revertCommit CONTROL: the restore limb restores an acceptable version of the same item
Tests 8 failed | 1 passed | 84 skipped (93)
read: { valid: false, errors: [ { path: 'nodes.1.config.filters', code: 'custom', ... get_record contract: Unrecognized key(s) ... `filters` } ] }
restore: Error: flow/acme_fetch failed author-time validation: 2 issues — flows[0].nodes[1].config.filters [expression-invalid]; flows[0] [flow-region-shape-invalid]
```
</details>
<details><summary>The excluded conversion, measured against the built
spec (scratch probe, not committed)</summary>
```text
start → decision (two conditioned out-edges, no mode) → two ends
constructed raw valid: true
constructed stored-verdict chain valid: true mode: (omitted)
constructed full chain valid: true mode: inclusive
```
The flip changes meaning, never acceptance: no body is refused for want
of it.
</details>
## Tests
- New top-level `describe('[#29696] …')` in
`packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts`
(the #29557 harness; its legacy-flow fixtures are hoisted to module
scope so both blocks share them). A legacy row seeded at rest: the
active item read, the list read, the layered read and the draft read of
a re-save each answer `_diagnostics: { valid: true }` and serve
`config.filters` as stored. A legacy row that also carries an undeclared
config key reads `valid: false` with exactly the `_diagnostics` its
`filter` twin reads.
- New `pinBothDoors` case in
`packages/metadata-protocol/src/protocol.restore-doors-save-refusal-family.test.ts`:
a legacy history version restores at both doors (the active row equals
the history bytes, `filters` included); a legacy version that also
carries an edge whose CEL does not parse is refused 422
`INVALID_METADATA` at both doors on `expression-invalid`, with nothing
reported at `filters`, and stores nothing.
- The #29557 block's parenthetical saying the read's schema half still
judged the stored body is replaced with a pointer to the new block.
<details><summary>Ablation (fix committed first at d2c880d, mutated
through scripts/ablation-replace.mjs, restored to the HEAD
blob)</summary>
```text
leg 1: metadata-diagnostics.ts withoutResolvedPicklistOptions(storedFlowVerdictBody(singular, stripReadDecorations(item))) -> withoutResolvedPicklistOptions(stripReadDecorations(item))
× the active item read / the list read / the layered read / the draft read / a row still refused once converted
Tests 5 failed | 4 passed | 84 skipped (93)
ok restored: blob == HEAD (2125c9e) and git diff HEAD is empty
leg 2: protocol-authoring-gate.ts body: storedFlowVerdictBody(type, body), -> body,
× rollbackMetaItem refused / rollbackMetaItem CONTROL / revertCommit CONTROL
Tests 3 failed | 6 passed | 84 skipped (93)
ok restored: blob == HEAD (6fb69ae) and git diff HEAD is empty
```
</details>
## Acceptance notes
- The boot census in `protocol.ts` (`[metadata_spec_invalid] … fails the
current spec schema even after conversion`) reads
`computeMetadataDiagnostics` too, so a legacy D2 flow row no longer
draws that warning: for a flow the sentence was false before (no read
converts a flow), and the engine registers the row. Not separately
pinned.
- A still-refused flow's `_diagnostics` error paths are located on the
converted body, as the publish doors' issues are (#29557); for a key
rename such as `filters` → `filter` the path of an unrelated finding is
unchanged.
- `runAuthoringGate` (plugin-registered gates) on the restore path still
receives the stored version. No in-tree plugin registers a `flow` gate
(only `object`), and the publish promotion does not call it at all; left
as is.
- `FLOW_FALLBACK_VERDICT_NOT_REPLAYED` and
`CONVERSIONS_NOT_REPLAYED_AT_REHYDRATION` stay tied by prose only:
neither is on its package's published surface, and metadata-protocol
cannot import service-automation (the dependency runs the other way).
Noted, not filed.
- PR template headings are folded into the os-dev section order.
## Verification
All readings on head `760e096f` unless named (the fix `d2c880d5` plus a
test-only fixture-typing commit). `origin/main` moved 10 commits since
the branch point `0eb97fa4`; none touches `packages/metadata-protocol`
or the spec's conversion or automation sources, so no merge was taken.
- `pnpm --filter @objectstack/metadata-protocol test`: Test Files 308
passed, 4 skipped; Tests 22277 passed, 26 skipped (lock VERDICT
command-exit 0; run on `d2c880d5`). On `760e096f` the two touched test
files: 2 files, 93 tests passed.
- `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0 on
`760e096f` (exit 2 on `d2c880d5`: a TS2352 cast in the new fixture,
fixed in `760e096f`).
- `dispatch-gates --commands` on `760e096f`: 61 derived; `--ran`
reconciliation: 60 run, 1 NOT MEASURED (`check:dual-build-cjs-loads`,
exit 3 PREREQUISITE NOT MET: it reads every package's dist and this
worktree built only the touched closure; CI runs it), 0 unrun.
`check-plugin-teardown-shape --self-test` first answered exit 3 (shallow
clone lacked its pinned fixture commit) and exit 0 after `git fetch
origin 621a487`. `pnpm lint` exit 0.
<details><summary>Gate record (command :: exit code)</summary>
```text
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-code-casing :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:filter-alias-parity :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm lint :: exit 0
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbaKY2r8oRQXNGhYHHg)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(metadata-protocol): a flow read's `_diagnostics` and the restore doors judge a stored flow in its D2-converted spelling, as the save and publish doors do
6
+
7
+
Clause-②: yes (widening)
8
+
Metadata-protocol: none
9
+
10
+
A flow stored in a legacy ADR-0087 D2 spelling that the automation engine registers (a `get_record` node's `config.filters`, for one) is accepted by the save door and both publish doors, but every read of it (`GET /meta/flow/:name`, the list read, the `?layers=true` read, the draft read and the diagnostics sweep) answered `_diagnostics: { valid: false }` naming the legacy key, and both restore doors (`rollbackMetaItem`, `revertCommit`) refused a history version in that spelling with `422 INVALID_METADATA`. The read's schema verdict and the restore doors' runtime rules now judge the row through the doors' conversion seam, with the chain the engine replays when it registers a stored row, and serve or restore the row as stored. A flow the rules still refuse once converted still reads `valid: false`, with the errors its current spelling gets, and a version refused once converted is still refused at restore.
0 commit comments