fix(metadata-protocol): the /meta list serves one row per name after a runtime PUT - #8332
Conversation
…override against the MetadataService baseline The MetadataService merge in `getMetaItems` keyed a hand-rolled Map on `(package, name)` with strict equality, while `mergePackageAwareOverlay` one layer above resolves per `(slot, package)` and lets a package-less row stand in for each package's row of that name. A runtime `PUT /api/v1/meta/<type>/<name>` sends no `?package=`, so its sys_metadata row is package-less; for a type whose baseline lives in the MetadataService (skill, agent, tool) the registry listing is empty, the override left the overlay merge unstamped, its key missed the package-bearing baseline row, and the list served both — the duplicate skill row the card reports. The MetadataService merge now runs the same package-aware resolution instead of a second implementation of it, with the runtime listing as the base and the registry+overlay result as the higher layer, so the 'overlay wins over the artifact baseline' guard it replaces is preserved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
…ll-read-path-dedup
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
…ll-read-path-dedup
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Part of #7654
Closes the metadata half of that card — symptom 1, the duplicate row. Symptom 2 (the MCP prompt bridge) is
domain:cli's lane, is deliberately not touched here, and is filed as #8328 with aBlocked-byline pointing at this work.packages/mcpis untouched by this PR.The card's premise, verified
Symptom 1 reproduces on current
main. Reverting just this PR's hunk and re-running the new suite serves the card's exact shape — one skill, two rows, disagreeing aboutactive:Root cause — neither "un-deduped" nor "registered wrongly"
The card was dispatched with two candidate shapes: add a dedup, or register
skillthe way types that already merge correctly are registered. It is neither, and the difference decides the fix.getMetaItemsmerges three layers, and two of them answered the identity question differently:mergePackageAwareOverlay— thesys_metadataoverlay merge — resolves per(slot, package)and treats a package-less row as standing in for each package's row of that name. That is exactly howgetMetaItem(name, packageId=P)resolves.Mapkeyed on(package, name)with strict equality, so a package-less row occupied a slot of its own rather than standing in for anything.Which merge decides a type is a function of where its baseline comes from.
email_templateis resolved by the first, because its rows come from the SchemaRegistry.skill,agentandtoolreach the list through the MetadataService's own loaders, so the registry listing is empty and they are resolved by the second.A runtime
PUT /api/v1/meta/{type}/{name}carries no?package=, sosys_metadatatakes apackage_id IS NULLrow. With an empty registry listing the overlay merge has no base row to take provenance from and leaves the override body with no_packageId. Its key then missed the package-bearing baseline row in the merge below, the "already present, do not overwrite" guard never fired, and both rows were served.The fix
Delete the second implementation and call the first. The MetadataService merge now runs that same package-aware resolution, with the runtime listing as the base layer and the registry-plus-overlay result as the higher one — so the documented precedence (a
sys_metadatacustomization wins over the artifact baseline) is preserved, while the two steps can no longer disagree about what a package-less row means.One hunk, in the
getMetaItemsread merge only.Not a
skillspecial caseTwo pins in the new suite make this a class fix rather than a patch:
agentduplicates identically and is closed by the same resolution. Any fix that special-casedskill, or that merely "registeredskilllike every other type", would leave it broken.ADR-0048 resolution is unchanged: two packages shipping one name still stay two rows, and a package-less override now reaches both of their slots.
Verification
Reverse verification, direction predicted before running: restoring the hand-rolled
itemMapmust turn the duplication cases red naming two rows where one is expected, and must leaveprotocol.i18n-bundle-list-merge.test.tsgreen. Both observed — 5 failed / 17 passed in the new suite, i18n suite green.Green for the right reason, proved rather than asserted. A control suite that passes in both states proves nothing on its own, so its reachability was measured directly: with
items = []injected immediately after the new merge call, 3 of the i18n suite's tests go red. It genuinely executes the branch this PR replaces, and its green result under the real fix is therefore meaningful.@objectstack/metadata-protocol@objectstack/rest@objectstack/objectql@objectstack/runtimeGates, re-derived against the actual changed paths rather than taken from the dispatch list:
check:nul-bytes,check:cross-package-test-inputs,check:durability-log-level,check:filter-alias-parity,check:changeset-gate-self-tests,check:objectui-changeset,check:type-check-coverage,check:type-check-debt,check:query-options-erasure,check-changeset-no-major— all pass. The re-derivation surfaced the four changeset-triggered families the dispatch list did not name.Concurrency
protocol.tsis under the region exemption. This PR's region is thegetMetaItemsread merge; #8136's is error and catch message construction. The regions are disjoint,mainis merged as of this push, and the merge queue arbitrates — no manual ordering was applied to either side.Generated by Claude Code