Skip to content

fix(metadata-protocol): an object extension reaches the by-name /meta read, not just the list (#7556) - #8015

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-7556-object-extension-overlay-agreement
Aug 12, 2026
Merged

fix(metadata-protocol): an object extension reaches the by-name /meta read, not just the list (#7556)#8015
huangyiirene merged 2 commits into
mainfrom
claude/issue-7556-object-extension-overlay-agreement

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#7556

⚠️PR opened by the domain:metadata PM seat on the implementing dev's behalf — the objectstack-ai org 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.ts was 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/object composes 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 metadataservice 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 (artifactSourceobjectstack 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.

And why it survived so long: an in-process dev boot was immune, because ObjectQL's bridgeObjectsToMetadataService seeds that service from registry.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 extend contributors are folded onto 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 is the registry's own.foldObjectExtendersOnto reuses the same private fold that resolveObject and resolveOwnerLayer apply, 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 overlay layer is deliberately left alone: it reports what a tenant customised, and a code-declared extension is not that.

Deployments with no objectExtensions see 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 by-name read and the list read are both measured off real handlers over a real protocol over a real registry, across four hosts that genuinely differ — artifact-ingested, bridged in-process, no metadata service, and an object nothing extends — plus an anti-vacuity case pinning that those hosts ARE discriminated.

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.

⚠️Cross-seat note for domain:engine-core (#6019): the objectql change is packages/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.

⚠️ The packages/rest file is a test only; no rest source changed, so this stays domain:metadata rather than re-routing to domain:cli.


Generated by Claude Code

… 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
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 11:30am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/objectql.

16 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-protocol, @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx(via packages/objectql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/metadata-protocol, @objectstack/objectql)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx(via packages/objectql)
  • content/docs/plugins/index.mdx(via @objectstack/objectql)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/metadata-protocol)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql)

2 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql)
  • content/docs/releases/v9.mdx(via @objectstack/metadata-protocol)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 12, 2026
@huangyiirene
huangyiirene marked this pull request as ready for review August 12, 2026 11:49
@huangyiirene
huangyiirene added this pull request to the merge queueAug 12, 2026
@huangyiireneClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — flipped ready, auto-merge SQUASH armed @ 67f9975

All 26 jobs concluded before arming: 24 success, 2 skipped (Build Docs, Console Pin Gate — neither applies), zero failures, TypeScript Type Check green at 11:41:42Z. Read per-job, not off an aggregate.

The dev could not open this PR or post its report — GitHub write is 403 in these containers (the objectstack-ai org has no Claude GitHub App connection). Body above is its own changeset; its report will be appended when relayed.

Why this one is worth reading

This card was deferred three times, each time correctly, because protocol.ts was held by another editor. It landed only once the maintainer authorised the mutually-exclusive-region exemption. It repaid the wait.

It answered the question rather than assuming the card's framing. The card read as "the by-name route is missing fields", and the dispatch demanded that be settled by measurement because if the list route were the wrong one, the fix points the other way. Measured: the list route was right. The by-name read consults the metadataservice, and an artifactSource boot ingests objects and objectExtensions as separate collections, so the service's copy has no extender in it.

And it explained why the bug survived: in-process dev boots seed that service from registry.getAllObjects() — bodies already folded — so the boot path most tests use is structurally blind to this defect. The dogfood proof boots the artifact path on purpose. Identifying that the standard harness cannot see the bug, and building one that can, is the load-bearing part of the verification.

⭐⭐ The pin is an AGREEMENT pin, which was this dispatch's central instruction — "the by-name route returns the extension fields" passes again the day someone special-cases that route, which is the same defect one layer over. Instead: both producers measured in one test, across four genuinely different hosts (artifact-ingested, bridged in-process, no metadata service, an object nothing extends), plus an anti-vacuity case pinning that those hosts are actually discriminated.

The fold is the registry's own.foldObjectExtendersOnto reuses the private fold resolveObject and resolveOwnerLayer already apply, rather than growing a second copy that could drift — which is exactly what a "just make the route return the fields" patch would have created. The overlay layer is deliberately left alone, with a reason: it reports what a tenant customised, and a code-declared extension is not that.

Byte-identical payload where nothing extends — a hard review criterion, met.

Cross-seat

⚠️domain:engine-core (#6019): packages/objectql/src/registry.ts gains one additive public API (SchemaRegistry.foldObjectExtendersOnto, hence the minor), wrapping an existing private fold with no behaviour change to existing callers. Flagged rather than assumed silent.

⚠️ The packages/rest file is a test only — no rest source changed, so this stays domain:metadata rather than re-routing to domain:cli.


Generated by Claude Code

Merged via the queue into main with commit e3c8ed0Aug 12, 2026
27 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-7556-object-extension-overlay-agreement branch August 12, 2026 12:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@huangyiirene@claude