Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): an object extension reaches the by-name /meta read, not just the list (#7556) - #8015
Conversation
… read, not just the list (#7556) `GET /meta/object` composes its objects from `SchemaRegistry.listItems('object')`, whose object branch resolves through `resolveObject` — a base layer with its `extend` contributors folded on (ADR-0029 D9.2). The by-name read consults the `metadata` SERVICE first, because that copy is the HMR-fresh one, and served whatever it returned. For every other metadata type the two agree. For `object` they did not: a deployment booted from a compiled artifact (`artifactSource` — `objectstack serve`, sealed runtimes, the cloud) ingests `objects` and `objectExtensions` as SEPARATE collections, so the service's copy is the owner's declaration with no extender in it. An in-process dev boot was immune, because ObjectQL's `bridgeObjectsToMetadataService` seeds that service from `registry.getAllObjects()` — bodies that are already folded. Measured on the showcase, whose account extension contributes three fields: they were served by the list read and persisted through the data API round-trip, and were absent from the by-name read and from BOTH layers of `?layers=true`. The edit and new forms derive from the by-name response, so three fields a client could read and write through the API could never be set in the UI. The fold is applied to the MetadataService body at the two places that adopt one: the by-name read and the `code` layer of the layered view (`effective` is `overlay ?? code`, so an object with no tenant overlay is corrected on both layers by that single fold). The fold itself is the registry's own — `foldObjectExtendersOnto` reuses the same private fold `resolveObject` and `resolveOwnerLayer` apply, rather than growing a second copy that could drift. The `overlay` layer is left alone: it reports what a tenant customised, and a code-declared extension is not that. An object nothing extends is returned untouched. Pinned as AGREEMENT rather than presence: both reads are measured off real handlers over a real protocol over a real registry, across four hosts that genuinely differ, plus an anti-vacuity case pinning that those hosts ARE discriminated. Asserting "the route returns the extension fields" would pass again the day someone special-cased that route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QjPSKbHaJ7aVtRPJeRgaf1
…ect-extension-overlay-agreement
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 16 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
huangyiirene
commented
Aug 12, 2026
ACCEPT — flipped ready, auto-merge SQUASH armed @ |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7556
domain:metadataPM seat on the implementing dev's behalf — theobjectstack-aiorg has no Claude GitHub App connection, so cloud dev containers can push but cannot reach the GitHub API (5 of 5 dispatches this shift). Body is the dev's own changeset; its full report is owed and will be appended.This card was deferred three times before dispatch, each time because
protocol.tswas held by another editor. It landed under the maintainer's mutually-exclusive-region exemption.⭐ The open question is answered: the LIST route was right
The card was filed as "the by-name route is missing fields", but explicitly did not assume that — the dispatch demanded it be settled by measurement, because if the list route were the wrong one the fix would point the other way. Measured:
GET /meta/objectcomposes fromSchemaRegistry.listItems('object'), whose object branch resolves throughresolveObject— a base layer with itsextendcontributors folded on (ADR-0029 D9.2). The by-name read consults themetadataservice first, because that copy is the HMR-fresh one, and served whatever it returned.For every other metadata type the two agree. For
objectthey did not:⭐ And why it survived so long: an in-process dev boot was immune, because ObjectQL's
bridgeObjectsToMetadataServiceseeds that service fromregistry.getAllObjects()— bodies that are already folded. The bug is invisible on the boot path most tests use.Why it was not cosmetic
Measured on the showcase, whose account extension contributes three fields: they were served by the list read and persisted through the data API round-trip, and were absent from the by-name read and from both layers of
?layers=true. The edit and new forms derive from the by-name response — so three fields a client could read and write through the API could never be set in the UI.The change
The registry's
extendcontributors are folded onto the MetadataService body at the two places that adopt one: the by-name read, and thecodelayer of the layered view (effectiveisoverlay ?? code, so an object with no tenant overlay is corrected on both layers by that single fold).⭐ The fold is the registry's own.
foldObjectExtendersOntoreuses the same private fold thatresolveObjectandresolveOwnerLayerapply, rather than growing a second copy that could drift — which is the failure mode a "just make the route return the fields" patch would have created.The
overlaylayer is deliberately left alone: it reports what a tenant customised, and a code-declared extension is not that.Deployments with no
objectExtensionssee a byte-identical payload — the fold applies only to a name something actually extends. That was a hard review criterion, not an afterthought.⭐⭐ Pinned as AGREEMENT, not presence
The dispatch's central instruction, because "the by-name route returns the extension fields" passes again the day someone special-cases that route — the same defect one layer over.
packages/rest/src/meta-object-extension-agreement.test.ts:The end-to-end proof is
packages/qa/dogfood/test/showcase-object-extension-meta-read.dogfood.test.ts, which boots the artifact path on purpose — the shared in-process harness cannot see this bug. Identifying that the standard harness is structurally blind to the defect, and building one that isn't, is the load-bearing part of this verification.Changeset levels, and a cross-seat note
@objectstack/metadata-protocol: patch — it restores the contract the route was already specified to answer; the divergence was the defect.@objectstack/objectql: minor — it gains one additive public API,SchemaRegistry.foldObjectExtendersOnto.domain:engine-core(#6019): theobjectqlchange ispackages/objectql/src/registry.ts(+52), purely additive — one exported method wrapping an existing private fold, no behaviour change to existing callers. Flagging it rather than assuming silence is consent.packages/restfile is a test only; norestsource changed, so this staysdomain:metadatarather than re-routing todomain:cli.Generated by Claude Code