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 bdfed89
Browse filesBrowse the repository at this point in the historyBrowse files
fix(metadata-protocol): the customizations read lists a moved edit under the package that serves it (#31668)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9W9WqZzLRELZak23ZMvZcMR)_Fixes#31656
Clause-②: no — what the customizations list holds for a moved edit; no
accept set changes
Metadata-protocol: none
## 维护者速读
- 改了什么:套件拆分(或回滚)把条目挪到同一 App 的另一个包后,"包的定制列表"(`GET
/packages/:id/customizations`)现在把租户的修改列在实际生效的新包下,且只列在那里。
- 为什么改:#31593 之后修改已在新包下生效,但列表仍按修改时声明的旧包来列:拆包后列在已不再发运该条目的 App
下、新模块下却没有;回滚后两边都列不出。列表改为读取与生效读取同一组候选与判定。
- 风险与回滚:只读,不写任何数据;"同一
App"仍只由安装记录(`appOf`)判定。条目锁本就读取该条目的全部存储行,实测对挪动的修改与在新包下保存的修改给出相同答案,未改,只加了钉。回滚即还原本
PR。
-
先看:`packages/metadata-protocol/src/protocol-served-overrides.ts`(逐条目按候选顺序选行,以及"旧包不再列"的排除)。
- 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run
src/domains/meta-overlay-follows-suite.test.ts`(需先构建
metadata-protocol),预期 12 个全绿。
- 需要你做的:无,席位落地。
## What changes
**As measured on `origin/main` after #31653.** The card read "listed
under neither". Measured, it depends on the direction: after a flat →
composed split the pair override declaring the App was still listed
under the App (which no longer ships the item) and not under the module
that serves it; after a composed → flat rollback it was listed under
neither. Both are fixed.
**The list reads the served reads' own candidates, one reading (seat
ruling 1).** `listServedOverrides` groups the pair's ACTIVE rows by item
(folded type, name, bundle member) and picks each item's row through
`servedOverlayRowCandidates` and `holdsServedOverlayCandidate`, in their
order, with the item's shipping packages read the way the by-name read
reads them (`shippingPackageIds(shippedArtifactsOf(...))`). So a package
lists the pair's declared override of its item first, else the pair's
override that `isMovedOverrideRowOf` holds as a moved edit. Same App is
read only inside the predicates (`appOf` over installed manifests); no
key list and no same-App test of its own.
**Listed only where it is served (ruling 2).** A declared override whose
package no longer ships the item, which a package shipping it now holds
as its moved edit (`isMovedOverrideRowOf`), is that package's and is not
also listed under the former owner. A former owner that still ships the
item keeps its edit, listed as today.
**The list stays the pair's rows.** A row bound to a package of the App
itself (a Regime O save from before the ADR-0146 D11 close) is served at
the new owner through `isMovedBaseRowOf`'s candidate, but it is not a
pair row: it is listed under no package, before or after the move, as
such a row bound to its still-shipping owner is not listed today. Named
in the report's open questions. Since the list reads no base-bound rows,
"two former owners' rows, list neither" has no listed case; the served
reads already serve neither.
**Item lock (ruling 3): measured, unchanged.** The lock's overlay layer
reads every ACTIVE row of the item whichever package it is bound to
(`overlayLockLayerAt`, no package filter), so a moved edit was already
in its scope. A pin holds that a moved edit carrying a stamped lock
answers at the module exactly what the module's own edit answers: the
read's `lock`/`editable`/`deletable`/`lockReason` and the save door's
`ITEM_LOCKED`. No save door refusal changes.
Files: `protocol-served-overrides.ts`, `protocol.ts` (one host member,
`shippedArtifactsOf`, passed to the list), the test, a patch changeset.
`protocol.ts` is outside the delta's named surface: the host-split
pattern needs the member wired there, as #31653 did for the by-name
read. No `packages/spec/**`, no `ui/**`, no `docs/adr/**`;
`served-override.ts` and `item-lock.ts` are not edited. No forced test
edits.
## Tests
Six cases added to
`packages/runtime/src/domains/meta-overlay-follows-suite.test.ts` (the
suite-move harness #31653 added: one SQLite tenant DB, a fresh real
kernel per version), as a top-level `describe`:
1. flat → composed: the module lists the edit, the App does not.
2. composed → flat (rollback): the App lists the edit, the uninstalled
module does not.
3. CONTROL: a former owner that still ships the view lists its edit as
today; the module lists nothing.
4. NEGATIVE: another App's pair declaring HotCRM's App, and HotCRM's
pair declaring the partner App: never listed under the module, the App
or the partner.
5. CONTROL: a pre-close row bound to the App is served at the module and
listed under neither, before or after the move.
6. The item lock answers for a moved edit at the module what it answers
for the module's own edit (pinned, holds on main).
<details><summary>Red on main first (529e535, fix sources restored from
main, tests kept): 2 failed | 10 passed (12)</summary>
```
× flat → composed: the module lists the edit saved under the App, and the App no longer does
AssertionError: expected { module: [], app: [ [ …(2) ] ] } to deeply equal { module: [ [ …(2) ] ], app: [] }
(received app: [["x3677e_case.default", "6.1.0"]], module: [])
× composed → flat (rollback): the App lists the edit saved under the module, though the module is gone
AssertionError: expected { app: [], module: [] } to deeply equal { app: [ [ …(2) ] ], module: [] }
Tests 2 failed | 10 passed (12)
```
The lock pin passes on main: the lock half of the premise does not hold.
</details>
<details><summary>Ablations (fix committed first;
scripts/ablation-replace.mjs, dist preflight, restore proven blob ==
HEAD each leg)</summary>
The runtime test reads `@objectstack/metadata-protocol` through its
`dist/`, so each leg rebuilt it; `ablation-dist-preflight.mjs` found the
marker in 2 built files before the run and in none of 24 after the
restore build, with the tree clean. The legs ran on a local commit over
#31653's branch head `0fad163d` (identical to its squash on main for
every file it touched) whose `protocol-served-overrides.ts` blob
`0b23b796` is the one at this PR's head.
- **A. Moved candidate not read by the list** (blob `0b23b796` →
`2f6f269e`): `Tests 2 failed | 10 passed (12)`, cases 1 and 2. Restored
`0b23b796` == HEAD.
- **B. Former-owner exclusion removed** (→ `a53e1f98`): `Tests 1 failed
| 11 passed (12)`, case 1, the App lists the moved edit too. Restored ==
HEAD.
- **C. Moved candidate admitted without its predicate** (→ `7dfc0eba`):
`Tests 2 failed | 10 passed (12)`, cases 3 and 4, the module lists the
App's still-shipped edit and the pair row declaring the partner App.
Restored == HEAD.
Case 5 and the lock pin hold on main and guard behaviour this diff does
not change, so no ablation is owed for them.
</details>
## Acceptance notes
- The package upgrade plan's override advice (`listOverrides` in
`@objectstack/objectql`, #31627) reads this list, so it follows the same
rule; its tests pass unchanged (`package-upgrade-environment.test.ts`).
- Lock prose ranking: among several rows declaring the strictest lock,
the row bound to the address's own package ranks first. A moved base row
is bound to the former owner, so it ranks as another package's row; the
lock value is unaffected, and a pair row ranks the same moved or not.
Not changed here.
## Verification
Head `a135e658` (branch `claude/project-thread-o96evq`): fix `28e7e921`
cut from `origin/main` `529e5351` after #31653 merged, then
`origin/main` merged in at `a135e658` (merge, no rebase; the two
incoming commits touch `plugin-auth` and `metadata`, not this surface).
- **Build:** `pnpm install` 0; `pnpm build` 119/119 (on `28e7e921`);
`turbo build --filter ./packages/**` 81/81 after the merge.
- **Tests:** the suite-move file 12/12, with `packages-customizations`,
`dispatcher-plugin.package-customizations-mount` and
`meta-legacy-pair-served`: 4 files / 41 tests passed on `a135e658`.
`@objectstack/metadata-protocol` 328 files / 22595 tests passed (4 files
/ 26 tests skipped) on `28e7e921`. `@objectstack/objectql`
`package-upgrade-environment` and `protocol-meta` 101 passed. Heavy runs
through `os-verify-lock.sh` (VERDICT command-exit 0).
- **Typecheck:** metadata-protocol 0, runtime 0 (runtime's
`check:test-typecheck` first reported
`organization-ownership-boot-guard.turso-remote.kernel.test.ts`, a known
prerequisite: the optional peer `driver-turso` had no dist; built,
re-run 0).
- **Lint:** eslint `--no-inline-config` over the 2 changed sources and
the test, exit 0.
- **Gates:** `dispatch-gates --commands` derived 72 families; all 72 run
on `a135e658`, each exit 0 (also on `28e7e921`, where two first exited 3
on container prerequisites: the teardown self-test's fixture commit,
fetched; `dual-build-cjs-loads` needs every dist, after `pnpm build` 0).
`dispatch-gates --ran`: 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN.
`check-platform-checklist --changed-from origin/main`: OK, 0 lost.
- **Dogfood:** not owed (no `!` in the title, no narrowing arm).
- **`test:repo`:** declared to CI, not run locally.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmPHUhgip3YbMfQh27s)_
Co-authored-by: Claude <noreply@anthropic.com>
fix(metadata-protocol): the customizations read lists a moved edit under the package that serves it
6
+
7
+
Clause-②: no — what the customizations list holds for a moved edit; no accept set changes
8
+
Metadata-protocol: none
9
+
10
+
When a later version of an App moves an item from one of its packages to another (an ADR-0130 D4 suite split, or its rollback), the tenant's edit of the item is served at the package that ships it now. `GET /packages/:id/customizations` (`listServedOverrides`) now lists that edit there too, and only there.
11
+
12
+
-**Before.** The list took a pair override's package from its declaration, the owner at the time of the edit. After a flat → composed split the edit was listed under the App, which no longer ships the item, and not under the module that serves it; after a rollback it was listed under neither.
13
+
-**Now.** The list picks each item's row through the served reads' own candidates and predicates (`servedOverlayRowCandidates`, `holdsServedOverlayCandidate`, `isMovedOverrideRowOf`), so a package lists exactly the pair override its address serves. Same App is read only from the install record (`appOf` over the installed manifests), never from an id prefix; a row of another App's pair, or a declaration naming another App, is never listed.
14
+
-**Unchanged.** An edit whose owner still ships the item is listed as before. The list stays the pair's rows: a row bound to a package of the App itself (saved before ADR-0146 D11) is listed under no package, before or after a move. The upgrade plan's override advice reads this list, so it follows the same rule. The item lock already reads every stored row of the item whichever package it is bound to, so it answers for a moved edit what it answers for an edit saved at the new owner; that is pinned, not changed.
15
+
-**Nothing is written.** The read moves and rewrites no row.
return{lock: r?.lock,editable: r?.editable,deletable: r?.deletable,lockReason: r?.lockReason, save };
255
+
}
256
+
257
+
describe('[#31656] the customizations read lists a moved edit under the package that serves it, and the item lock answers for it as for an edit there',()=>{
258
+
it('flat → composed: the module lists the edit saved under the App, and the App no longer does',async()=>{
259
+
constdb=tenantDb();
260
+
constflat=awaitboot(db,'flat');
261
+
awaiteditInPair(flat,APP,'6.1.0');
262
+
// CONTROL: listed under its own package, as before the move.
it('CONTROL: a pre-close row bound to the App is served at the module and listed under neither, as such a row bound to its owner is not listed today',async()=>{
0 commit comments