Repository navigation
Commit 7e6e8a4
fix(metadata-protocol)!: every metadata read states a stored row's package from its column, never from its body (#28734)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XYUY6repLynvqXTV558ytVx)_
Fixes #28673
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — nothing an author writes changes: the save door has
stripped a body `_packageId` since #16702, so no skill or doc teaches
writing one, and the served package of a row whose body carries none is
unchanged
## 维护者速读
- **改了什么**:所有读取存储元数据的地方(按名读取、草稿读取与预览、草稿列表、列表合并、注册表水合及视图容器展开)现在都只按数据库列
`package_id`
说明一行属于哪个包;正文里自带、与列不一致的包标记一律不再被回显。规则只写一处(`withRowBinding`),#28594
已在列表读取用过的同一条规则搬过来共用。
- **为什么改**:#28671
只修了列表读取;其余读取仍在正文没有标记时才补列值,正文里残留的标记会被原样返回,读出来的“所属包”与数据库不一致。
-
**风险与回滚**:低。只影响“正文带了与列不一致的包标记”的存储行(保存入口早已剥除该标记,只有旧数据或绕过保存入口写入的行才有);正文无标记或标记与列一致的行读出结果完全不变。不改任何保存、发布、恢复、安装入口。回滚即还原本
PR。
- **先看**:`packages/metadata-protocol/src/protocol.ts`(`withRowBinding`
及其六个调用点);`packages/metadata-protocol/src/protocol.row-binding-column.test.ts`;`packages/metadata-protocol/src/protocol-stored-overlay-reads.ts`(改为调用同一函数)。
- **两分钟验证**:`pnpm --filter @objectstack/metadata-protocol exec vitest
run src/protocol.row-binding-column.test.ts`,预期 21/21 通过(main 上 16 个失败)。
- **需要你做的**:无,席位落地。
## What changes
- **One rule, `withRowBinding` (`protocol.ts`).** It was the hydrator's
helper ("stamp the column's package while the body names none"). It is
now the rule #28671 wrote inline in `storedOverlayEntries`: a body
`_packageId` is replaced by the column's package, or dropped on a row
bound to no package (`boundPackageOf`). A body that already states the
column's binding is returned as it is, and any other is a copy, so a
cached row is never written. It is exported from `protocol.ts` for its
sibling module only; the package's `exports` map has `.` alone and
`index.ts` does not re-export it, so the published surface does not
move.
- **Its readers**, each one line:
- the list read, `storedOverlayEntries`
(`protocol-stored-overlay-reads.ts`): #28671's inline pass is replaced
by the call, so the rule has one spelling;
- the by-name active read, which is also the strict draft read's site;
- the by-name draft preview (`previewDrafts`);
- the draft list (`placeDraft` in `getMetaItems`);
- the list merge, `mergePackageAwareOverlay`: a stored record enters
under its own binding, so a stamp its body carries is not served, either
as itself or where a package-less row stands in for a package's slot. A
record that is not a stored row (the MetadataService merge's items,
slotted by their own `_packageId`) enters as before;
- the registry hydration, `hydrateOverlayIntoRegistry`: its
`registerItem` line keeps its census text
(`protocol.register-item-call-sites.test.ts`) and now drops or replaces
a body stamp; `hydrateExpandedViewItems` expands the container under the
same binding, so no item a view container row expands to is placed in a
package its body names.
- No save, publish, restore or install door is touched, and neither is
`stripDerivedProvenance`. No `packages/spec` hunk and no gate.
## Tests
<details><summary>Red on a base with #28671, green at head:
protocol.row-binding-column.test.ts (21 cases)</summary>
Written first against #28671's head (`43953ed3`) and run there: `Tests
16 failed | 5 passed (21)`; the 5 green are the controls. Ported
unchanged onto `origin/main` `15e83114` (#28671 squash-merged). At head:
`Tests 21 passed (21)`.
- **The rule:** dropped on a package-less row (`undefined`, `null`,
`''`, the `'sys_metadata'` sentinel), replaced by another package, kept
when absent or agreeing, a copy whenever it changes anything,
non-records untouched.
- **The card's four pins:** a by-name active read and a draft read
(strict and preview) of a package-less row whose body carries
`_packageId` serve none; a row whose body names another package serves
the column's; an absent or agreeing stamp is served as before.
- **One pin per remaining site:** the draft list (both cases plus
controls); the list merge (a package-less row served as itself, the same
row standing in for a package's slot, a row naming another package,
controls including "the caller's body is not written"); hydration (a
package-less row, a row naming another package, a package-less view
container whose expansions must carry no package, controls).
- **The view-expansion stand-in** of a scoped list (`held` in
`getMetaItems`, a package-less row the merge held back and served in a
package's slot) keeps its own line: it states the slot's package
(#21817), not a column, and its input is now bound at the source (the
list read and the merge). Its behaviour stays pinned by
`protocol.scoped-list-fallback.test.ts`, green at head.
</details>
<details><summary>Ablation (restored, `blob after restore == blob at
HEAD`)</summary>
Via `node scripts/ablation-replace.mjs`, the suite importing `src/`
directly (no rebuild in the path). The by-name active read pointed back
at the old rule, `if (recPkg && item && … _packageId === undefined)
item._packageId = recPkg`. On disk: old spelling 1, new spelling 0.
Result `Tests 3 failed | 18 passed (21)`, exactly that site's three pins
(package-less, another package, strict draft). Restore: `blob after
restore dfdd126… == HEAD`, `git diff HEAD` empty.
</details>
<details><summary>Package suite and typecheck</summary>
- `vitest run` (whole package) at `192e943b`: `Test Files 293 passed | 4
skipped (297) · Tests 21975 passed | 26 skipped (22001)`, under the
lock.
- `pnpm typecheck` at that commit answered `error TS6133` on an unused
import in the new test file; `9ed89b48` drops it. At `9ed89b48`: `pnpm
typecheck` exit 0, and the new file `Tests 21 passed (21)`. The only
later commit, `47907244`, records the new test's engine doubles in
`scripts/engine-double-contract.pinned.json`.
- `eslint --no-inline-config` on the three touched source files: exit 0.
</details>
<details><summary>Dogfood battery (the `(narrowing)` arm)</summary>
`pnpm --filter @objectstack/dogfood test` under the lock on the
`@objectstack/dogfood^...` closure at `9ed89b48`: `Test Files 371 passed
| 3 skipped (374) · Tests 2994 passed | 28 skipped (3022)`, `VERDICT
command-exit 0`. The one later commit, `47907244`, touches the
engine-double ledger only.
</details>
## Acceptance notes
- **Clause-② measured: `no (narrowing)`.** No accepted input widens, and
the published surface does not move (the `exports` map is `.` only and
the new export is not re-exported). What shrinks is a served value: a
row whose body carries a `_packageId` other than its column's is now
served and registered under the column's package, or none. That is the
arm #28671's `@objectstack/metadata-protocol` changeset took for the
same rule at the list read, accepted by its contract review
(`6558328209`, ②). So the changeset carries the BREAKING banner and the
ADR-0087 `no-migration-prescription` disposition, shipped `patch`
(`Metadata-protocol: none`: what an author can write is unchanged).
- **The two stand-in lines that stamp a slot's package keep their own
spelling.** `mergePackageAwareOverlay`'s "a package-less body standing
in for package `real`" line and the scoped list's held stand-in state
the slot's package (#21817, #28409), not a column. Pointing them at
`withRowBinding` would also run `boundPackageOf` over a slot key that
can be the `'sys_metadata'` sentinel and drop it. The echo they carried
came from their inputs, which are now bound where a column is known (the
list read, the merge's stored records, the draft list).
- **`'sys_metadata'` at the list read.** `storedOverlayEntries` now
normalises through `boundPackageOf`, as the hydrator always has. A row
whose column holds the `'sys_metadata'` sentinel (no writer stores one;
the sentinel is the registry's) is served with no `_packageId` instead
of `'sys_metadata'`; every reader already reads that sentinel as "no
package" (`isCodeArtifactBody`, `lookupArtifactItem`).
- **PR #28693 (#28412, in review)** adds `stored-row-binding.ts`: the
hydrator records a flow row's column on the registered body under a
non-enumerable symbol (`bindStoredRow`), which the automation boot pull
judges. That helper and this one answer different questions and stay
separate. `withRowBinding` decides the served `_packageId`.
`bindStoredRow` is an unforgeable carrier a registry reader can trust
where a served `_packageId` cannot, because code artifacts and envelope
grafts carry one too. Both read the same column at the same hydration
line, and they no longer disagree. One sentence in that module's header
("`withRowBinding` keeps a body's own stamp") becomes false with this
PR. Whichever lands second corrects it.
## Verification
Derived gate list (`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, 61 commands), local subset run at the final
head with every exit code captured before any pipe and reconciled by
`--ran`; every family CI runs is read from this PR's check-runs.
At `47907244` (the head): all 69 derived commands exit 0, including
`check:engine-double-contract` (which reds at `9ed89b48` until the new
test's doubles are recorded, and is green after that commit),
`check:type-check-debt` and `check:dual-build-cjs-loads`.
`dispatch-gates --ran`: `69 derived famil(ies) accounted for — 69 run, 0
NOT-MEASURED`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5pjZq4w6ScHBPDy6t1
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5pjZq4w6ScHBPDy6t1)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e735b79 commit 7e6e8a4
5 files changed
Lines changed: 403 additions & 28 deletions
File tree
- .changeset
- packages/metadata-protocol/src
- scripts
| 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 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| |||
171 | 171 | | |
172 | 172 | | |
173 | 173 | | |
174 | | - | |
175 | | - | |
176 | | - | |
177 | | - | |
178 | | - | |
179 | | - | |
180 | | - | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
181 | 177 | | |
182 | 178 | | |
183 | 179 | | |
| |||
0 commit comments