Skip to content

Object-extension overlay fields are missing from GET /meta/object/:name (present in the list route) — the overlay's fields can never be set through the UI #7556

Description

@huangyiirene

Symptom

The showcase account extension contributes 3 fields. Where they show up:

Surface Extension fields present?
GET /meta/object (list) yes
GET /meta/object/showcase_account (by name) no
GET /meta/object/showcase_account?layers=true no — absent from both layers
GET /api/v1/data/... round-trip yes — they are real columns, values persist
Read-only record detail page yes — they render

Expected: the by-name response composes the same contributors the list response does, so an object's resolved schema is identical whichever way it is fetched.

Consequence, and why this is not cosmetic: the edit and new forms derive from the by-name response, so the three overlay fields are absent from every writable form. They can be read, and they can be written through the data API, but they can never be set through the UI.

Root cause

Not located by the report. What the evidence constrains:

  • The contribution is registered and is materialised — the columns exist and round-trip through /api/v1/data, and the list route composes them.
  • So the fault is on the by-name read path, not in registration: the by-name handler resolves the object from a source that omits extension contributors, and the ?layers=true projection omits them too (they are in neither layer, not merely folded into the wrong one).

Whether the omission sits in the REST /meta/object/:name handler or in the metadata read path it calls needs to be measured; the ?layers=true behaviour points at the layer-resolution side rather than at REST plumbing, which is why this card is filed domain:metadata — re-route if the fix turns out to land in packages/rest.

Timing note for whoever picks this up: #7306 (ADR-0029 D9, "register a tenant object overlay as its own contributor layer") merged 2026-08-10T09:34Z, around when this run's framework commit was cut, and it changes layer classification and the getArtifactItem read. It targets tenant (_provenance:'org') overlays rather than a code-declared extension, so it is not expected to fix this — but re-measure on current main before starting.

Reproduction

  1. Boot the showcase (it declares the account extension with its 3 extra fields).
  2. GET /meta/object — the extension fields are in the listed showcase_account entry.
  3. GET /meta/object/showcase_account — they are gone.
  4. GET /meta/object/showcase_account?layers=true — still gone, in both layers.
  5. Open the record's edit/new form in the console — no inputs for those 3 fields; open the read-only detail page — they render, with values written through /api/v1/data.

Source

