Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-grid,plugin-dashboard): derive the relational copy set from its consumers, so a lookup cell honours the spec-declared displayField - #7156
Conversation
…m its consumers, so a lookup cell honours the spec-declared `displayField` The copy set ObjectGrid writes onto each relational column's fieldMeta was hand-kept and had become a strict subset of what its own lookup cell renderer and inline picker read off that bag. displayField, descriptionField and lookupColumns were read on this grid's path and never copied — and they are the spellings a spec-compliant producer actually emits, since @objectstack/spec 17.2.0's strict FieldSchema declares them and none of the snake_case twins. Derived instead of restated: relationalMetaKeys.ts classifies every key the consumers read, and a gate re-extracts that read set from the consumer sources so the two cannot drift apart again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-warren
commented
Sep 1, 2026
✅ Reviewed and armedAll 30 check runs in one call at The gating measurement came back opposite to the hypothesis — and the card got biggerI made the card's own first step a ruling: does a live
⇒ camelCase is the only dialect a live path can carry, and it is exactly the half the grid never copied. The defect is more reachable than the card supposed, not less. "Derive, do not restate" — delivered as a gate, not a longer listI ruled that a hand-maintained list is not the fix, because a hand-maintained list is what drifted. The copy set now lives in Ablation leg 2 is the load-bearing one: adding Three of the five keys added, two deliberately not — The user-visible bug was SHOWN, not assertedI said "the key isn't copied" is a code fact and "the cell shows the wrong thing" is the bug. You built two columns over one data source and one referenced record, differing only in spelling: on the unmodified tree the control column resolved ⚖️ The producer-side direction was rejected on the meritsThis is the part I most wanted to see. I fenced against picking the cheap direction and presenting it as the design conclusion. You did not take the internal fix because it was affordable — you rejected the producer fix because extending Clause ② verified not engaged rather than assumed — neither A3.2 falsified: the read set is not the five namedFour more spec-declared keys are read on this grid's path and never copied — ⚖️ Ruling on your open question: option A, keep it filed. Your reasoning is right and it is the same standard this card just met: ⚖️ On objectui#7155: agreed, C then likely B, and it is not yours or mine to make. Labelled Landing will be verified by content with a live control once Generated by Claude Code |
Fixes#6875
Verified on
dd35800af(origin/mainat branch point). Gate + suite numbers below arefrom
a5a6df951, the final commit.The card's own first step, run first — and it does not shrink the card
#6875 named one thing triage had to measure before anything else: "Whether the object
schema a live
getObjectSchemareturns ever carries the camelCase spellings (the servermay normalise on the way out)." If the producer normalises, widening the copy set fixes
nothing reachable.
It does not normalise, and the answer runs the other way — the camelCase spellings are
the only ones a live path can carry.
@objectstack/spec17.2.0'sFieldSchemais strict, with 71 properties. Enumeratedoff
FieldSchema.shapeon the installed package,name/type/labelas the positivecontrol:
Strict means an absent key does not pass through — it is refused by name:
The producer repo emits no snake_case. Whole-repo sweep of
objectstack, each zeroagainst a control on the same instrument:
display_field3 files /displayField68;description_field1 /descriptionField17;lookup_columns2 /lookupColumns13;lookup_filters4 /lookupFilters50;reference_field0. Every snake hit isprose — release notes, a CHANGELOG line, liveness-ledger notes describing what objectui
reads. The platform's published authoring rule states the split outright
(
skills/objectstack-data/rules/naming.md): field KEYS snake_case, schema PROPERTIEScamelCase, with
lookup_filters: []shown as ❌.Nothing renames anything on the way in.
ObjectStackAdapter.getObjectSchema(
@object-ui/data-objectstack) is the choke point every schema read passes through. Itsonly key rewrites are
normalizeSchemaReferenceKeys— thereference⇄reference_topair, and nothing else — and
applyFieldWidgetOverrides, which addswidget. Everyother property arrives verbatim.
⇒ An author who declares
displayFieldgets a def carryingdisplayField, and the gridcopied
display_fieldinstead. Item 2 of the card is not merely reachable; it is the onlyreachable half.
The bug reaching a user
The card is labelled
bugwith no user report attached, so this is shown rather thanasserted.
__tests__/lookupDisplayFieldSpelling-6875.test.tsxrenders two columns over onedata source, one referenced record and one cell renderer, differing only in the spelling on
the field def. Referenced record:
{ id:'p1', name:'Wrong Name', project_code:'ACME-42' }.code_snake(control)display_field: 'project_code'ACME-42ACME-42code_cameldisplayField: 'project_code'Wrong NameACME-42Measured on the unmodified tree:
{ acme: 1, wrongName: 1, rawId: 0 }— the spec-spelledcolumn rendered the referenced record's generic
.name, because with no display pointergetRecordDisplayNamefalls through to the.name/.titleheuristic. That is the bug: alookup cell showing the wrong display value because the key never arrived. The control
column is what makes it a reading rather than "the fixture never reached the lookup path".
Derived, not restated
The ruling on this card was that the copy set must be derived from the consumers' read
set — a longer hand-kept list is the same defect with a later expiry date. It is also the
card's strongest internal evidence:
lookupFilters(camel) was in the list whiledisplayFieldanddescriptionFieldwere not, though all three come off the same kind ofchain in the same files. Confirmed — that asymmetry is real, and it is the tell that
the list was assembled from defect reports.
packages/plugin-grid/src/relationalMetaKeys.ts— a table classifying every key thethree consumers read off this bag, each with a verdict and a reason.
RELATIONAL_META_KEYSisObject.entries(...)filtered by verdict. There is nohand-kept list left to drift.
__tests__/relationalMetaCopySet.derivation.test.ts— re-extracts the read set from theconsumer sources on every run (
LookupCellRenderer's cast reads, bounded to its ownfunction body;
LookupField'sfieldMeta?.k;UserField'smeta?.k) and requires thetable to match exactly, in both directions. An unclassified spelling is red; an
orphan is red.
prose: every
no-producerkey must be absent fromFieldSchema.shape, everyspeckeypresent, and every
legacy-aliaskey absent — so if a future spec version declares one,the gate says reclassify.
ObjectGrid.tsxloses 87 lines and gains 8: the literal, the helper and the retirementdocblock move to the new module, where the retirement history for
reference_to_field(#6711) and
titleFormat(#6874) is preserved in full. Both are now enforced structurallytoo — neither appears in the extracted read set, so re-adding either turns the gate red as
an orphan. Their behavioural pins are untouched and still green.
Which keys were added, and which were deliberately left out
Of the five the card named as never copied, three are added:
displayField,descriptionField,lookupColumns— spec-declared, read on this path.Two are not, and that is the card shrinking where it should:
reference_fieldandlookup_columnsare read on this path butFieldSchemadeclares neither, so no producercan fill them. Copying them would write a member from the def on every column build that
nothing can ever set — precisely what #6711 (
reference_to_field), #6625 (decimals) and#6597 (
referenceTo) retired, and what #6531 removed fromgetRecordDisplayNameon thesame reasoning.
plugin-dashboard— the card's unmeasured mention, now measuredThe card flagged
CELL_RELATIONAL_META_KEYSas having the same gap and marked it explicitlyNOT measured. It does. Its docblock enumerated what
LookupCellRendererreads as three keysbecause it was written from the FIRST leg of each chain; the renderer resolves
display_field || displayField || reference_field.displayFieldis added there too (samespelling, same reason, same read-only path),
reference_fieldstays out for the sameproducer reason,
FieldMetagains the member, the docblock is corrected, and the #6694 pinis extended in both directions. The four picker-only keys it deliberately does not copy are
untouched — that verdict does not move with this card.
Two of the four assumptions the dispatch asked me to falsify came back changed, both
recorded above: the card's line numbers had moved (#7141 landed in
packages/fields/src/widgets/), and the read set is larger than the five keys named.Out of scope, filed not fixed
multiple,allowCreate,lookupPageSize,dependsOnare read on its own path and never copied #7154 — the re-sweep found four more spec-declared keys read on this path and nevercopied:
multiple,allowCreate,lookupPageSize,dependsOn. Each changes pickerbehaviour rather than a display string, so each wants its own rendering test. They are
carried in the new table with verdict
deferred, so the gate keeps them visible.display_field,description_field,lookup_filters,id_field— is refused by the strictFieldSchema, so no spec-compliant producer can emit it #7155 — four keys that were already in the copy set (display_field,description_field,lookup_filters,id_field) are also refused by the strictFieldSchema. They are kept here — retiring a shipped key is its own adjudication, andthe chains read snake FIRST so removing a leg changes which value wins. Recorded as
legacy-aliasso the asymmetry is visible rather than implied.titleFormatin ObjectGrid's RELATIONAL_META_KEYS has zero FIELD-meta readers — every reader takes it off the OBJECT schema #6874 and finding(plugin-grid): ObjectGrid copiesreference_to_fieldonto every column's fieldMeta, and nothing in the repo reads it #6711 were not touched.Verification
Run from the repo root with root-relative paths (#3378).
vitest run packages/plugin-grid/vitest run packages/plugin-dashboard/type-check(both packages)Scope: 2 of 47, both echoed theirtsc --noEmitplustsc -p tsconfig.test.json;--listFilesconfirms all 3 new files are inside the test programlint(both packages, plaineslint .)no-explicit-anyon the string-to-anyRecordsignature carried over unchangedchangeset:check✅ All workspace packages are in the changeset fixed group.·✅ privatePackages declared: version=true, tag=false.·✅ No changeset declares a 'major' bump.check-changeset-presence✅ 6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)check-changeset-overwrite✅ No pre-existing changeset was modified or deleted.check:control-bytes✅ OK (scanned 5926 tracked text file(s))check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.(@objectstack/specwas already aplugin-griddep)check:self-import·check:spec-symbols·check:vi-mock-specifiers·check:vi-mock-inherit·check:side-effects-array·check:element-data-source-declaration·check:entry-guard·check:esm-specifiersTwo readings are absent, not green:
check:sdui-registration-pins→ exit 2,PREREQUISITE NOT MET: "No console build toweigh at apps/console/dist/assets." Not run here (a full console build on a shared box,
for a gate this diff has no exposure to: the new module declares types, a frozen array and
a pure function, with zero load-time registration, and
check:side-effects-array— theinstrument that judges load-time registration — walked the tree green). CI builds console
and runs it.
check:readme-exports→ exit 1, entirelydist/index.d.ts is not on disk — run pnpm build firstacross 22 packages. After buildingplugin-gridandplugin-dashboard, both drop to 0 findings; the remaining 240 belong to 20 packagesthis diff does not touch and are the unbuilt closure, which CI resolves by building first.
Ablation — the gate can fail, in the way that matters
Predicted red before running; mutation proven on disk by marker count and blob hash,
restore proven by state (
git diff HEAD,git diff --cached,git status --shortallempty). No rebuild needed:
vitest.config.mtsaliases@object-ui/fieldstopackages/fields/src, so these runs read source — which leg 2 also demonstrates.displayFieldentry from the table. Marker 1 → 0, blobb9b046c2→2c7724dc. Result:Tests 3 failed | 7 passed— the gate's unclassifiedcheck, its copy-set exactness check, and the rendering test all go red together.
fieldMeta?.zzNewSpellingadded toLookupField.tsx. Marker 0 → 1, blob8e75d7ce→920b18c1. Result:Tests 1 failed | 7 passed, messageexpected [ 'zzNewSpelling' ] to deeply equal [].Leg 2 is the one that matters: it is the drift that produced this bug, and the gate now
catches it from the consumer side without anyone editing a list.
Clause ② — not engaged
Nothing published moves.
RELATIONAL_META_KEYS,applyRelationalMetaand the newrelationalMetaKeys.tsare internal toplugin-gridand are not re-exported fromsrc/index.tsx(named exports only, verified).FieldMetaandCELL_RELATIONAL_META_KEYSlive in
plugin-dashboard/src/recordFields.tsx, whichsrc/index.tsxdoes not re-export —so the
FieldMetamember is internal too. No parse accept/reject behaviour changes; thestrict
FieldSchemais read, never modified.The producer-side alternative was considered and rejected on the merits, not on budget.
normalizeFieldReferenceKeysin@object-ui/coreis the repo's blessed choke-pointnormaliser and extending it would have been a one-line fix — but it would run backwards
here. For
referenceit stamps the objectui-side spelling from the served one; for thesekeys the spec spelling is the served one, so stamping
display_fieldfromdisplayFieldwould manufacture a twin the contract refuses. That is AGENTS.md #0.1's anti-pattern in its
worst direction — the renderer inventing a dialect the spec rejects — and it is a public
export's behaviour change on top. The renderer-side fix forwards what the def already
carries and invents no spelling; the dialect tolerance stays where it already was, in
@object-ui/fields, unchanged, and #7155 records the case for retiring it.Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Generated by Claude Code