fix(metadata-protocol): unscoped metadata list dedupes package-aware, not by bare name (ADR-0048 #1828)#3231
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
force-pushed
the
claude/adr-0048-metadata-dedupes-yje377
branch
from
July 18, 2026 15:51
a53890a to
3eea67e
Compare
… not by bare name (ADR-0048 #1828) `getMetaItems` merged registry items, `sys_metadata` overlay rows, draft-preview rows, and MetadataService items into `Map`s keyed by bare `name`. When two installed packages ship the same `type/name` (e.g. `page/home`) and either has an overlay, the DB-overlay merge collapsed them to one row (last-write-wins), so an unscoped `GET /meta/:type` returned only one — and the frontend prefer-local resolution, which reads the unscoped list, could no longer tell the two packages' rows apart. Key the three merge sites (plus the env/org `mergedMap`) by `(package, name)` via a shared `mergePackageAwareOverlay` helper whose per-package resolution mirrors `getMetaItem(name, packageId=P)` (scoped row, else package-less/global fallback), so: - two packages' same-name rows stay distinct, each with its own `_packageId`; - a package-less (env-wide) overlay still wins over the single artifact it customizes — ADR-0005 precedence and single-package behaviour unchanged; - a legacy row whose active/draft layers disagree on package attribution still collapses (draft wins), matching the single-item path. Two provenance fixes so a collision no longer mislabels `_packageId`: - scope the registry-hydration artifact graft to the row's own `package_id`; - `SchemaRegistry.getArtifactItem(type, name, packageId)` no longer returns a bare-key overlay owned by a DIFFERENT package. The bare slot holds one row (last hydrate wins); once the list stopped collapsing, the list-decorate step (`lookupArtifactItem(type, name, <row's own package>)`) would otherwise graft that one package's id onto every same-name row. Found by dogfooding a real two-package collision against the running server. Tests: two-package overlay collision keeps both rows with correct `_packageId`; single-package env-wide overlay unchanged; MetadataService collision keeps both; `getArtifactItem` rejects a foreign-package bare entry. Verified end-to-end on a booted showcase app (unscoped `/meta/page` returns both rows, each correct `_packageId`; scoped reads isolate). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DKFjwN2EVXPB2CJwNzPs8c
os-zhuang
force-pushed
the
claude/adr-0048-metadata-dedupes-yje377
branch
from
July 18, 2026 16:16
3eea67e to
4b99652
Compare
os-zhuang
marked this pull request as ready for review
July 18, 2026 16:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1828.
Problem
The unscoped
getMetaItems(packages/metadata-protocol/src/protocol.ts) merges registry items,sys_metadataoverlay rows, draft-preview rows, and MetadataService items intoMaps keyed by barename(four sites). When two installed packages ship the sametype/name(e.g.page/home) and either has asys_metadataoverlay, the DB-overlay merge collapses them to one row (last-write-wins), so an unscopedGET /meta/:typereturns only one. The frontend prefer-local resolution (preferLocal(list, name, app._packageId)in objectuiapp-shell) reads that unscoped list, so a collapsed list makes it pick the surviving row regardless of package.The single-item (
?package=) and Studio/layered paths were already fixed by ADR-0048's earlier issues (#1810/#1816/#1819/#1822/#1827); the unscoped list was the remaining gap.Fix
Key the merge by
(package, name)via a sharedmergePackageAwareOverlayhelper whose per-package resolution mirrorsgetMetaItem(name, packageId=P)— a package-scoped row wins for its own package, else the package-less ("global",package_id IS NULL) row is the fallback — so the list and single-item paths agree. Applied to all four dedup sites: env/orgmergedMap, DB-overlay active merge, draft-preview merge, MetadataService merge.Two provenance fixes so a collision no longer mislabels
_packageId:package_id;SchemaRegistry.getArtifactItem(type, name, packageId)no longer returns a bare-key overlay owned by a different package. The bare slot holds a single row (last hydrate wins); once the list stopped collapsing, the list-decorate step (lookupArtifactItem(type, name, <row's own package>)) would otherwise graft that one package's id onto every same-name row. This second bug was invisible to unit tests and only surfaced by dogfooding a real collision against the running server (see below).Behaviour
_packageId.package_id-bearing draft still collapse to one slot (draft wins), matching the single-item path.Acceptance (from #1828)
GET /meta/pagewith two packages each shipping the same page returns both rows, each with its own correct_packageId.preferLocal(pre-existing objectui code) filters this now-correct list by_packageId; the backend contract it depends on is proven. The objectui console UI itself was not driven — the console is a vendored bundle (.objectui-sha) not built in this framework worktree.Live verification (dogfood)
Booted the showcase example app (
objectstack dev --ui --seed-admin, SQLite, isolated port) and created a real collision — two packages each with a package-scopedpage/dogfood_collisionoverlay:GET /meta/page(unscoped)com.dogfood.alpha→ "Home from ALPHA",com.dogfood.beta→ "Home from BETA"GET /meta/page?package=com.dogfood.alphaGET /meta/page?package=com.dogfood.betaBoth rows survive with the correct
_packageId; scoped reads isolate. Pre-fix, the unscopedbyNamededup collapsed these to a single row. (This live run is what exposed and drove thegetArtifactItemprovenance fix above.)Tests
New unit tests:
protocol-meta.test.ts— two-package same-name overlays both survive with correct_packageId; single-package env-wide overlay still overlays (one row); MetadataService same-name collision keeps both.registry-prefer-local-resolution.test.ts—getArtifactItemrejects a foreign-package bare entry.Verified locally:
@objectstack/objectqlsuite (71 files, 956 tests) green — including view-identity-overlay, registry-shadow, layered-overlay, and cross-package-collision regressions;@objectstack/runtime(556) and@objectstack/restgreen; changed files typecheck clean;check:nul-bytes+eslint --no-inline-configpass.Related: #1822 (single-item/list provenance), #1827 / #1824 (package-scoped overlays).
🤖 Generated with Claude Code
https://claude.ai/code/session_01DKFjwN2EVXPB2CJwNzPs8c