Extracted from the QA run #7514 (framework a86db17, console 09987b68).

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Deferred to a later round by the domain:metadata seat — batch-independence, not priority. Recording the reason and what is now known, because a deferred card looks dispatchable to every later sweep including another PM's, and whatever was learned about it is worthless until it is written here.

    Why deferred: #7559 (commit-revert's version-lookup key) is dispatched this round in the same package, packages/metadata-protocol. Same-package cards serialize across rounds; this one rides in the next.

    Two things worth knowing before anyone picks this up:

    1. Branch from current main, not from anything cached. Three cards landed on packages/metadata-protocol/src/protocol.ts today — #7532 (dotted projection refused at the ingress gate), #7534 (unknown fields gated on the explicit filter axes), #7541 (the capabilities.search predicate). The file has moved substantially since this card was filed.

    2. The pin this card needs is an AGREEMENT pin, and that is not the obvious one. The defect here is that two routes answer the same question differently — overlay fields present on the list route, absent from GET /meta/object/:name. The tempting assertion is "the single-object route returns the overlay fields", and it is the wrong one: it passes again the day someone hardcodes or special-cases that route, which is the same defect class one layer over. #7541 hit exactly this and the shape that worked was to measure both producers in one test and assert they agree, across hosts that genuinely differ, plus an anti-vacuity case proving the hosts are discriminated. That pattern transfers directly — see packages/rest/src/discovery-search-capability-agreement.test.ts for a worked example in this repo.

    Also worth deciding rather than assuming when it is dispatched: which route is right. The card reads as "the single route is missing fields", but the honest question is whether object-extension overlays belong in that payload at all — if the list route is the one that is wrong, the fix points the other way, and that should be settled by measurement before implementation.


    Generated by Claude Code

  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Deferred a second time — different reason, recorded so this does not read as neglect.

    The first deferral was #7559 holding packages/metadata-protocol. That landed (52d1a7d81), which freed the slot — but this round it went to #7674 instead: the ADR-0090 D11 OWD authoring gate turns out never to run on any host-config deployment, i.e. a declared gate that executes nowhere self-hosted, and that outranks a missing-fields read defect on severity.

    Five cards are now queued on packages/metadata-protocol behind #7674 — this one, #7654, #7682, #7643, #7656. That is a genuine queue, not a backlog of forgotten cards, and it is worth saying plainly that the strict same-package serialization rule is what is producing it. If that becomes the throughput bottleneck rather than a safety measure, the maintainer's mutually-exclusive-region exemption (declare regions at claim time, merge main before opening the PR, merge again after each sibling lands, let the queue arbitrate) is the sanctioned way to run several of them concurrently — but that is the maintainer's call to make explicitly, not something a seat adopts on its own.

    The trap recorded in my earlier comment still stands and is still the important part: the pin this card needs is an AGREEMENT pin, not "the single-object route returns the overlay fields" — that assertion passes again the day someone special-cases that route. The worked example in this repo is now packages/rest/src/discovery-search-capability-agreement.test.ts (landed with #7541). And the open question stands too: decide by measurement which route is right, since "the list route is correct and the single route is missing fields" is the card's assumption rather than an established fact.

    One thing that has changed since the first note and is worth re-checking before dispatch: protocol.ts has taken four merges today (#7532, #7534, #7541, #7559). Branch from current main, and expect the surrounding code to have moved.


    Generated by Claude Code

  3. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Deferred a third time — and this round I measured the queue instead of just recording the collision, because three deferrals is the point where "not yet" needs evidence.

    Why deferred again: packages/metadata-protocol/src/protocol.ts is held by #7779 (dispatched 14:54Z — listCommits missing env-wide sys_metadata_commit rows, the sibling of the #7705 fix that landed as ecd83fd50). Same-package serialization; this card rides the next free slot in that lane.

    The measurement, because it changes the picture. A slot freed when PR #7764 merged (bdd0120bc). I went looking for any dispatchable domain:metadata card that would not collide with the two in-flight lanes, and read six candidates. All six collided:

    card lands in held by
    #7556 (this one) metadata-protocol/src/protocol.ts #7779
    #7743 — field overlay lock unenforced protocol.ts:8678 (isArtifactBacked) #7779
    #7736 — view container never expanded on the write door protocol.ts (+ metadata/src/plugin.ts) #7779
    #7774 — list merge dedups without a locale discriminator protocol.ts:988/4363/4377 #7779
    #7309 — 16 actions declaring confirmText + params packages/platform-objects #7728
    #7544 — dead JWKS nav entry platform-objects/src/apps + /identity #7728

    So the rolling window is running at 2 of 3, and the binding constraint is not supply — the queue holds 30 cards — it is that this seat's queue is concentrated in two places, one of which is the single hottest file in the repo (protocol.ts has taken seven merges today).

    That is the same bottleneck I recorded on this card at 11:14Z, now with numbers behind it. The sanctioned remedy is the maintainer's mutually-exclusive-region exemption, and it stays the maintainer's explicit call — ⛔ this seat does not adopt it unilaterally, which is why the deferral stands rather than being reasoned around. I've put the question to them with this measurement attached.

    Nothing about the card's own content has changed, and the trap still stands — repeating it because it is the part most likely to be lost across three deferrals: the pin this card needs is an AGREEMENT pin. "The single-object route returns the overlay fields" is the wrong assertion; it passes again the day someone special-cases that route, which is the same defect class one layer over. Measure both producers in one test and assert they agree, with an anti-vacuity case proving the hosts are genuinely discriminated. Worked example: packages/rest/src/discovery-search-capability-agreement.test.ts.

    And the open question still needs settling by measurement, not assumption: which route is right? The card reads as "the single route is missing fields", but whether object-extension overlays belong in that payload at all is not established. If the list route is the wrong one, the fix points the other way.

    Branch from current main when this is finally dispatched — protocol.ts has moved substantially since the last note.


    Generated by Claude Code

  4. self-assigned this
    on Aug 12, 2026
  5. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    Dispatched on the fourth look — and the deferral that held it is now formally lifted

    Claim: domain:metadata PM seat (#6367), session session_01AmES43BMDg4bPrxTdi5q7t, branch claude/issue-7556-object-extension-overlay-agreement. Read all three deferral comments before claiming; no competing claim. pm:queue → pm:dispatched.

    The maintainer made the call this card has been waiting on

    My predecessor deferred this three times and, at the third, did the right thing: they measured the bottleneck rather than just re-recording the collision (6 of 6 candidate cards colliding, the lane's queue concentrated in the repo's hottest file), named the sanctioned remedy — the mutually-exclusive-region exemption — and then explicitly declined to adopt it:

    that is the maintainer's call to make explicitly, not something a seat adopts on its own.

    That was correct restraint, and the call has now been made: the maintainer instructed this seat to accelerate dispatch. So the exemption is in force, and I am adopting their protocol rather than my own looser improvisation of it, because theirs is better specified:

    1. Declare regions at claim time (below),
    2. merge main before opening the PR,
    3. merge again after each sibling lands,
    4. let the merge queue arbitrate.

    ⚠️ Correction to my own posture, stated rather than quietly fixed: earlier today I relaxed this lane's serialization from file-level to function-level on my own reading, before finding this comment. The relaxation stands — it is what the maintainer authorized — but the discipline attached to it is now the four-step protocol above, not the two mitigations I improvised.

    Regions declared for this card: the GET /meta/object/:name by-name read path and its layer resolution in packages/metadata-protocol/src/protocol.ts. ⚠️ Concurrently held in that file: #7780 → deletePackage (package-uninstall tenancy predicate). Non-adjacent. Two concurrent editors is this lane's cap; I am not adding a third.

    ⭐ The trap, carried forward because it is the part most likely to be lost across four deferrals

    The pin this card needs is an AGREEMENT pin.

    "The single-object route returns the overlay fields" is the wrong assertion — it passes again the day someone hardcodes or special-cases that route, which is the same defect class one layer over. The shape that works is to measure both producers in one test and assert they agree, across hosts that genuinely differ, plus an anti-vacuity case proving the hosts are discriminated. Worked example in this repo: packages/rest/src/discovery-search-capability-agreement.test.ts (landed with #7541, which hit this exact trap).

    ⭐ And the open question is genuinely open — ⛔ do not assume the card's framing

    Which route is right?

    The card reads as "the single route is missing fields", but whether object-extension overlays belong in that payload at all is not established. If the list route is the wrong one, the fix points the other way. That must be settled by measurement before implementation, and the dev is told a falsified card premise is a success outcome on this seat, not rework.

    ⚠️ Re-measure on current main before starting — the timing note about #7306 (ADR-0029 D9) is from 2026-08-10 and protocol.ts has taken many merges since. If the fix turns out to land in packages/rest, that is a re-route to domain:cli, reported not performed.

    Brief carried inline in the dev's prompt, not only here.


    Generated by Claude Code

  6. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    ⚠️ Posted by the PM seat on the dev's behalf — GitHub API 403 both directions: "GitHub access is not enabled for this session. An org admin must connect the Claude GitHub App for this organization." It tried add_repo with access:"push" (re-attached the repo, did not unlock the API), made exactly one PR-create attempt (403, nothing created), and stopped. gh is not installed.

    ✅ Claim check answered: the dev could not claim the issue itself and asked me to verify no sibling raced it. I claimed it at 09:5xZ (assign + claim comment 5265145293, session ID and branch named) before dispatch, and re-read all three deferral comments first. No race.

    Branch claude/issue-7556-object-extension-overlay-agreement @ 67f9975d5, merged with origin/main @ 06770c0a8. PR opened by the PM as #8015.


    §1 — Which route was right: premise_still_valid: true. The by-name route is wrong.

    Settled from the source of truth, then measured. ADR-0029 D9.2 (resolveObject) and D9.6 (resolveOwnerLayer) both define an object's resolved schema as a base layer with its extend contributors folded on. Every registry entry point honours that — getItem, listItems, getArtifactItem. The list route and the data plane read the registry. Only the by-name path dissents.

    ⭐ The re-measurement inverted the dev's first answer

    On current main the symptom does not reproduce on the default dogfood harness: by-name, code and effective all carried the 3 fields.

    Because the in-process boot lets ObjectQL's bridgeObjectsToMetadataService seed the metadata service from registry.getAllObjects() — already-folded bodies. The real repro condition is artifact-mode boot (artifactSource — objectstack serve, sealed runtimes, cloud). Confirmed in the compiled showcase artifact:

    showcase_account fields: name,industry,annual_revenue,…,billing_email
    has loyalty_tier in object: false
    objectExtensions: [{extend:"showcase_account",fields:["loyalty_tier","linkedin_url","csat_score"]}]
    

    Measured on a real artifact-mode showcase over real HTTP:

    Surface before after
    GET /meta/object ✅ all 3 ✅ all 3
    GET /meta/object/showcase_account ❌ none ✅ all 3
    ?layers=true → code ❌ none ✅ all 3
    ?layers=true → overlay null null (correct — no tenant customisation)
    ?layers=true → effective ❌ none ✅ all 3
    data round-trip ✅ 201, persists ✅ 201, persists

    Positive control: those 3 fields exist only in account.extension.ts (grep across the showcase returns nothing else), so their presence is genuine folding, not coincidence.

    §2 — The agreement pin

    packages/rest/src/meta-object-extension-agreement.test.ts asserts byName === listed (never "contains loyalty_tier"), both measured off real handlers → real protocol → real registry, across four genuinely-different hosts: artifact-ingested, bridged in-process, no metadata service, and an object nothing extends. It further pins that what they agree on is the registry's resolved schema, so they cannot drift together.

    Anti-vacuity case proves the hosts are discriminated — the artifact host's service body genuinely lacks all 3 fields while the bridged host's has them; the absent host's is empty; the unextended object genuinely serves none; and artifact.listed !== unextended.listed. If the fixture ever stopped distinguishing the hosts, this goes red (demonstrated as arm D).

    Reverse-verification, per arm

    Arm rest agreement pin dogfood repro
    A — getMetaItem fold reverted 1 red: artifact-ingested host 1 red: by-name serves same fields as list
    B — layered code fold reverted 1 red: both layers of ?layers=true 1 red: ?layers=true resolves both layers
    C — both reverted (pre-fix main) 2 red (both above) 2 red (both above)
    D — fixture stops discriminating hosts 1 red: anti-vacuity 5 pass (fixture-only arm)

    Each arm reddens exactly its own case and nothing else; the unextended-object case stays green in A–C, proving it tracks an independent signal.

    Gates (on the merged, built closure)

    check:type-check-debt exit 0 — "none above its recorded number"; no ledger entry raised, --lower never run. check:nul-bytes ✅ · check:empty-changeset ✅ · check:test-source-alias ✅ · eslint ✅. Suites: metadata-protocol 1082 ✅ · rest 1537 ✅ · runtime 2158 ✅ · dogfood 609 ✅ (96 files).

    ⭐ A self-correction worth reading

    My first two dogfood full-suite runs reported 2 and 11 failing files. Both were my own contamination — I had pnpm build rewriting dist/ while the suite ran, and these tests resolve siblings through dist. A clean serial run is green (exit 0). No pre-existing flake; I'd have mis-reported one had I not re-run.

    That is exactly the discipline that keeps a flake ledger meaningful. A fabricated "pre-existing flake" entry is worse than no entry, because the next agent trusts it.

    Out-of-scope finding — filed as #8025

    A tenant sys_metadata overlay row drops every objectExtensions field from the resolved schema on both routes, and effective contradicts code in the same ?layers=true response.

    ⚠️ This is the case the agreement pin deliberately cannot catch — both routes agree, on a body that has lost the fields. The dev stated that limitation itself rather than letting the pin imply coverage it does not have. Measured, distinct from this card, and not caused by this fix.


    Generated by Claude Code

  7. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    ⚠️ Correction: the out-of-scope card is #8027, not #8025.

    Second time today. Earlier I cited #8017 for a card that landed as #8018. Twice is a pattern, and I now know the mechanical cause: I was issuing the create call and the comment that cites it in the same batch, so the number in the comment was a prediction, not a read-back.

    ⇒ Rule, recorded on the seat sticker: never batch an issue-create with the comment that cites it. Create first, read the number back from the response, then write the comment. This seat enforces "file first, read the number back, then cite it" on every dispatch brief it writes; batching the two calls quietly violates it while appearing to comply.

    #8027 — "A tenant object overlay drops every objectExtensions field from the resolved schema, and effective contradicts code in the same response". Everything the report says about it stands, against that number: measured, distinct from this card, not caused by PR #8015's fix, and the case this card's agreement pin deliberately cannot catch because both routes agree on a body that has already lost the fields.

    #8025 is somebody else's issue; nothing in the report refers to it.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions