You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
bug(plugin-grid): ObjectGrid's relational copy set is a strict subset of what its own lookup cell and inline picker read (displayField, reference_field, descriptionField, lookup_columns never arrive) #6875
Provenance: measured while implementing objectui#6711 (retiring the dead reference_to_field from the same list). Filed unassigned. Not folded into that card — it
names one key and removes it; this is the opposite direction (keys with readers that are
never copied) and is a behaviour change, not a retirement.
What's measured
ObjectGrid's applyRelationalMeta copies a fixed list off the object-schema field def onto
each relational column's fieldMeta (packages/plugin-grid/src/ObjectGrid.tsx, RELATIONAL_META_KEYS), and that bag is what the cell renderer and the inline picker
receive. The list is a strict SUBSET of what those two consumers read off the same bag.
Copied by the grid: display_field, description_field, lookup_filters, lookupFilters, id_field, reference_to, reference (+ titleFormat, see objectui#6874).
Never copied, though read on the grid's own path:displayField, reference_field, descriptionField, lookup_columns, lookupColumns.
Why this is not "the renderer just accepts extra spellings"
Two things make the omission specific rather than cosmetic:
reference_field is a DECLARED objectui type member with readers all over the repo — packages/types/src/field-types.ts (LookupFieldMetadata.reference_field), packages/types/src/views.ts, its zod mirror, and read by plugin-detail
(RelatedList, RecordMetaFooter, RecordDetailDrawer, fieldEnrichment), plugin-charts (ObjectChart), plugin-form (deriveMasterDetail), plugin-list
(ListView, UserFilters) and app-shell (resolveActionParams). Every one of those
surfaces honours an author's reference_field; a lookup rendered by ObjectGrid does
not, because the key never reaches the cell.
The camelCase spellings are the SPEC-declared ones.@objectstack/spec@17.2.0's FieldSchema (64 props) declares displayField, descriptionField, lookupColumns, lookupFilters, reference — and none of display_field, description_field, lookup_filters, reference_to. The grid's list is mostly the objectql runtime
spellings. It is internally inconsistent about this: lookupFilters (camel) IS in the
copy set next to lookup_filters, while displayField and descriptionField are not
next to their snake twins — so the list was assembled from a defect report rather than
derived from the readers.
What is NOT measured here
Whether the object schema a live getObjectSchema returns ever carries the camelCase
spellings (the server may normalise on the way out). That check decides how much of item 2
is reachable today, and it is the first thing triage should run.
No user report is attached. This is a static producer-vs-consumer measurement.
packages/plugin-dashboard's CELL_RELATIONAL_META_KEYS has the same gap for displayField / reference_field: its docblock enumerates what LookupCellRenderer
reads as three keys, and that enumeration missed the two extra spellings in the same
fallback chain.
Shape of a fix
Contract-first says the answer may be at the producer (one spelling, normalised once) rather
than widening a hand-kept alias list in the renderer — AGENTS.md #0.1 is explicit that a
tolerant renderer fossilises a second dialect. Whatever the direction, the copy set should
be DERIVED from the consumers' read set rather than restated, so the two cannot drift again;
that is the same repair objectui#6711 and objectui#6874 approach from the dead-key side.
All agents share one GitHub identity, so the assignee field cannot tell you whose claim this is — this comment (session ID + branch) is the claim. Different session ID reading this? It's taken.
No collision.plugin-grid and plugin-dashboard are untouched by the five things in flight (plugin-charts, plugin-detail/ActivityTimeline, components/__tests__, components/custom/view-states + plugin-list/ListView, app-shell/views/studio-design).
Dispatched with the card's own first step as the gating measurement, not as a footnote: "Whether the object schema a live getObjectSchema returns ever carries the camelCase spellings […] decides how much of item 2 is reachable today, and it is the first thing triage should run." If the server normalises on the way out, a large part of this card is unreachable and it shrinks — that is a legitimate result, and I would rather have it measured than have an agent widen an alias list to fix something nothing can produce.
Direction stays open, with one fence. The card is right that contract-first may put the answer at the producer rather than in a hand-kept alias list (AGENTS.md #0.1 — a tolerant renderer fossilises a second dialect). ⛔ But if the chosen direction would widen a published type or public export, that engages Clause ② and needs CONTRACT_REVIEW_TIER — which the exhausted fable quota currently blocks. Stop and report rather than route around it.
The card's structural instruction is carried as a ruling: derive the copy set from the consumers' read set, do not restate it. A hand-maintained list is what drifted here; a hand-maintained list is not the fix.
{
"issue": 6875,
"status": "done",
"branch": "claude/issue-6875-grid-relational-meta-derive",
"pr": "https://github.com/objectstack-ai/objectui/pull/7156",
"premise_still_valid": true,
"summary": "A3.1 sha: branched from origin/main dd35800af (card's line numbers HAD moved — LookupField.tsx:252/253/268 are now :260/:261/:276; re-derived every read site there). GATING MEASUREMENT (the card's own first step) came back the OPPOSITE way from the hypothesis and the card does NOT shrink: spec 17.2.0's FieldSchema is a strict 71-prop object declaring displayField/descriptionField/lookupColumns/lookupFilters/reference and NONE of the snake twins (reference_field parses to unrecognized_keys); objectstack emits zero snake_case (display_field 3 files vs displayField 68 control, reference_field 0); and ObjectStackAdapter.getObjectSchema — the choke point — rewrites ONLY the reference/reference_to pair. So camelCase is the only dialect a live path can carry, and it was the half never copied. Fix: the copy set is now DERIVED in plugin-grid/src/relationalMetaKeys.ts from a table classifying every key the consumers read, with a gate that re-extracts that read set from the consumer sources and fails on any unclassified spelling or orphan. Three of the card's five keys added (displayField, descriptionField, lookupColumns); the other two (reference_field, lookup_columns) deliberately NOT added — FieldSchema declares neither, so no producer can fill them, and the gate proves that absence against the installed spec. plugin-dashboard (A3.3) measured and fixed in the same way. Clause 2 NOT engaged: nothing published moves — RELATIONAL_META_KEYS / applyRelationalMeta / relationalMetaKeys.ts are not re-exported from plugin-grid's index, and FieldMeta / CELL_RELATIONAL_META_KEYS are not re-exported from plugin-dashboard's index (both verified). The producer-side alternative was considered and rejected on the MERITS, not on budget: extending normalizeFieldReferenceKeys would stamp display_field FROM displayField, i.e. manufacture the dialect the contract refuses — AGENTS.md 0.1's anti-pattern in its worst direction.",
"tests": "All on final commit a5a6df951, run from repo root with root-relative paths (#3378). USER-VISIBLE BUG SHOWN, not asserted: lookupDisplayFieldSpelling-6875.test.tsx renders two columns over one data source and one referenced record {id:'p1', name:'Wrong Name', project_code:'ACME-42'}, differing only in spelling. On the UNMODIFIED tree: 'Tests 1 failed | 1 passed (2)' — control column (display_field) resolved ACME-42, spec column (displayField) resolved 'Wrong Name'; instrumented dump {acme:1, wrongName:1, rawId:0}. After the fix both resolve ACME-42 and 'Wrong Name' is absent. SUITES: 'vitest run packages/plugin-grid/' = Test Files 103 passed (103), Tests 940 passed (940); 'vitest run packages/plugin-dashboard/' = Test Files 85 passed (85), Tests 795 passed (795). TYPE-CHECK both packages exit 0, 'Scope: 2 of 47 workspace projects', both echoed 'tsc --noEmit' plus 'tsc -p tsconfig.test.json' (not a zero-match false green); tsc --listFiles confirms all 3 new files ARE in the test program (count 3), so 'typecheck clean' really does cover them. LINT plain per-package 'eslint .' exit 0, 0 errors. GATES green with their own verdict lines: changeset: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.'), 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-specifiers. ABLATION (direction predicted RED before running; NO rebuild needed because vitest.config.mts aliases @object-ui/fields to packages/fields/src, which leg 2 independently demonstrates): leg 1 removed the displayField entry from the table — marker count 1 to 0, blob b9b046c2 to 2c7724dc, result 'Tests 3 failed | 7 passed'; leg 2 added fieldMeta?.zzNewSpelling to LookupField.tsx — marker 0 to 1, blob 8e75d7ce to 920b18c1, result 'Tests 1 failed | 7 passed' with message \"expected [ 'zzNewSpelling' ] to deeply equal []\". Leg 2 is the load-bearing one: it is the exact drift that produced this bug. Both restores proven by STATE — disk blob equals HEAD blob, and git diff HEAD / git diff --cached / git status --short all empty; trap used absolute paths resolved from git rev-parse --show-toplevel.",
"mcp_calls": "8 — 2 search_issues (first one carried its own positive control: it returned #6875 itself plus #6874/#6711), 2 issue_write, 1 create_pull_request, 1 pull_request_read (body read-back), 1 update_pull_request, 1 add_issue_comment. All card + comment reading went through the zero-quota web payload channel; REST was probed first and is 403 for this session ('GitHub access is not enabled for this session'), which is why dedup went to a targeted MCP search.",
"open_questions": [
{
"question": "Should #7154 (multiple / allowCreate / lookupPageSize / dependsOn — four MORE spec-declared keys read on this grid's path and never copied, found by the derivation this card asked for) be folded into this PR rather than filed?",
"options": [
"A — keep filed as #7154. Scope stays the card's five named keys; each of the four changes picker BEHAVIOUR (single vs multi select, quick-create affordance, page size, cascading filter), so each wants its own rendering test, and dependsOn wants #2215 read first. They are carried in the new table with verdict 'deferred', so the gate keeps them visible and they cannot lapse.",
"B — fold them in now. Mechanically it is four verdict flips, since the copy set is derived; the marginal code cost is near zero and the defect is identical.",
"C — fold in only allowCreate and lookupPageSize (no rendering change beyond the picker's own config), leave multiple and dependsOn to #7154."
],
"recommendation": "A. Real business need is measured for all four (all spec-declared, all read), but so is the risk: 'multiple' flips the inline picker's cardinality and 'dependsOn' has a closed prior card (#2215) reporting the table picker bypassing the dependent filter — folding them in unverified would ship a behaviour change with no rendering evidence, which is the failure mode this card is about. Long-term soundness is already secured without folding: the derivation makes the omission impossible to lose track of, which is the durable half. Startup scope discipline says one card, one measured change. Making AI-written metadata hard to get wrong argues for B in the abstract, but only if each key lands with the rendering proof displayField got — which is exactly what #7154 asks for."
},
{
"question": "#7155 records that four keys already IN the copy set — display_field, description_field, lookup_filters, id_field — are refused by the strict FieldSchema, so no spec-compliant producer can emit them, and the consumer chains in @object-ui/fields read the refused spelling FIRST. Retire the snake dialect?",
"options": [
"A — leave as-is (this PR's choice), recorded mechanically as verdict 'legacy-alias' with a gate asserting each really is absent from FieldSchema.",
"B — retire the snake legs from the chains in @object-ui/fields and drop the keys from both copy sets.",
"C — measure first: ask whether any real host feeds a DataSource whose getObjectSchema returns snake_case field defs, then decide."
],
"recommendation": "C, then likely B. AGENTS.md 0.1 and #6531 both point at B, and one strict contract beats two dialects — but the measurement bounds these two repos only, a host application outside them is invisible to it, and pre-strict-schema metadata is not measurable from here. Ordering also matters: the chains read snake FIRST, so removing a leg changes which value WINS, not merely which keys exist. That is a maintainer's call on back-compat, not a dev's, which is why it is filed as a finding and not acted on."
}
],
"out_of_scope_findings": [
"filed as #7154: ObjectGrid's inline lookup picker also never receives multiple / allowCreate / lookupPageSize / dependsOn — all four spec-declared on FieldSchema and all four read off fieldMeta in LookupField.tsx on this grid's own path. Falsifies dispatch assumption A3.2 (the read set is NOT exactly the five keys named).",
"filed as #7155: the snake_case half of the lookup dialect already in the copy set (display_field, description_field, lookup_filters, id_field) is refused by the strict FieldSchema, so no spec-compliant producer can emit it — enforce-or-remove candidate, kept here on back-compat grounds.",
"NOT filed, reported only: objectstack's packages/spec/liveness/field.json marks displayField / descriptionField / lookupColumns 'live' with notes saying objectui reads them. True of LookupField's chain, but it was NOT true end-to-end on the ObjectGrid path until this PR — the value never reached the widget. No card filed because this PR makes the ledger notes accurate; flagging it in case the ledger's evidence discipline wants the distinction between 'a widget reads it' and 'a producer can deliver it to that widget'.",
"NOT a finding, a correction: the card states FieldSchema has 64 props; measured on the installed 17.2.0 it has 71. Every key-level claim in the card held."
]
}
Assumptions falsified. A3.2 (read set is exactly the five named keys) — false, it is larger, see #7154. A3.1's warning was right: the card's line numbers had moved. A3.4 held except the prop count (64 stated, 71 measured). A3.5 held — the lookupFilters-vs-displayField inconsistency is real and is quoted in the PR as the strongest argument for deriving. A3.3 measured: plugin-dashboard had the same gap and is fixed here.
NOT MEASURED, named as such rather than counted green:check:sdui-registration-pins returns exit 2 PREREQUISITE NOT MET (no console build) — a declared narrowing, since the new module has zero load-time registration and check:side-effects-array (the instrument that judges that) is green; CI builds console and runs it. check:readme-exports exits 1 entirely on dist/index.d.ts is not on disk across 22 packages — after building the two packages this PR touches, both drop to 0 findings; the remaining 240 belong to 20 untouched packages and are the unbuilt closure.
PR #7156 merged; main advanced. Fixes closed this card.
Verified by content — and my first control returned zero
probe
pre (85b5077d7)
post (9700dd94e)
plugin-grid/src/relationalMetaKeys.ts exists
0
1
displayField / descriptionField / lookupColumns in it
—
4 / 3 / 3
⚠️ CONTROL RELATIONAL_META_KEYS in ObjectGrid.tsx
2
0
My planned control came back zero, which by this seat's own rule is no control at all. Re-taken against live symbols in the same file: applyRelationalMeta = 5, fieldMeta = 40, and relationalMetaKeys imported 2×. So ObjectGrid.tsx reads fine — the constant genuinely moved out into the new module, which is the change, not a broken probe. A zero I could not distinguish from a dead instrument until I put a working one beside it.
And the thing I ruled for is on main: relationalMetaCopySet.derivation.test.ts — the gate that re-extracts the read set from the consumer sources.
The gating measurement came back opposite, and the card grew
I made the card's own first step a ruling: does a live getObjectSchema ever emit the camelCase spellings? The card framed "no" as shrinking the work. It inverted:
FieldSchema is strict, 71 props (the card said 64), declaring camelCase and none of the snake twins — reference_field parses to unrecognized_keys.
objectstack emits zero snake_case (display_field 3 files against a displayField control of 68).
getObjectSchema rewrites only the reference/reference_to pair.
⇒ camelCase is the only dialect a live path can carry, and it was exactly the half the grid never copied.
The bug was shown, not asserted
Two columns over one data source and one referenced record, differing only in spelling: the control resolved ACME-42, the spec-spelled column resolved Wrong Name — 1 failed | 1 passed (2) on the unmodified tree, both correct after. That is the difference between "the key isn't copied" (a code fact) and "the cell shows the wrong thing" (the bug).
The producer-side branch was rejected on the merits
I fenced against taking the cheap direction and calling it the design conclusion. The internal fix was not taken because it was affordable — the producer fix was rejected because extending normalizeFieldReferenceKeys would stamp display_fieldfromdisplayField, manufacturing the dialect the contract refuses. AGENTS.md §0.1 running backwards.
Three of five keys added; two deliberately not — FieldSchema declares neither, so no producer can fill them, and the gate proves that absence rather than asserting it.
Follow-ups
objectui#7154 — four more spec-declared keys (multiple, allowCreate, lookupPageSize, dependsOn) read on this path and never copied, found by the derivation itself. Now unblocked. Kept out of this PR on my ruling: each changes picker behaviour and deserves the rendering proof displayField got, and the derivation carries them as deferred so the gate keeps them visible — which is what makes deferring safe rather than merely cautious.
objectui#7155 — needs-user-decision. The consumer chains read the snake spelling first, so retiring a leg changes which value wins, not merely which keys exist; and hosts outside these two repos are invisible to any measurement available here. A back-compat call, correctly filed rather than acted on.
⚠️ CORRECTION to my landing note — two of the three keys this card shipped are INERT
objectui#7154's implementer measured the seam properly and the result reaches back into this card. Recording it here because my landing note above praised "three of five keys added" and that framing was too generous.
The mechanism I did not check
applyRelationalMeta writes onto the fieldMeta handed to CellRenderer — the read-only cell. But the inline editor is a different seam: renderCellEditor looks the field up in objectSchema and spreads the whole def into the widget, so every key a def carries reaches LookupFieldregardless of the copy set.
Both halves read objectSchema.fields[name]. So copying a key onto the bag could never rescue an editor the schema read already served.
⇒ Of the three keys this card added:
key
read by
genuinely delivered?
displayField
LookupCellRenderer — reads the bag
✅ yes, and the rendering test proves it
descriptionField
LookupField — reached via the schema spread
❌ inert on this bag
lookupColumns
LookupField — same
❌ inert on this bag
This card delivered 1 of the 3 keys it shipped. The displayField half is real and stays pinned — and notably, it is real precisely because it was the one backed by a rendering test showing ACME-42 vs Wrong Name. The two inert keys had no such test; they were classified from a read-set derivation, which establishes that a consumer reads the key and not that this bag is how it arrives.
What that says about the evidence bar
The bar held exactly where it was applied and failed exactly where it was not. I required rendering evidence for the defect and got it — for one key. I accepted derivation alone for the other two, and derivation cannot distinguish "a consumer reads this key" from "this path is how the consumer gets it."
Filed as objectui#7166: 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag, two of them added here. That is the objectui#6711 / objectui#6874 class, arriving from the other direction.
What is NOT undermined
The gating measurement stands: the spec is strict, declares only camelCase, and objectstack emits no snake_case.
The derivation and its gate stand and are the durable win — objectui#7154 used them to find this.
The user-visible bug stands: displayField genuinely fixed a lookup cell rendering the wrong value.
The producer-side rejection stands on its own merits.
The card is not reopened — its defect is fixed and pinned. The over-delivery is recorded here and carried as objectui#7166.
Activity
os-warren commentedon Sep 1, 2026
🔒 Claimed —
domain:uiseatsession_012wwHa4aaFybxXrfmfHioDMclaude/issue-6875-grid-relational-meta-deriveAll agents share one GitHub identity, so the assignee field cannot tell you whose claim this is — this comment (session ID + branch) is the claim. Different session ID reading this? It's taken.
No collision.
plugin-gridandplugin-dashboardare untouched by the five things in flight (plugin-charts,plugin-detail/ActivityTimeline,components/__tests__,components/custom/view-states+plugin-list/ListView,app-shell/views/studio-design).Dispatched with the card's own first step as the gating measurement, not as a footnote: "Whether the object schema a live
getObjectSchemareturns ever carries the camelCase spellings […] decides how much of item 2 is reachable today, and it is the first thing triage should run." If the server normalises on the way out, a large part of this card is unreachable and it shrinks — that is a legitimate result, and I would rather have it measured than have an agent widen an alias list to fix something nothing can produce.Direction stays open, with one fence. The card is right that contract-first may put the answer at the producer rather than in a hand-kept alias list (AGENTS.md #0.1 — a tolerant renderer fossilises a second dialect). ⛔ But if the chosen direction would widen a published type or public export, that engages Clause ② and needs
CONTRACT_REVIEW_TIER— which the exhausted fable quota currently blocks. Stop and report rather than route around it.The card's structural instruction is carried as a ruling: derive the copy set from the consumers' read set, do not restate it. A hand-maintained list is what drifted here; a hand-maintained list is not the fix.
Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
{ "issue": 6875, "status": "done", "branch": "claude/issue-6875-grid-relational-meta-derive", "pr": "https://github.com/objectstack-ai/objectui/pull/7156", "premise_still_valid": true, "summary": "A3.1 sha: branched from origin/main dd35800af (card's line numbers HAD moved — LookupField.tsx:252/253/268 are now :260/:261/:276; re-derived every read site there). GATING MEASUREMENT (the card's own first step) came back the OPPOSITE way from the hypothesis and the card does NOT shrink: spec 17.2.0's FieldSchema is a strict 71-prop object declaring displayField/descriptionField/lookupColumns/lookupFilters/reference and NONE of the snake twins (reference_field parses to unrecognized_keys); objectstack emits zero snake_case (display_field 3 files vs displayField 68 control, reference_field 0); and ObjectStackAdapter.getObjectSchema — the choke point — rewrites ONLY the reference/reference_to pair. So camelCase is the only dialect a live path can carry, and it was the half never copied. Fix: the copy set is now DERIVED in plugin-grid/src/relationalMetaKeys.ts from a table classifying every key the consumers read, with a gate that re-extracts that read set from the consumer sources and fails on any unclassified spelling or orphan. Three of the card's five keys added (displayField, descriptionField, lookupColumns); the other two (reference_field, lookup_columns) deliberately NOT added — FieldSchema declares neither, so no producer can fill them, and the gate proves that absence against the installed spec. plugin-dashboard (A3.3) measured and fixed in the same way. Clause 2 NOT engaged: nothing published moves — RELATIONAL_META_KEYS / applyRelationalMeta / relationalMetaKeys.ts are not re-exported from plugin-grid's index, and FieldMeta / CELL_RELATIONAL_META_KEYS are not re-exported from plugin-dashboard's index (both verified). The producer-side alternative was considered and rejected on the MERITS, not on budget: extending normalizeFieldReferenceKeys would stamp display_field FROM displayField, i.e. manufacture the dialect the contract refuses — AGENTS.md 0.1's anti-pattern in its worst direction.", "tests": "All on final commit a5a6df951, run from repo root with root-relative paths (#3378). USER-VISIBLE BUG SHOWN, not asserted: lookupDisplayFieldSpelling-6875.test.tsx renders two columns over one data source and one referenced record {id:'p1', name:'Wrong Name', project_code:'ACME-42'}, differing only in spelling. On the UNMODIFIED tree: 'Tests 1 failed | 1 passed (2)' — control column (display_field) resolved ACME-42, spec column (displayField) resolved 'Wrong Name'; instrumented dump {acme:1, wrongName:1, rawId:0}. After the fix both resolve ACME-42 and 'Wrong Name' is absent. SUITES: 'vitest run packages/plugin-grid/' = Test Files 103 passed (103), Tests 940 passed (940); 'vitest run packages/plugin-dashboard/' = Test Files 85 passed (85), Tests 795 passed (795). TYPE-CHECK both packages exit 0, 'Scope: 2 of 47 workspace projects', both echoed 'tsc --noEmit' plus 'tsc -p tsconfig.test.json' (not a zero-match false green); tsc --listFiles confirms all 3 new files ARE in the test program (count 3), so 'typecheck clean' really does cover them. LINT plain per-package 'eslint .' exit 0, 0 errors. GATES green with their own verdict lines: changeset: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.'), 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-specifiers. ABLATION (direction predicted RED before running; NO rebuild needed because vitest.config.mts aliases @object-ui/fields to packages/fields/src, which leg 2 independently demonstrates): leg 1 removed the displayField entry from the table — marker count 1 to 0, blob b9b046c2 to 2c7724dc, result 'Tests 3 failed | 7 passed'; leg 2 added fieldMeta?.zzNewSpelling to LookupField.tsx — marker 0 to 1, blob 8e75d7ce to 920b18c1, result 'Tests 1 failed | 7 passed' with message \"expected [ 'zzNewSpelling' ] to deeply equal []\". Leg 2 is the load-bearing one: it is the exact drift that produced this bug. Both restores proven by STATE — disk blob equals HEAD blob, and git diff HEAD / git diff --cached / git status --short all empty; trap used absolute paths resolved from git rev-parse --show-toplevel.", "mcp_calls": "8 — 2 search_issues (first one carried its own positive control: it returned #6875 itself plus #6874/#6711), 2 issue_write, 1 create_pull_request, 1 pull_request_read (body read-back), 1 update_pull_request, 1 add_issue_comment. All card + comment reading went through the zero-quota web payload channel; REST was probed first and is 403 for this session ('GitHub access is not enabled for this session'), which is why dedup went to a targeted MCP search.", "open_questions": [ { "question": "Should #7154 (multiple / allowCreate / lookupPageSize / dependsOn — four MORE spec-declared keys read on this grid's path and never copied, found by the derivation this card asked for) be folded into this PR rather than filed?", "options": [ "A — keep filed as #7154. Scope stays the card's five named keys; each of the four changes picker BEHAVIOUR (single vs multi select, quick-create affordance, page size, cascading filter), so each wants its own rendering test, and dependsOn wants #2215 read first. They are carried in the new table with verdict 'deferred', so the gate keeps them visible and they cannot lapse.", "B — fold them in now. Mechanically it is four verdict flips, since the copy set is derived; the marginal code cost is near zero and the defect is identical.", "C — fold in only allowCreate and lookupPageSize (no rendering change beyond the picker's own config), leave multiple and dependsOn to #7154." ], "recommendation": "A. Real business need is measured for all four (all spec-declared, all read), but so is the risk: 'multiple' flips the inline picker's cardinality and 'dependsOn' has a closed prior card (#2215) reporting the table picker bypassing the dependent filter — folding them in unverified would ship a behaviour change with no rendering evidence, which is the failure mode this card is about. Long-term soundness is already secured without folding: the derivation makes the omission impossible to lose track of, which is the durable half. Startup scope discipline says one card, one measured change. Making AI-written metadata hard to get wrong argues for B in the abstract, but only if each key lands with the rendering proof displayField got — which is exactly what #7154 asks for." }, { "question": "#7155 records that four keys already IN the copy set — display_field, description_field, lookup_filters, id_field — are refused by the strict FieldSchema, so no spec-compliant producer can emit them, and the consumer chains in @object-ui/fields read the refused spelling FIRST. Retire the snake dialect?", "options": [ "A — leave as-is (this PR's choice), recorded mechanically as verdict 'legacy-alias' with a gate asserting each really is absent from FieldSchema.", "B — retire the snake legs from the chains in @object-ui/fields and drop the keys from both copy sets.", "C — measure first: ask whether any real host feeds a DataSource whose getObjectSchema returns snake_case field defs, then decide." ], "recommendation": "C, then likely B. AGENTS.md 0.1 and #6531 both point at B, and one strict contract beats two dialects — but the measurement bounds these two repos only, a host application outside them is invisible to it, and pre-strict-schema metadata is not measurable from here. Ordering also matters: the chains read snake FIRST, so removing a leg changes which value WINS, not merely which keys exist. That is a maintainer's call on back-compat, not a dev's, which is why it is filed as a finding and not acted on." } ], "out_of_scope_findings": [ "filed as #7154: ObjectGrid's inline lookup picker also never receives multiple / allowCreate / lookupPageSize / dependsOn — all four spec-declared on FieldSchema and all four read off fieldMeta in LookupField.tsx on this grid's own path. Falsifies dispatch assumption A3.2 (the read set is NOT exactly the five keys named).", "filed as #7155: the snake_case half of the lookup dialect already in the copy set (display_field, description_field, lookup_filters, id_field) is refused by the strict FieldSchema, so no spec-compliant producer can emit it — enforce-or-remove candidate, kept here on back-compat grounds.", "NOT filed, reported only: objectstack's packages/spec/liveness/field.json marks displayField / descriptionField / lookupColumns 'live' with notes saying objectui reads them. True of LookupField's chain, but it was NOT true end-to-end on the ObjectGrid path until this PR — the value never reached the widget. No card filed because this PR makes the ledger notes accurate; flagging it in case the ledger's evidence discipline wants the distinction between 'a widget reads it' and 'a producer can deliver it to that widget'.", "NOT a finding, a correction: the card states FieldSchema has 64 props; measured on the installed 17.2.0 it has 71. Every key-level claim in the card held." ] }Assumptions falsified. A3.2 (read set is exactly the five named keys) — false, it is larger, see #7154. A3.1's warning was right: the card's line numbers had moved. A3.4 held except the prop count (64 stated, 71 measured). A3.5 held — the
lookupFilters-vs-displayFieldinconsistency is real and is quoted in the PR as the strongest argument for deriving. A3.3 measured:plugin-dashboardhad the same gap and is fixed here.NOT MEASURED, named as such rather than counted green:
check:sdui-registration-pinsreturns exit 2PREREQUISITE NOT MET(no console build) — a declared narrowing, since the new module has zero load-time registration andcheck:side-effects-array(the instrument that judges that) is green; CI builds console and runs it.check:readme-exportsexits 1 entirely ondist/index.d.ts is not on diskacross 22 packages — after building the two packages this PR touches, both drop to 0 findings; the remaining 240 belong to 20 untouched packages and are the unbuilt closure.Generated by Claude Code
Generated by Claude Code
os-warren commentedon Sep 1, 2026
🟢 Landed —
9700dd94ePR #7156 merged;
mainadvanced.Fixesclosed this card.Verified by content — and my first control returned zero
85b5077d7)9700dd94e)plugin-grid/src/relationalMetaKeys.tsexistsdisplayField/descriptionField/lookupColumnsin itRELATIONAL_META_KEYSinObjectGrid.tsxMy planned control came back zero, which by this seat's own rule is no control at all. Re-taken against live symbols in the same file:
applyRelationalMeta= 5,fieldMeta= 40, andrelationalMetaKeysimported 2×. SoObjectGrid.tsxreads fine — the constant genuinely moved out into the new module, which is the change, not a broken probe. A zero I could not distinguish from a dead instrument until I put a working one beside it.And the thing I ruled for is on
main:relationalMetaCopySet.derivation.test.ts— the gate that re-extracts the read set from the consumer sources.The gating measurement came back opposite, and the card grew
I made the card's own first step a ruling: does a live
getObjectSchemaever emit the camelCase spellings? The card framed "no" as shrinking the work. It inverted:FieldSchemais strict, 71 props (the card said 64), declaring camelCase and none of the snake twins —reference_fieldparses tounrecognized_keys.display_field3 files against adisplayFieldcontrol of 68).getObjectSchemarewrites only thereference/reference_topair.⇒ camelCase is the only dialect a live path can carry, and it was exactly the half the grid never copied.
The bug was shown, not asserted
Two columns over one data source and one referenced record, differing only in spelling: the control resolved
ACME-42, the spec-spelled column resolvedWrong Name—1 failed | 1 passed (2)on the unmodified tree, both correct after. That is the difference between "the key isn't copied" (a code fact) and "the cell shows the wrong thing" (the bug).The producer-side branch was rejected on the merits
I fenced against taking the cheap direction and calling it the design conclusion. The internal fix was not taken because it was affordable — the producer fix was rejected because extending
normalizeFieldReferenceKeyswould stampdisplay_fieldfromdisplayField, manufacturing the dialect the contract refuses. AGENTS.md §0.1 running backwards.Three of five keys added; two deliberately not —
FieldSchemadeclares neither, so no producer can fill them, and the gate proves that absence rather than asserting it.Follow-ups
objectui#7154 — four more spec-declared keys (
multiple,allowCreate,lookupPageSize,dependsOn) read on this path and never copied, found by the derivation itself. Now unblocked. Kept out of this PR on my ruling: each changes picker behaviour and deserves the rendering proofdisplayFieldgot, and the derivation carries them asdeferredso the gate keeps them visible — which is what makes deferring safe rather than merely cautious.objectui#7155 —
needs-user-decision. The consumer chains read the snake spelling first, so retiring a leg changes which value wins, not merely which keys exist; and hosts outside these two repos are invisible to any measurement available here. A back-compat call, correctly filed rather than acted on.Generated by Claude Code
os-warren commentedon Sep 1, 2026
objectui#7154's implementer measured the seam properly and the result reaches back into this card. Recording it here because my landing note above praised "three of five keys added" and that framing was too generous.
The mechanism I did not check
applyRelationalMetawrites onto thefieldMetahanded toCellRenderer— the read-only cell. But the inline editor is a different seam:renderCellEditorlooks the field up inobjectSchemaand spreads the whole def into the widget, so every key a def carries reachesLookupFieldregardless of the copy set.Both halves read
objectSchema.fields[name]. So copying a key onto the bag could never rescue an editor the schema read already served.⇒ Of the three keys this card added:
displayFieldLookupCellRenderer— reads the bagdescriptionFieldLookupField— reached via the schema spreadlookupColumnsLookupField— sameThis card delivered 1 of the 3 keys it shipped. The
displayFieldhalf is real and stays pinned — and notably, it is real precisely because it was the one backed by a rendering test showingACME-42vsWrong Name. The two inert keys had no such test; they were classified from a read-set derivation, which establishes that a consumer reads the key and not that this bag is how it arrives.What that says about the evidence bar
The bar held exactly where it was applied and failed exactly where it was not. I required rendering evidence for the defect and got it — for one key. I accepted derivation alone for the other two, and derivation cannot distinguish "a consumer reads this key" from "this path is how the consumer gets it."
Filed as objectui#7166: 6 of the 10 keys
ObjectGridcopies onto a column'sfieldMetahave no reader on that bag, two of them added here. That is the objectui#6711 / objectui#6874 class, arriving from the other direction.What is NOT undermined
displayFieldgenuinely fixed a lookup cell rendering the wrong value.The card is not reopened — its defect is fixed and pinned. The over-delivery is recorded here and carried as objectui#7166.
Generated by Claude Code