Skip to content

Object-extension fold leaks the extension's label onto ?layers=true only — the two reads #7556 reconciled on FIELDS now disagree on LABEL #8037

Closed

Description

@baozhoutao

Found while re-testing #7556 on a baseline that contains its fix. #7556 / PR #8015 is genuinely fixed — do not revert it. GET /meta/object/:name now serves all three account.extension.ts overlay fields, ?layers=true carries them on both code and effective, the edit form renders real controls for them, and a PATCH round-trip persists. Every acceptance clause of platform-core.metadata-registry-serving passes. This is a smaller, adjacent inconsistency the same fold introduces.

Symptom

One object, three metadata reads, two different labels:

read label
GET /api/v1/meta/object (list) Account
GET /api/v1/meta/object/showcase_account Account
GET /api/v1/meta/object/showcase_account?layers=true → code.label / effective.label Account (Success Overlay)

Reproduced 3/3 on baseline 279ee48a (git merge-base --is-ancestor e3c8ed0f8675 HEAD → in build).

Mechanism

examples/app-showcase/src/data/extensions/account.extension.ts:15 declares a label alongside its additive fields:

export const AccountExtension = defineObjectExtension({
  label: 'Account (Success Overlay)',
  ...
  priority: 210,
});

mergeObjectDefinitions (packages/objectql/src/registry.ts:137-140) merges fields/validations/indexes additively but overrides the scalars last-writer-wins:

// Override scalar props (last writer wins)
if (extension.label !== undefined) merged.label = extension.label;
if (extension.pluralLabel !== undefined) merged.pluralLabel = extension.pluralLabel;
if (extension.description !== undefined) merged.description = extension.description;

Before #8015 the code layer served the raw MetadataService body straight from the artifact (objects.showcase_account.label = 'Account', 12 fields) and never went through the fold, so the overlay label had nowhere to surface. Now the fold runs on that layer — which is exactly what fixed the fields — and it carries the scalar override with it, while the list and by-name reads still report the base label.

Why the existing pins can't see it

Both agreement pins added with the fix compare field names only — packages/rest/src/meta-object-extension-agreement.test.ts and packages/qa/dogfood/test/showcase-object-extension-meta-read.dogfood.test.ts reconcile via fieldNamesOf. Both are green (6/6 and 5/5 on this baseline). A scalar divergence is structurally outside what they assert.

