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 f2f6bf0
Browse filesBrowse the repository at this point in the historyBrowse files
fix(metadata-protocol): migrate-stored re-saves a draft-only permission set granting on an object gone since (#29350)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9Cu6rc6Nw1Ty87DTpPRgFnW)_Fixes#29279
Clause-②: yes (widening) — measured: base `a19759ac` reports the
draft-only set `failed` (422 unknown-grant sentence); head reports it
`rewritten`. Every author-facing door judges as on `main` (the two
author pins below hold on both trees).
Metadata-protocol: none — measured: no key, value or type an author can
write changes; the only accept-set change is a server-stated `source:
'migrate-stored'` re-save, which no client can claim (the REST layer
never forwards `source`).
## 维护者速读
- **改了什么**:`os migrate meta --stored --apply`(以及 `POST
/api/v1/meta/_migrate-stored`)重存一个只有草稿、需要转换(如去掉已退役的
`managedBy`)的权限集时,不再因为它授权的对象已经不存在而报
`failed`;发布该草稿时仍然会被拒。发布时的提示文字改成说明发布时查找的范围。
- **为什么改**:#28990
的「授权对象不存在」拒绝用权限集的已发布行判断「已持有的授权」,只有草稿的权限集没有已发布行,于是迁移自己的重存被拒,旧键留在库里。
- **风险与回滚**:低。只放行服务端自己声明的 `migrate-stored` 重存(客户端无法伪造 `source`),和旁边种子
`externalId` 规则(#28952)的豁免同一写法;作者的保存、发布、回滚判定不变。回滚即撤回本 PR。
- **先看**:`packages/metadata-protocol/src/protocol.ts`(`saveMetaItem` 里
#28990 块加了 `writeSource !== 'migrate-stored'`;发布路径传
`'publish'`);`packages/metadata-protocol/src/protocol-save-door-refusals.ts`(提示文字分两种门);`packages/objectql/src/protocol-migrate-stored-draft-permission-grant.test.ts`(真内核用例)。
- **两分钟验证**:`pnpm --filter @objectstack/metadata-protocol exec vitest
run src/protocol.unknown-grant-object-refusal.test.ts`,预期 18/18 通过(新增的
`[#29279]` 两条在内)。
- **需要你做的**:无,席位落地。
## What changes
- **Fix 1 (behaviour).** The `unknownGrantObjectSaveRefusal` call in
`saveMetaItem` is skipped for a `migrate-stored` re-save, copying the
#28952 precedent beside it (`seedExternalIdSaveRefusal`'s `writeSource
!== 'migrate-stored'`). Chosen over "read the held keys from the draft
row" because it keeps every other door judged exactly as on `main`:
`writeSource` is server-stated (only `migrateStoredMetadata` sets it),
the publish promotion and the restore doors do not pass through this
branch, and the draft is still judged at its publish. Reading the draft
row instead would also have widened an author's draft re-save of a
draft-only set (its held keys would come from the draft), which the card
rules out.
- **Fix 2 (wording).** At `promoteDraftForPublish` the finding now reads
"…which is not defined in objects: no package registers an object of
that name, and no published object, or object draft of this batch,
carries it, so the grant applies to nothing." The save door and the
restore doors keep "…and none is saved here…". The path (`objects.NAME`)
and the leading "Permission 'SET' grants on object 'NAME' which is not
defined in objects" (the sentence family `defineStack` and Studio key
on) are unchanged. Implemented as a `door: 'save' | 'publish'` parameter
(default `'save'`) on the refusal; only the promotion passes
`'publish'`.
- **Fix 3 (changeset).**
`.changeset/29279-migrate-stored-draft-permission-grant.md` (patch) says
a draft stored before the rule is refused at its publish, as
`26724-object-unknown-datasource-refused.md` does. The pending #28990
changeset is not edited.
- Docblocks of `unknownGrantObjectSaveRefusal` (protocol.ts) updated:
the "stored-row migration … is not judged" bullet now sits under the
carve-out, which is what makes it true for a draft-only set.
## Tests
- New real-kernel file
`packages/objectql/src/protocol-migrate-stored-draft-permission-grant.test.ts`
(`ObjectKernel` + `ObjectQLPlugin`, in-memory driver copied from
`protocol-duplicate-package-objects-key-collision.test.ts`): an object
draft and a set draft granting on it, the set's stored body given the
retired `managedBy` key, the object draft removed. The measured case (→
`rewritten`, key stripped, grant kept, still a draft), its publish still
refused, the control with the object draft present (→ `rewritten`), and
an author's draft re-save still refused.
- `protocol.unknown-grant-object-refusal.test.ts`: new `[#29279]`
describe (the pass rewrites a planted draft-only set; its publish is
refused; an author re-save is refused).
- **Forced test edit:** the same file's three publish-path assertions
(`publishMetaItem` ×2, `publishPackageDrafts`) now expect the publish
wording; `unknownFinding`/`expectRefusal` take a `door` argument. The
`publishPackageDrafts` case now asserts the whole first sentence instead
of the `grants on object` fragment.
## Acceptance notes
- The premise test needed the planted row's `checksum` cleared: a row
whose stored stamp equals the converted body's hash is a no-op write
(`storedBodyUnchanged`), so planting the key under the old stamp made
the control read `rewritten` while writing nothing. A row really stored
with the key carries its own stamp. Noted, not a defect.
## Verification
All readings at head `f7ee536e` unless marked.
<details><summary>Premise on base a19759a (real-kernel file, before the
fix)</summary>
```
× the pass rewrites it: the legacy key is stripped and the grant kept, and the set stays a draft
AssertionError: {..."state":"draft",...,"outcome":"failed","reason":"permission/qa_probe failed spec validation: objects.qa_widget: Permission 'qa_probe' grants on object 'qa_widget' which is not defined in objects: no package registers an object of that name and none is saved here, so the grant applies to nothing. ..."}
Tests 1 failed | 3 passed (4) (control rewritten, author re-save refused, publish refused: green)
```
</details>
<details><summary>Ablations (fix committed first; each leg restored with
git checkout HEAD, blob hash equal to HEAD, git status clean)</summary>
- A1 carve-out removed (`'migrate-stored'` → `'ablate-29279'`),
metadata-protocol rebuilt, `ablation-dist-preflight` marker present in
dist/index.js and dist/index.cjs: metadata-protocol file `1 failed | 17
passed` (× `migrate-stored` rewrites it…); objectql real-kernel file `1
failed | 3 passed` (× the pass rewrites it…). Restore leg rebuilt,
preflight `--absent` exit 0, tree clean.
- A2 carve-out widened to every draft save (`&& mode !== 'draft'`): `3
failed | 15 passed` (× a draft save into a package…, × a draft save
naming no package…, × an author re-saving the same draft is still
refused).
- A3 the promotion stops asking (`if (draftForGate && false)`): `4
failed | 14 passed` (× per-item publish…, × publishMetaItem refuses it…,
× publishPackageDrafts answers refused…, × migrate-stored rewrites it…,
and its publish is still refused).
- Head legs: `18 passed (18)`; `4 passed (4)`.
</details>
<details><summary>Consumer tests (head f7ee536, after merging
origin/main 504ba51; merge-tree clean)</summary>
- `pnpm --filter @objectstack/metadata-protocol test`: Test Files 301
passed, 4 skipped (305); Tests 22129 passed, 26 skipped. `typecheck`:
exit 0.
- `@objectstack/objectql` `src/protocol-duplicate-package-*` (4 files)
plus the new real-kernel file: Test Files 5 passed (5), Tests 24 passed
(24). `typecheck`: exit 0.
- Dogfood (closure built under the lock):
`test/permission-unknown-grant-object-refusal.dogfood.test.ts` and
`test/wall-catalog-refusal.dogfood.test.ts`: Test Files 2 passed (2),
Tests 24 passed (24).
- Full dogfood battery: not owed (no `!` in the title, no `(narrowing)`
arm).
</details>
<details><summary>Gates (head f7ee536)</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 62 families derived; all 62 run, exit 0.
Two first exited 3 (prerequisite): `check-plugin-teardown-shape
--self-test` (pinned commit fetched, then 48 cases pass) and
`check:dual-build-cjs-loads` (all packages built, then 115 entry points
load). `--ran` reconciliation: "62 run, 0 NOT-MEASURED (a DERIVED
zero)".
- Lint: neither package has a `lint` script; `pnpm exec eslint
--no-inline-config` on the four touched source and test files: exit 0.
- CI families: read from this PR's check-runs.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmRrBmeYea4FHGY5wro)_
Co-authored-by: Claude <noreply@anthropic.com>
fix(metadata-protocol): the stored-metadata healing pass re-saves a draft-only permission set that grants on an object which exists nowhere now, instead of reporting it `failed`
6
+
7
+
Clause-②: yes (widening) — `migrateStoredMetadata` (`os migrate meta --stored --apply`, `POST /api/v1/meta/_migrate-stored`) now rewrites a draft-only permission set whose stored body needs a conversion and whose grant names an object with no active row and no draft; it reported that row `failed` with the unknown-grant `422`. Every author-facing save, publish and restore door judges the grant as before.
8
+
Metadata-protocol: none
9
+
10
+
-**Before.** The save door's unknown-grant refusal skips a grant the set already holds, read from the set's active row. A set that is only a draft has none, so the pass re-saving its own draft (for example to strip the retired `managedBy` key) was refused once the object draft it granted on had been discarded, and the legacy key stayed at rest.
11
+
-**Now.** A `migrate-stored` re-save is not judged for unknown grants, as it already is not for a seed's keyed field: it rewrites a row the store already holds. A draft stored before the rule is refused at its publish, as any other publish of it is.
12
+
-**Wording.** At a draft's publish the finding now says what the publish looked for: "no package registers an object of that name, and no published object, or object draft of this batch, carries it". The save door's sentence, the finding path and the leading "Permission 'SET' grants on object 'NAME' which is not defined in objects" are unchanged.
0 commit comments