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 ec29753
Browse filesBrowse the repository at this point in the historyBrowse files
fix(metadata-protocol): the metadata list serves every stored seed item, named by its row (#31480)
domain:server#3 · os-project-manager · session_01FEkQ5pXkgNXVpCn2GZDjyy
Body: seat refresh (round 1) · 2026-11-08T11:58Z
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X1nhftM2hZHHvqk2GLFyVTN)_
domain:server#3 · os-project-manager · session_01FEkQ5nBSzc6ktJyjdFcDb3
Fixes#31399
Clause-②: no (a list read starts serving rows already stored and already
served by name; no accepted input changes, no SeedSchema change)
Metadata-protocol: none
## 维护者速读
- 改了什么:元数据列表合并 `mergePackageAwareOverlay` 遇到「正文里没有 `name`」的存储项(种子
`seed`,`SeedSchema` 不声明 `name`,名字在 `sys_metadata` 行上)时,改用该行的 `name`
列归槽,并在列出的副本上带出这个名字;列表的诊断(`_diagnostics`)按行里存的正文判定,不把这个补上的名字算作违规。存储的正文不改,注册表水合不改,`SeedSchema`
不改。
- 为什么改:`GET /api/v1/meta/seed` 与 `getMetaItems({ type: 'seed' })`
一条存储的种子都列不出来,而 `GET /api/v1/meta/seed/:name` 每条都读得到。真实 showcase 启动实测:3
条已发布种子,列表 `[]`。
- 风险与回滚:只影响正文里根本没有 `name` 键的存储项:`seed`,也包括其它无名的存储行(例如 schema 尚未声明 `name`
时存下的旧 translation
行、畸形的无名行),它们以前从列表里消失,现在按行名列出(诊断照旧按存储正文判定)。`object_extension` 按 `extend`
归槽,不受影响。正文自带 `name` 的(包括 `name: null`)照旧按自身名字列出,带 `name` 的旧种子与 `name:
null` 的正文仍被诊断标出(有对照 pin)。列出的副本多带一个 `name`,原样 PUT 回严格 schema
会被拒(按名读不受影响)。metadata-protocol、objectql、cloud-connection、rest
全量测试全绿。回滚即还原本 PR。
- 先看:`packages/metadata-protocol/src/protocol-overlay-merge.ts` 的
`mergePackageAwareOverlay` 记录循环与
`SERVED_ROW_NAME`,`protocol-meta-list-reads.ts` 的 `decorateListedItem`,新
pin `protocol.seed-list-row-name.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run
src/protocol.seed-list-row-name.test.ts`,预期 11 条全绿;其中 7 条在 main 上是红的。
- 需要你做的:无,席位落地。
## What changes
- **`mergePackageAwareOverlay`** (`protocol-overlay-merge.ts`): a stored
record whose body states no slot name of its own (`listSlotNameOf`
answers nothing) is slotted by its row's `name` column, the name the
by-name read finds it under, and served as a copy carrying that `name`.
The copy is marked with a symbol key, `SERVED_ROW_NAME`, which object
spreads along the list keep and the wire never sees. A body that states
its own name keeps it, `name: null` included (only an absent `name`
falls to the row's). The stored body is never written. ⚠️ The one
exception to `listSlotNameOf`'s "what a read serves PUTs back": a listed
row-named item carries a `name` its strict schema refuses, so it is
refused if PUT back verbatim; the by-name read serves the body as
stored. Stated once in the `SERVED_ROW_NAME` docblock, pointed to from
`listSlotNameOf` and `getMetaItemsForExecution`. `StoredRowPlace` gains
the optional `name`.
- **`readFlattenedMetaItems`** (`protocol-meta-list-reads.ts`): the
active, stand-in, override stand-in and draft-preview records pass their
row's `name` in `stored`. The registry hydration loop is untouched, so
seeds are still not registered as metadata items
(`packages/metadata/src/plugin.ts` keeps its note). The served exit
decorates through `decorateListedItem`: a row-named item's
`_diagnostics` are computed on the body as stored (without the served
`name`, which `SeedSchema` does not declare), then the item is served
with the name. Every other item is decorated exactly as before.
- The MetadataService merge and `protocol-stored-collection-fold.ts`
pass no `stored`, so they are unchanged.
## Tests
- New
`packages/metadata-protocol/src/protocol.seed-list-row-name.test.ts` (11
rows):
- §1: N=3 stored seed rows list 3 named items on the environment's list
and on the package-scoped list, each body and `_diagnostics` equal to
what `getMetaItem` serves under that name, and `names` narrows by the
row's name.
- §2: a package's shipped seed and its stored row of the same name are
one slot (the stored row's), and another package's same-name seed keeps
its own slot.
- §3: the draft preview lists draft seed rows by their row names.
- §4 controls: a body that names itself keeps its name (a dashboard), a
stored seed body that spells the retired `name` keeps its diagnostic as
the by-name read does, a body whose `name` is null is still listed once
with `_diagnostics.valid` false (as on main), and listing does not write
the stored body.
## Acceptance notes
- **The door measured first (order step 1).** On a real showcase boot
(dogfood `bootShowcase`, a scratch probe, not committed), with 3 seed
rows published into an environment package, `GET /api/v1/meta/seed`,
`?package=` and `/meta/seeds` all answered `{"type":"seed","items":[]}`,
while `GET /api/v1/meta/seed/s_one` served the body. The door and the
protocol method agree, so the card was not narrowed.
- **Responsibility (by blame).** Keying the list on the body's own
`name` began in ee9312f (2026-04-23, "feat(template-seeding)", `if
(data && typeof data === 'object' && 'name' in data)
byName.set(data.name, data)`), carried through ac1fc4c (#1569). It
was restated as the merge's skip `if (!(body && … 'name' in body))
continue;` in 0e41302 (#3231, ADR-0048), and moved to
`listSlotNameOf` + `if (bodyName === undefined) continue;` in 0c1829a
(#28811 / #28574). Seeds became stored items with no `name` in the body
and met that skip.
- **Found on the real boot, fixed here.** The first cut served the row
name but the list's diagnostics then judged it against the closed
`SeedSchema` and marked every listed seed `valid: false` (`Unrecognized
key(s) on this seed: name`). The served exit now judges such a body as
stored. After the fix the same probe lists `s_one`, `s_three`, `s_two`,
each with `_packageId`, `name` and `_diagnostics: {"valid":true}`, on
all three list forms.
- **#30808's install-local seed receipt**
(`readUnmanagedCopySeedReceipt`, `unmanagedCopySeedNames`): it could go
back to a list read now. `getMetaItems({ type: 'seed', packageId })`
serves each of the base's seed rows under its name, with the same
served-row order as the by-name read, and `seedDatasetOfReceiptItem`
projects to `SeedSchema`'s members, so the served `name` would not reach
the loader. Not changed here, per the card.
- **Worktree.** The claim names `objectstack-issue-31399`; this ran in
the chain's existing worktree `objectstack-issue-31317` on the same
claimed branch, cut from fresh `origin/main` after #31398 merged.
## Verification
Head `2eeb629e` (base `1ff99441`, the #31398 merge; `origin/main`
`f67f3b35` merged once before opening).
<details><summary>Red first, fix, suites</summary>
- The pin with both source files restored to base: Tests 7 failed | 3
passed (10), e.g. `AssertionError: expected [] to have a length of 3 but
got +0`; the three controls green. With the fix: 10 passed.
- `@objectstack/metadata-protocol` vitest after the merge of main: Test
Files 327 passed | 4 skipped (331), Tests 22587 passed, 26 skipped.
- `@objectstack/objectql` vitest after the merge (metadata-protocol from
the rebuilt dist): Test Files 571 passed (571), Tests 11558 passed.
- Before the merge of main: `@objectstack/cloud-connection` Test Files
75 passed (75), Tests 994 passed; `@objectstack/rest` Test Files 399
passed (399), Tests 7834 passed, 354 skipped; runtime's seed-listing
files (`http-dispatcher.test.ts`,
`packages-protocol-handle-typing.test.ts`,
`packages-publish-drafts-response-conformance.test.ts`) Tests 270
passed.
- Contract review (6720799308) should-fix S1: the new `name: null`
control row red on `f7fcd126` (Tests 1 failed | 10 passed (11),
`expected [] to have a length of 1 but got +0`), green on main's sources
and with the fix (11 passed). At the fixed head:
`@objectstack/metadata-protocol` vitest Test Files 327 passed | 4
skipped (331), Tests 22588 passed, 26 skipped.
- `typecheck` for `@objectstack/metadata-protocol`: exit 0. `eslint
--no-inline-config packages/metadata-protocol`: exit 0.
</details>
<details><summary>Ablation (ablation-replace, each restored to
HEAD)</summary>
- `protocol-overlay-merge.ts`: `const bodyName = ownName ?? rowName;`
made to drop the row name (`String("ABL31399").length < 0 ? rowName :
undefined`). Mutation landed (anchor 1 to 0, blob 092cd58 to
caaa58088f89). Pin: Tests 7 failed | 3 passed (10). Restored: blob ==
HEAD, `git diff HEAD` empty.
- `protocol-meta-list-reads.ts`: `decorateListedItem` made to judge
every item as served (`|| String("ABL31399").length > 0`). Mutation
landed (blob 847f260 to b47ee1324e3d). Pin: Tests 1 failed | 9
passed (10), the §1 diagnostics row, `expected { valid: false, errors: [
… ] } to deeply equal { valid: true }`. Restored: blob == HEAD, `git
diff HEAD` empty.
- Review fix (S1): `const bodyName = ownName !== undefined ? ownName :
rowName;`, ablated back to `ownName ?? rowName` (blob 2cb92a5 to
cd890289c11b): Tests 1 failed | 10 passed (11), the `name: null` row,
`expected [] to have a length of 1 but got +0`. Restored: blob == HEAD,
`git diff HEAD` empty.
- The pin reads its own `src/`, so no dist leg.
</details>
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `23eb8808` (63 commands) and again at
`f7fcd126` (71, the ledger commit derives 8 more `scripts/` gates), each
run with `timeout 1500` and stdin from `/dev/null`, exit codes captured
before any pipe, after a `@objectstack/runtime` closure build. First
pass: 62 exit 0; `check:engine-double-contract` exit 1, asking the
pinned ledger to record the new pin's `findOne` double. Fixed by `node
scripts/check-engine-double-contract.mjs --write`
(`scripts/engine-double-contract.pinned.json`, +5 lines, the one file
outside the claim's surface), then exit 0. The 8 added gates: exit 0.
`dispatch-gates --ran`: 71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN.
After the review fix, at `2eeb629e`: 79 derived (the touched
`protocol.ts` derives 8 more), all 79 run with exit 0 after a
metadata-protocol build; `dispatch-gates --ran`: 79 derived, 79 run, 0
NOT-MEASURED, 0 UNRUN. Plus typecheck and scoped eslint above (re-run at
`2eeb629e`: exit 0). Not run locally (CI): `test:repo`, dogfood shards,
type-check lanes.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5nBSzc6ktJyjdFcDb3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(metadata-protocol): the metadata list serves every stored `seed` item, named by its row
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
`getMetaItems({ type: 'seed' })`, the read behind `GET /api/v1/meta/seed`, listed no stored seed item while `GET /api/v1/meta/seed/:name` served each one. A seed body states no name (`SeedSchema` declares none; the name lives on the stored row), and the list merge keyed every stored body by its own `name`, so it skipped each seed. The merge now slots a stored body that states no name by its row's name, the name the by-name read finds it under, and serves it carrying that name. This holds on the environment's list, the package-scoped list and the draft preview, and a package's shipped seed of the same name shares that one slot. A body that states its own name keeps it, the stored body is not written, and `SeedSchema` is unchanged.
0 commit comments