Reproduction

  1. Boot the showcase: node packages/cli/bin/run.js dev -a dist/objectstack.json --seed-admin -d file:/tmp/x.db -p <port>
  2. Sign in as admin@objectos.ai (Origin: http://localhost:<port>), take the bearer from set-auth-token.
  3. GET /api/v1/meta/object/showcase_account → item.label is Account.
  4. GET /api/v1/meta/object/showcase_account?layers=true → code.label and effective.label are Account (Success Overlay).

Expected one object, one label, on every read. Actual the layered read disagrees with the other two.

Which side is right is a decision, not a given

Both readings are defensible and the fix should pick one deliberately:

  • The overlay label should win everywhere — defineObjectExtension accepts label, mergeObjectDefinitions documents last-writer-wins, and priority 210 says the extension outranks the base. Then the list and by-name reads are the ones under-reporting.
  • The base label should win everywhere — an additive overlay arguably has no business renaming the object it extends, and the showcase's (Success Overlay) suffix is a fixture affordance, not a product intent. Then the fold should not carry scalars, or defineObjectExtension should refuse a label.

Whichever way it goes, the gate that keeps it from drifting again is extending the two agreement pins past fieldNamesOf to the scalar triple (label, pluralLabel, description) across all three reads.

Scope

Extension-independent objects are unaffected: across all 96 registered objects, no object other than showcase_account gains an extension field, and showcase_task (unextended) serves 24 fields with none of the overlay fields. The fold is also not double-applying — validations stayed at 5 with zero duplicates across three consecutive reads, so the non-idempotency PR #8015 warns about is not firing here.

Source

Extracted from the checklist retest of platform-core.metadata-registry-serving (framework 279ee48a, console 6314e87f2d49).

Activity

  1. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Triage: → domain:metadata, queued. The divergence surfaces across the meta reads served by packages/metadata-protocol; the fold itself is mergeObjectDefinitions in packages/objectql/src/registry.ts:137-140. Re-laned from domain:metadata-protocol (not a lane-table label). Type intent: Bug (type field not set — list_issue_types 403 for this credential).

    PM ruling premise (mechanism-assumption grade — verify before coding, falsify loudly): the existing code standard decides the "which side is right" question, so this is queued rather than escalated. defineObjectExtension accepts label, mergeObjectDefinitions documents scalar override as last-writer-wins, and the extension carries priority: 210 — declared semantics say the overlay label wins everywhere, and the list/by-name reads are the under-reporting side. Re-check command: git grep -n "last writer wins" packages/objectql/src/registry.ts against origin/main. If you find an ADR or ruling that additive overlays must not rename their base object, stop and report the fork — do not silently implement the other side.

    Deliverable includes the drift gate the filer names: extend both agreement pins past fieldNamesOf to the scalar triple (label, pluralLabel, description) across all three reads.

    Serial constraints: same serving family as in-flight #8027, queued #8031 and #8038 (this round) — same-package serial, no two in one batch without confirmed file-surface disjointness.

    Size/model suggestion: S–M, opus.


    Generated by Claude Code

  2. self-assigned this
    on Aug 12, 2026
  3. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    Collaborator

    Claim — domain:metadata PM seat (#6367). PM session session_01AmES43BMDg4bPrxTdi5q7t, branch claude/issue-8037-extension-label-leak. pm:queue → pm:dispatched.

    ⚠️ This is the THIRD defect in the same family, and the pattern is now the point

    #7556 (PR #8015) made the by-name /meta read fold objectExtensions contributors onto the MetadataService body — reconciling the two reads on fields. Since then:

    # what the fold got wrong found by
    #8027 → PR #8045 (landed fa6dd59) validations and indexes duplicated — the fold is not idempotent, and two shipped call sites violated its documented precondition the next card
    #8037 (this card) the extension's label leaks onto ?layers=true only — the two reads now disagree on label the next card

    ⇒ #8015's pin compared FIELD NAMES. It was structurally blind to every other property of a merged object. I accepted that PR and called the pin excellent — it was, for the dimension it tested — and I did not ask what it did not cover. That gap has now produced two follow-on defects.

    Standing review criterion, recorded on #8045 and repeated here: when a merge reconciles a structure, a pin over one of its keys proves nothing about the others. mergeObjectDefinitions treats fields (key-keyed spread, idempotent), validations/indexes (concatenated), and scalars like label (last-writer-wins) three different ways — so a fold has three distinct failure modes and a field-name pin sees none of them but its own.

    ⭐ What I want from this card beyond the label fix

    ⚠️ Do not fix label and stop. mergeObjectDefinitions has a small, enumerable set of merge behaviours. Enumerate every property it touches, classify each by merge kind, and say for each whether the two reads agree. If a fourth divergence exists, I would rather find it here than in a fourth card.

    That enumeration is the deliverable I care most about; the label fix is the easy half.

    Region and concurrency

    ⚠️ The maintainer has raised concurrency to 8, so packages/metadata-protocol/src/protocol.ts and packages/objectql/src/registry.ts will have several concurrent editors. Every brief carries the same discipline: declare your region, anchor on function names (never line numbers), merge origin/main before opening the PR, and merge again after each sibling lands. A conflict is my cost, not yours — resolve it and say so.

    ⚠️ protocol.ts line numbers have gone stale three times in one day; this file has taken many merges today.

    Brief carried inline in the dev's prompt — cloud containers still get 403 on every GitHub API call (measured three times since the App was connected), so a brief that lives only here is unreadable to them.


    Generated by Claude Code

  4. removed their assignment
    on Aug 13, 2026
  5. huangyiirene commented on Aug 13, 2026

    @huangyiirene
    Collaborator

    Part of closure — PR #8258 merged as 3a27c46edb, and this card deliberately stays open.

    Re-graded pm:dispatched → needs-user-decision and unassigned, in one label write, because what remains is a maintainer call rather than dispatchable work.

    Delivered

    The property-class enumeration (test-only, 708 insertions, no source change): packages/rest/src/meta-object-extension-property-classes.test.ts plus a scalar-divergence dogfood test. It pins every property class mergeObjectDefinitions touches, so the next member of this family fails on a unit instead of arriving as a fourth card.

    That was worth landing on its own. This family exists precisely because each pin was blind to the next defect: #7556/#8015 reconciled fields; #8027/#8045 then found validations/indexes duplicated — invisible to #8015's pin because that pin compares field names and the field spread is idempotent; #8037 arrived next about label. Three different merge semantics — spread (idempotent), concatenation (not), last-writer-wins — and a pin over one proves nothing about the others.

    Remaining — and the premise correction

    ⚠️ The label fix is NOT delivered, and the card's premise moved. The dev's measurement relocates the defect: the divergence is in i18n, not in the fold. So this card was not satisfied; it was corrected.

    ⛔ Blocking fork (maintainer's call, why this is needs-user-decision):

    Should the catalog lose to an explicit override, or should the untranslated diagnostic be reconciled with the translated reads?

    That is i18n precedence — product semantics, not an implementation detail. It was carried only in PR #8258's body until now; recording it here so the decision has its natural anchor. ⛔ Not dispatchable until ruled.

    Owner

    domain:metadata seat (#6367). Returns to pm:queue the moment the fork is ruled.

    ⚠️ Related work that may absorb part of this

    #8268 was filed minutes ago from the #8038 dev's rescued handover: GET /meta/object/:name drops nameField for artifact-ingested objects. Its own caveat is directly relevant here — nameField is a scalar property resolved last-writer-wins, so it may already fall inside this card's enumeration. Whoever picks either up should read both first.

    #8268 also argues the sharper framing: three materialization stamps have now diverged on one seam one at a time (#6562 injected system columns → #8038 __search → #8268 nameField), and the class is worth ruling on rather than the third instance. This card is the same lesson from the merge side.


    Generated by Claude Code

  6. huangyiirene commented on Aug 13, 2026

    @huangyiirene
    Collaborator

    Dev report relayed by the PM (its container is 403 on the whole GitHub API). ⚠️ It corrects this card's premise, and it corrects my own review of PR #8258 — I under-read what was delivered.

    ⚠️ First, a disclosure about the merge

    PR #8258's body, as the dev wrote it, carried "⛔ do not flip ready / no auto-merge". I never saw that text. The dev could not open the PR, so I opened #8258 myself from its pushed branch with a body I wrote, reviewed the diff, and merged it as 3a27c46edb.

    On the merits I still think the merge is right — test-only, no source change, Part of so it closes nothing, CI fully green, and the two it.fails cases are correct as shipped. But the instruction existed and I acted against it without knowing, so it goes on the record rather than being quietly absorbed. ⇒ Lesson for this seat: when opening a PR on a 403-blocked dev's behalf, ask for its intended PR body before arming anything — the body is where a dev puts its merge conditions, and reconstructing it loses them.

    ⭐ The headline: this is NOT a fold defect

    mergeObjectDefinitions names exactly six keys and copies nothing else — confirmed from both sides (the function body, and ObjectExtensionSchema's own guidance). The fold has zero divergences across all six. Measured, not argued:

    [BYNAME] fold(showcase_account) baseLabel="Account" -> outLabel="Account (Success Overlay)"
    [BYNAME] served label = "Account"          ← discarded downstream
    [LAYERS] fold(...)                          -> outLabel="Account (Success Overlay)"
    [LAYERS] served effective="Account (Success Overlay)"
    

    The fold returns the same body to both reads. translateObject (packages/spec/src/system/i18n-resolver.ts) then resolves each scalar as catalog ?? document; the showcase catalog declares objects.showcase_account.label = "Account"; list and by-name are translated, ?layers=true is deliberately not. Hence "onto ?layers=true only" — the symptom pointed at the one layer that is behaving.

    ⇒ A fix applied to the fold would be applied to the only component that is correct.

    ⛔ And the milder half was the one we were looking at

    The same mechanism defeats the tenant's own Studio rename (PUT, 200 OK):

    Surface Label
    list / by-name Account
    layers.overlay Customer ← what the tenant saved
    layers.effective Account (Success Overlay)

    The rename reaches neither read a writable form derives from. Filed as #8284, carrying the design ruling. That is a materially more severe defect than the extension-label leak this card was opened for.

    ⛔ Why the "easy half" was not shipped — and this is the part I most want on the record

    I described the label fix as the deliverable and the enumeration as the supporting work. The dev reverse-verified per arm and arm B — the card's proposed "fold drops scalars" — turns exactly ONE new case red (the anti-vacuity one) and is invisible to both existing pins (#8015's agreement pin and #8045's fold pin stay green).

    So the "easy half" would have: deleted a documented ObjectExtensionSchema feature, passed every pre-existing pin, made the symptom disappear, and left the tenant-rename defect untouched. A regression that looks like a fix and that our existing tests could not see. The dev shipped no behavioural change and asked for a ruling instead. That was the right call and it is the opposite of what my dispatch framing invited.

    What did land (3a27c46edb)

    Test-only, no source files changed: the six-property enumeration across twelve host shapes, asserted against the registry's resolved schema (ADR-0029 D9.2) — never against another route, because both prior defects had the two routes agreeing with each other on a body that was already wrong. Plus the dogfood repro with two it.fails cases. Byte-identity preserved for objects with no contributor and no overlay (toBe reference-identity asserted); #8045's idempotency and the layers.overlay boundary untouched.

    Gates: metadata-protocol 1094 ✅ · objectql 3390 ✅ · rest 1747 ✅ · runtime 2195 ✅ · dogfood 676 ✅ + 2 expected-fail (its own) · check:type-check-debt exit 0 on a built closure, no ledger entry raised, no --lower on anyone's lines.

    This card's status

    Stays needs-user-decision, unassigned. The fork now lives properly on #8284 with the full measurement and the arm-B evidence. This card remains the label-shaped entry point; ⛔ do not dispatch a fold-side fix against it.

    ⚠️ pluralLabel and description carry the identical exposure and are simply not triggered on main today (no shipped extension overrides pluralLabel; showcase_account's catalog has no description key). Any ruling should cover all three scalars, not label alone.


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions