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
An object overlay write over a packaged object is silently discarded by registerObject's ownership rule while saveMetaItem reports success #6995
Out-of-scope finding measured while scoping #6853 (registry heal / map-vs-contributors split). Recorded, not claimed. Pre-existing.
Fact (measured on origin/main @ 51f2bb8c3)
applyObjectRegistryMutation (packages/metadata-protocol/src/protocol.ts:7870) mirrors an accepted object overlay write into the registry with:
this.engine.registry.registerItem(request.type,request.item,'name');try{this.engine.registry.registerObject({ ...(request.itemasRecord<string,unknown>),_provenance: 'org'}asany,request.packageId||'sys_metadata',);}catch(err: any){console.warn(`[Protocol] registerObject failed for ${request.name}: ${err?.message??err}`);}
(generics spaced so the GitHub body sanitizer does not eat them)
SchemaRegistry.registerObject (packages/objectql/src/registry.ts:1148) throws when an own contributor from a different package already holds the name. So whenever the row's package_id does not equal the packaged owner's package id — including the ordinary package-less row, which normalises to the sys_metadata sentinel — the second half of the write is thrown away, warn-logged, and the save still reports success.
The two registry views then disagree permanently:
metadata['object'][name] (written by registerItem) holds the overlay body, so GET /api/v1/meta/object/:name serves it;
objectContributors still holds the package body, and that is what registry.getObject resolves — the surface assertObjectRegistered and every data-CRUD dispatch reads.
Measured with the protocol-delete-object-registry-heal.test.ts harness (real SchemaRegistry, real SysMetadataRepository), a packaged myapp_invoice owned by app.myapp plus a package-less overlay save carrying only name + overlay_only:
[save warning] [Protocol] registerObject failed for myapp_invoice: Object "myapp_invoice" is already
owned by package "app.myapp". Package "sys_metadata" cannot claim ownership.
[contributors] packageId=app.myapp ownership=own provenance=package
fields=[ ... name, amount, packaged_only ] <- the PACKAGE body, untouched
[metadata map] plain key "myapp_invoice" present <- the OVERLAY body
[getObject] fields=[ ... name, amount, packaged_only ] <- data plane never saw the overlay
The save's own receipt reports success, and nothing in the response says the runtime discarded half of it.
Reachability
Save path: needs the documented operator escape hatch (OS_METADATA_WRITABLE=object) or an equivalent door, because both the protocol gate (protocol.ts:8434) and SysMetadataRepository.assertAllowed (sys-metadata-repository.ts:1037) refuse an overlay over an artifact-backed object otherwise (measured: NOT_OVERRIDABLE/403 on both tenant and control-plane kernels).
Boot path: loadMetaFromDb (protocol.ts:11753) makes the same registerObject call for every stored object row. There the throw is counted — errors++ plus a Failed to hydrate warning (protocol.ts:11789) — so the boot leg is at least honest about it; the save leg is not.
Dispositions worth pricing (no recommendation forced)
Fail the save. Turn the swallowed throw into a named refusal (the overlay cannot claim ownership of a name another package owns) so declared = enforced and the caller learns the write will not take effect. Cost: a write that "worked" before now 4xx-es; needs an error code + wording decision.
Triage (pass #2, 2026-08-09, registered on #6015): held as finding, routed domain:metadata — subsumed prospectively by the #6853-B ruling (maintainer, 2026-08-09: the tenant overlay becomes its own contributor layer, which removes this silent-discard seam structurally; recorded in that ruling's item 2). Restart condition: if the ADR-0029 amendment's design ends up NOT covering this write path, this card promotes on its own measurements; otherwise it closes when B lands.
Closing — subsumed and implemented by ADR-0029 D9, landed as f16e54e1d (PR #7306, card #7277) at 2026-08-10T09:03:31Z.
This card offered three dispositions and deliberately forced no recommendation. Disposition 2 is what shipped — "register the overlay as a distinct contributor rather than as a competing own, so the ownership rule is never reached and both layers coexist" — via the contributor model D9 ratified: an overlay is a third, non-owningoverlay kind with replace semantics on the base layer, so registerObject's single-owner rule is never consulted for it and assertSingleOwnerPerObject stays literally unchanged.
The three bindings this card's measurement exposed were each implemented as ruled, and pinned:
Same package — normal registration.
Package-less row (the sys_metadata sentinel) — now accepted. The old throw was an artefact of the overlay borrowing the own slot; with its own kind there is no ownership claim to reject. This is the ordinary case the card measured, and it no longer loses half the write.
Different package — refused loudly at the producer, with the newly minted OBJECT_OVERLAY_PACKAGE_MISMATCH (422), and counted in loadMetaFromDb's per-record errors at boot. That closes the specific asymmetry this card opened with: the boot leg was already honest (errors++ plus a hydrate warning) while the save leg swallowed the throw and returned success. Both legs now report.
The permanent two-view disagreement the card measured — metadata['object'][name] serving the overlay body while objectContributors still resolved the package body, so the data plane never saw the overlay — cannot arise, because the overlay is a registered layer rather than a discarded write. D9's load-bearing property is that resolution is bit-for-bit what the old splice produced (_provenance: 'org' included), pinned directly by comparing a D9 registry against a pre-D9 one; only what the registry remembers changes.
Verified on origin/main: the landing is a single-parent commit carrying packages/objectql/src/registry.ts, packages/metadata-protocol/src/protocol.ts, packages/objectql/src/plugin.ts, both new pin files, and the two packages/spec declarations, with D9's markers live in registry.ts.
Not closed as a duplicate — this card's measurement is what established the failure was silent on the save leg, and #7306's #6995-labelled bindings are traceable to it.
Out-of-scope finding measured while scoping #6853 (registry heal / map-vs-contributors split). Recorded, not claimed. Pre-existing.
Fact (measured on
origin/main@51f2bb8c3)applyObjectRegistryMutation(packages/metadata-protocol/src/protocol.ts:7870) mirrors an acceptedobjectoverlay write into the registry with:(generics spaced so the GitHub body sanitizer does not eat them)
SchemaRegistry.registerObject(packages/objectql/src/registry.ts:1148) throws when anowncontributor from a different package already holds the name. So whenever the row'spackage_iddoes not equal the packaged owner's package id — including the ordinary package-less row, which normalises to thesys_metadatasentinel — the second half of the write is thrown away, warn-logged, and the save still reports success.The two registry views then disagree permanently:
metadata['object'][name](written byregisterItem) holds the overlay body, soGET /api/v1/meta/object/:nameserves it;objectContributorsstill holds the package body, and that is whatregistry.getObjectresolves — the surfaceassertObjectRegisteredand every data-CRUD dispatch reads.Measured with the
protocol-delete-object-registry-heal.test.tsharness (realSchemaRegistry, realSysMetadataRepository), a packagedmyapp_invoiceowned byapp.myappplus a package-less overlay save carrying onlyname+overlay_only:The save's own receipt reports success, and nothing in the response says the runtime discarded half of it.
Reachability
OS_METADATA_WRITABLE=object) or an equivalent door, because both the protocol gate (protocol.ts:8434) andSysMetadataRepository.assertAllowed(sys-metadata-repository.ts:1037) refuse an overlay over an artifact-backedobjectotherwise (measured:NOT_OVERRIDABLE/403 on both tenant and control-plane kernels).loadMetaFromDb(protocol.ts:11753) makes the sameregisterObjectcall for every storedobjectrow. There the throw is counted —errors++plus aFailed to hydratewarning (protocol.ts:11789) — so the boot leg is at least honest about it; the save leg is not.Dispositions worth pricing (no recommendation forced)
own, so the ownership rule is never reached and both layers coexist. This subsumes the finding but is the same contributor-model contract change Registry heal restores the metadata-map view but never re-registers a packaged contributor definition an overlay save replaced #6853 is escalating — see that card's option set.Related
MetadataFacade.register('object', …)writes where neither of its own object reads look #6725 (MetadataFacade.register('object', …)writing where its own reads do not look)objectis still served bySchemaRegistry.getObject— the registry heal reaches themetadatamap but neverobjectContributors#6808 / PR fix(objectql): deleting anobjectreally unregisters it — a name-addressedSchemaRegistry.unregisterObject(#6808) #6818, fix(objectql,metadata-protocol): a deleted runtime-created overlay leaves the registry, so list/get/dispatch agree (#5079) #6687, ADR-0029, ADR-0005