Repository navigation
Object-extension fold leaks the extension's label onto ?layers=true only — the two reads #7556 reconciled on FIELDS now disagree on LABEL #8037
Description
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
- Boot the showcase:
node packages/cli/bin/run.js dev -a dist/objectstack.json --seed-admin -d file:/tmp/x.db -p <port> - Sign in as
admin@objectos.ai(Origin: http://localhost:<port>), take the bearer fromset-auth-token. GET /api/v1/meta/object/showcase_account→item.labelisAccount.GET /api/v1/meta/object/showcase_account?layers=true→code.labelandeffective.labelareAccount (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 —
defineObjectExtensionacceptslabel,mergeObjectDefinitionsdocuments 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, ordefineObjectExtensionshould refuse alabel.
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
hotlong commentedon Aug 12, 2026
Triage: →
domain:metadata, queued. The divergence surfaces across the meta reads served bypackages/metadata-protocol; the fold itself ismergeObjectDefinitionsinpackages/objectql/src/registry.ts:137-140. Re-laned fromdomain:metadata-protocol(not a lane-table label). Type intent: Bug (typefield not set —list_issue_types403 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.
defineObjectExtensionacceptslabel,mergeObjectDefinitionsdocuments scalar override as last-writer-wins, and the extension carriespriority: 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.tsagainstorigin/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
fieldNamesOfto 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
huangyiirene commentedon Aug 12, 2026
Claim —
domain:metadataPM seat (#6367). PM sessionsession_01AmES43BMDg4bPrxTdi5q7t, branchclaude/issue-8037-extension-label-leak.pm:queue→pm:dispatched.#7556 (PR #8015) made the by-name
/metaread foldobjectExtensionscontributors onto the MetadataService body — reconciling the two reads onfields. Since then:fa6dd59)validationsandindexesduplicated — the fold is not idempotent, and two shipped call sites violated its documented preconditionlabelleaks onto?layers=trueonly — the two reads now disagree on label⇒ #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.
mergeObjectDefinitionstreatsfields(key-keyed spread, idempotent),validations/indexes(concatenated), and scalars likelabel(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
labeland stop.mergeObjectDefinitionshas 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
labelfix is the easy half.Region and concurrency
packages/metadata-protocol/src/protocol.tsandpackages/objectql/src/registry.tswill have several concurrent editors. Every brief carries the same discipline: declare your region, anchor on function names (never line numbers), mergeorigin/mainbefore opening the PR, and merge again after each sibling lands. A conflict is my cost, not yours — resolve it and say so.protocol.tsline 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
huangyiirene commentedon Aug 13, 2026
Part ofclosure — PR #8258 merged as3a27c46edb, and this card deliberately stays open.Re-graded
pm:dispatched→needs-user-decisionand 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.tsplus a scalar-divergence dogfood test. It pins every property classmergeObjectDefinitionstouches, 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 foundvalidations/indexesduplicated — invisible to #8015's pin because that pin compares field names and the field spread is idempotent; #8037 arrived next aboutlabel. 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
labelfix 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):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:metadataseat (#6367). Returns topm:queuethe moment the fork is ruled.#8268 was filed minutes ago from the #8038 dev's rescued handover:
GET /meta/object/:namedropsnameFieldfor artifact-ingested objects. Its own caveat is directly relevant here —nameFieldis 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→ #8268nameField), 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
huangyiirene commentedon Aug 13, 2026
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.
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 ofso it closes nothing, CI fully green, and the twoit.failscases 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
mergeObjectDefinitionsnames exactly six keys and copies nothing else — confirmed from both sides (the function body, andObjectExtensionSchema's own guidance). The fold has zero divergences across all six. Measured, not argued:The fold returns the same body to both reads.
translateObject(packages/spec/src/system/i18n-resolver.ts) then resolves each scalar ascatalog ?? document; the showcase catalog declaresobjects.showcase_account.label = "Account"; list and by-name are translated,?layers=trueis deliberately not. Hence "onto?layers=trueonly" — 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):Accountlayers.overlayCustomer← what the tenant savedlayers.effectiveAccount (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
labelfix 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
ObjectExtensionSchemafeature, 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.failscases. Byte-identity preserved for objects with no contributor and no overlay (toBereference-identity asserted); #8045's idempotency and thelayers.overlayboundary untouched.Gates: metadata-protocol 1094 ✅ · objectql 3390 ✅ · rest 1747 ✅ · runtime 2195 ✅ · dogfood 676 ✅ + 2 expected-fail (its own) ·
check:type-check-debtexit 0 on a built closure, no ledger entry raised, no--loweron 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 thelabel-shaped entry point; ⛔ do not dispatch a fold-side fix against it.pluralLabelanddescriptioncarry the identical exposure and are simply not triggered onmaintoday (no shipped extension overridespluralLabel;showcase_account's catalog has nodescriptionkey). Any ruling should cover all three scalars, notlabelalone.Generated by Claude Code
test(rest,dogfood): enumerate the object-extension fold's property cl…