From 09a2726a57f988a0080209f8c083da2c2076d5d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 14:33:35 +0000 Subject: [PATCH 1/3] docs(plugin-charts, plugin-form, plugin-list, app-shell): record the object-schema snake-lookup census at each site, retiring none MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Census only, no runtime change. Each of the six sites now carries the verdict for the bag it actually reads, traced to its producer. Three measurements moved every site to KEEP: 1. The serve path runs no parse. `ObjectStackAdapter.getObjectSchema` returns the server document verbatim plus `normalizeSchemaReferenceKeys` and `applyFieldWidgetOverrides` only, so a stored pre-strict document still delivers these keys. `FieldSchema`'s strictness gates authoring, not serving. 2. Five of the six sites have no camelCase leg, so retiring the snake read deletes the only read of the value rather than re-pointing it — and, conversely, those five already ignore a spec-compliant `displayField`/`idField`/`descriptionField`/ `lookupFilters` today (objectui#7435 for two of them; this census adds three). 3. The object-schema field def and the widget bag are the same object at runtime: `ObjectForm` threads each `getObjectSchema` field def to the widget, where `LookupField` reads the snake spellings first and `LookupFieldMetadata` declares all four. Retiring here would split one stored document's rendering between the form and the chart/list/filter/action consumers. One site of the six is not the object-schema def at all: `ListView`'s declared-columns branch reads a list-view column, and `ListColumnSchema` refuses both castings of every key involved (objectui#7531). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .changeset/lucky-donkeys-shave.md | 58 +++++++++++++++++++ .../src/utils/resolveActionParams.ts | 5 ++ .../inspectors/ObjectFieldInspector.tsx | 8 +++ packages/plugin-charts/src/ObjectChart.tsx | 9 +++ .../plugin-form/src/deriveMasterDetail.ts | 5 ++ packages/plugin-list/src/ListView.tsx | 13 +++++ packages/plugin-list/src/UserFilters.tsx | 4 ++ 7 files changed, 102 insertions(+) create mode 100644 .changeset/lucky-donkeys-shave.md diff --git a/.changeset/lucky-donkeys-shave.md b/.changeset/lucky-donkeys-shave.md new file mode 100644 index 0000000000..05be281abe --- /dev/null +++ b/.changeset/lucky-donkeys-shave.md @@ -0,0 +1,58 @@ +--- +--- + +Census only (objectui#7642): record, at each of the six sites, which contract types +the field-def bag it reads. No runtime behaviour changes — every snake leg is kept, +so this declares no release. + +The card proposed retiring `display_field` / `description_field` / `lookup_filters` / +`id_field` reads on the ground that `@objectstack/spec`'s `FieldSchema` is strict and +refuses them. Measured against the installed spec, that is true — of the AUTHORING +path. Three findings moved every site to KEEP: + +1. The SERVE path runs no parse. `ObjectStackAdapter.getObjectSchema` returns the + server document verbatim plus exactly two rewrites (`normalizeSchemaReferenceKeys`, + `applyFieldWidgetOverrides`); there is no `ObjectSchema.parse`/`safeParse` on that + path. A stored pre-strict document therefore still delivers these keys to every + consumer. The legs are not unreachable. +2. Five of the six sites have NO camelCase leg. They read the refused spelling and + nothing else, so retiring it does not re-point the read to the declared spelling — + it deletes the only read of the value. The corollary is the real user-facing gap: + on fully spec-compliant metadata those five sites already ignore a configured + `displayField` / `idField` / `descriptionField` / `lookupFilters` today. +3. The object-schema field def and the widget bag are the same object at runtime. + `ObjectForm` builds its fields from `getObjectSchema` and threads each def to the + widget, where `@object-ui/fields` `LookupField` reads `display_field` / + `description_field` / `id_field` / `lookup_filters` SNAKE-FIRST, and + `@object-ui/types`' `LookupFieldMetadata` (published, and documented as authorable + in `content/docs/fields/lookup.mdx`) declares all four. Retiring the legs at the + object-schema consumers while the form widget keeps reading snake-first off the + same def would make one stored document render one way in the form and another in + the chart, list, filters and action dialogs. + +Per site, which way the value would have flipped had the leg been retired: + +- `plugin-charts` `ObjectChart` (`id_field`, `display_field`) — bag proved to be the + object-schema def (`ds.getObjectSchema`). No camel leg: the value would have + collapsed to the constants `'id'` and `'name'` for every host, spec-compliant or not. +- `plugin-form` `deriveMasterDetail` (`display_field`) — object-schema def in-repo, + but `deriveColumns` is a public export, so external callers' bags are untraceable. + No camel leg: `col.displayField` would have become `undefined`. +- `plugin-list` `ListView`, columns branch — NOT the object-schema def. The bag is a + list-view column (`ListColumnSchema`), which refuses BOTH castings of all three keys. + A third contract, filed separately rather than half-retired. +- `plugin-list` `ListView`, object-def branch — object-schema def proved. No camel leg: + `displayField` and `idField` would have gone `undefined`. +- `plugin-list` `UserFilters` — `objectDef` is a public prop typed `any` on a publicly + exported component; the bag cannot be traced past this package. No camel leg. +- `app-shell` `resolveActionParams` — the in-file provenance note is correct; this is + the object-schema def. No camel leg for any of its four reads. +- `app-shell` `ObjectFieldInspector` (`lookup_filters`) — the only camel-first site, + and it writes camel back. Its snake leg reads a stored pre-strict document, so + retiring it would show an admin an empty filter list and let a save strand the real + filters. + +One live bug found and filed, not fixed here: the designer reads +`lookupFilters ?? lookup_filters` (camel first) while the runtime `LookupField` reads +`lookup_filters ?? lookupFilters` (snake first), so a document carrying both keys with +different values is displayed one way and honoured the other. diff --git a/packages/app-shell/src/utils/resolveActionParams.ts b/packages/app-shell/src/utils/resolveActionParams.ts index 73ea31eb4f..e1a69d7213 100644 --- a/packages/app-shell/src/utils/resolveActionParams.ts +++ b/packages/app-shell/src/utils/resolveActionParams.ts @@ -535,6 +535,11 @@ export function resolveActionParam( // contract declares, and renaming it would be a separate change. // Source here is `owner.fields[param.field]` — an object schema field def, // i.e. the protocol. Target contract: `ActionParamDef.referenceTo`. + // objectui#7642 CENSUS — verdict KEEP. The in-file provenance note above is + // CORRECT (`ctx.objects` is `useMetadata().objects`, the `/api/v1/meta/object` + // documents), so this really is the object-schema def. It is still kept: the + // serve path runs no parse, and none of the four reads below has a camel leg, + // so retiring them deletes the only read of four authorable keys. referenceTo: param.reference ?? field.reference, displayField: field.display_field ?? field.reference_field, idField: field.id_field, diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx index 31c5cf3eea..6eb67bd524 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx @@ -1142,6 +1142,14 @@ const LOOKUP_OPERATORS: Array<{ value: string; label: string }> = [ type LookupFilter = { field?: string; operator?: string; value?: unknown }; function readLookupFilters(def: Record): LookupFilter[] { + // objectui#7642 CENSUS — verdict KEEP. This is the one site of the six that + // reads camel FIRST and writes camel back (`patchDef({ lookupFilters })`), so + // its snake leg is purely a read of a STORED pre-strict document. Retiring it + // would show an admin an EMPTY filter list for such a document and let a save + // strand the real filters. NOTE the live inversion it participates in: the + // runtime reads `lookup_filters ?? lookupFilters` (snake first, + // `@object-ui/fields` LookupField) while this designer reads camel first — a + // document carrying both keys is displayed one way and honoured the other. const raw = def.lookupFilters ?? (def as Record).lookup_filters; return Array.isArray(raw) ? (raw as LookupFilter[]) : []; } diff --git a/packages/plugin-charts/src/ObjectChart.tsx b/packages/plugin-charts/src/ObjectChart.tsx index ed802ebf49..aecc6dbdbd 100644 --- a/packages/plugin-charts/src/ObjectChart.tsx +++ b/packages/plugin-charts/src/ObjectChart.tsx @@ -194,6 +194,15 @@ export async function resolveGroupByLabels( if (ids.length === 0) return data.map(row => ({ ...row, [rawKey]: row[groupByField] })); // Derive the ID field from metadata (fallback to 'id') + // objectui#7642 CENSUS — verdict KEEP (bag traced: `objectSchema` is + // `ds.getObjectSchema(schema.objectName)`, so this IS the object-schema def). + // `FieldSchema` refuses `id_field`/`display_field` on the AUTHORING path, but + // the SERVE path runs no parse — `ObjectStackAdapter.getObjectSchema` returns + // the server document plus only `normalizeSchemaReferenceKeys` and + // `applyFieldWidgetOverrides` — so a stored pre-strict def still arrives here. + // And there is NO camel leg below to fall back to: retiring these reads would + // delete the only read of the value, not re-point it. Adding the declared + // `idField`/`displayField` is a separate, contract-shaped change. const idField: string = fieldDef.id_field || 'id'; try { diff --git a/packages/plugin-form/src/deriveMasterDetail.ts b/packages/plugin-form/src/deriveMasterDetail.ts index b952177ada..0904bc7042 100644 --- a/packages/plugin-form/src/deriveMasterDetail.ts +++ b/packages/plugin-form/src/deriveMasterDetail.ts @@ -246,6 +246,11 @@ export function deriveColumns( if (col.type === 'select' && options) col.options = options; if (col.type === 'lookup') { col.reference = d?.reference; + // objectui#7642 CENSUS — verdict KEEP. In-repo the bag is the object-schema + // def (`MasterDetailForm` passes `dataSource.getObjectSchema(d.childObject)`), + // but `deriveColumns` is a PUBLIC export of `@object-ui/plugin-form`, so an + // external caller's `childSchema` cannot be traced from here. There is also no + // camel `d?.displayField` leg: retiring this read deletes the only read. col.displayField = d?.display_field || d?.reference_field; } if (col.type === 'file') applyFileColumnProps(col, d); diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index fbaa651784..f86652b4de 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -2710,6 +2710,14 @@ export const ListView = React.forwardRef(({ label: tFieldLabel(fieldName, f.label || fieldName), type: f.type || 'text', options: buildOptions(fieldName, f.options), + // objectui#7642 CENSUS — verdict KEEP, and NOTE the bag differs from + // the sibling branch below: `f` here is a LIST-VIEW COLUMN + // (`ListColumnSchema`), not an object-schema field def. Measured against + // the installed spec, `ListColumnSchema` refuses BOTH castings of all + // three keys (`display_field` AND `displayField`, `id_field` AND + // `idField`, `reference_to` AND `reference`), so this is not a + // snake-vs-camel question at all — it is a third contract. Filed + // separately rather than half-retired here. referenceTo: f.reference_to || f.reference, displayField: f.display_field || f.reference_field, idField: f.id_field, @@ -2733,6 +2741,11 @@ export const ListView = React.forwardRef(({ // with no rename hint. That is a different question and is filed, not // answered here. referenceTo: field.reference, + // objectui#7642 CENSUS — verdict KEEP. Bag traced: `objectDef` is + // `dataSource.getObjectSchema(schema.objectName)`, so this IS the + // object-schema def. But the serve path runs no parse, so a stored + // pre-strict def still arrives; and there is no camel leg here, so + // retiring these reads deletes the only read of the value. displayField: field.display_field || field.reference_field, idField: field.id_field, })); diff --git a/packages/plugin-list/src/UserFilters.tsx b/packages/plugin-list/src/UserFilters.tsx index 9c89326d39..e68664f6ff 100644 --- a/packages/plugin-list/src/UserFilters.tsx +++ b/packages/plugin-list/src/UserFilters.tsx @@ -297,6 +297,10 @@ function resolveFields( // legacy-only def is canonicalised ONCE at the ingestion choke point // (`normalizeSchemaReferenceKeys`, which warns in dev) — never here. referenceTo = fieldDef.reference; + // objectui#7642 CENSUS — verdict KEEP. `objectDef` is a PUBLIC prop typed + // `any` on a publicly exported component, so the bag cannot be traced past + // this package: the in-repo caller (`ListView`) passes `getObjectSchema` + // output, but an external host's is unknown. No camel leg here either. displayField = fieldDef.display_field ?? fieldDef.reference_field; idField = fieldDef.id_field; From 8c99d10d77246dbd2026aae7220d03282e823aef Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 5 Sep 2026 02:12:56 +0000 Subject: [PATCH 2/3] =?UTF-8?q?docs(plugin-charts,=20changeset):=20correct?= =?UTF-8?q?=20the=20census=20record=20after=20contract=20review=20?= =?UTF-8?q?=E2=80=94=20idField=20is=20not=20a=20declared=20FieldSchema=20s?= =?UTF-8?q?pelling,=20and=20the=20inversion=20bug=20was=20not=20filed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text-only remediation of the REFUSE verdict on PR #7649 (issuecomment-5548607662). No code, test or behaviour change: the only .tsx delta is comment text, proven by stripping //-comment lines from ObjectChart.tsx at the base and at this commit and comparing hashes. 1. idField is NOT a declared camel spelling. Re-measured on the pinned @objectstack/spec@17.2.0 with lit controls (minimal lookup def ACCEPTED; displayField / descriptionField / lookupFilters ACCEPTED; bogus key REJECTED unrecognized_keys): FieldSchema REJECTS idField with unrecognized_keys exactly as it rejects id_field. The spec's only idField sits on InlineGridColumnSchema, a different shape. The in-tree ObjectChart.tsx comment and the changeset no longer list idField beside displayField as a declared spelling; the id_field reads are routed to the ingestion choke point (objectui#7650, option A), not to an additive camel leg. 2. The changeset said the designer/runtime lookupFilters inversion bug was "filed". No card exists (targeted search, control lit). It now says the bug was deliberately NOT filed, and why. 3. Two non-blocking imprecisions, both re-measured: the resolveActionParams site is served by useMetadata().objects via client.meta.getItems in app-shell's MetadataProvider (no schema parse there either; the client's three safeParse calls are all event-payload schemas), and content/docs documents three of the four snake keys as authorable — display_field has zero hits in content/docs. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .changeset/lucky-donkeys-shave.md | 28 ++++++++++++++++------ packages/plugin-charts/src/ObjectChart.tsx | 8 +++++-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/.changeset/lucky-donkeys-shave.md b/.changeset/lucky-donkeys-shave.md index 05be281abe..30a7fcc1b8 100644 --- a/.changeset/lucky-donkeys-shave.md +++ b/.changeset/lucky-donkeys-shave.md @@ -13,19 +13,30 @@ path. Three findings moved every site to KEEP: 1. The SERVE path runs no parse. `ObjectStackAdapter.getObjectSchema` returns the server document verbatim plus exactly two rewrites (`normalizeSchemaReferenceKeys`, `applyFieldWidgetOverrides`); there is no `ObjectSchema.parse`/`safeParse` on that - path. A stored pre-strict document therefore still delivers these keys to every - consumer. The legs are not unreachable. + path. The `resolveActionParams` site is served by a different path — + `useMetadata().objects`, filled by `client.meta.getItems('object')` in `app-shell`'s + `MetadataProvider` — and that path runs no schema parse either (the provider's only + `parse` is `JSON.parse` of its session cache; the pinned `@objectstack/client`'s + three `safeParse` calls are all event-payload schemas, none on `getItems`). A + stored pre-strict document therefore still delivers these keys to every consumer. + The legs are not unreachable. 2. Five of the six sites have NO camelCase leg. They read the refused spelling and nothing else, so retiring it does not re-point the read to the declared spelling — it deletes the only read of the value. The corollary is the real user-facing gap: on fully spec-compliant metadata those five sites already ignore a configured - `displayField` / `idField` / `descriptionField` / `lookupFilters` today. + `displayField` / `descriptionField` / `lookupFilters` today. `idField` is NOT in + that list: measured on the pinned spec 17.2.0, `FieldSchema` refuses `idField` with + `unrecognized_keys` exactly as it refuses `id_field` — neither spelling of the id + key is declared, so the `id_field` reads have no camel leg to gain and their only + route is the ingestion choke point (objectui#7650, option A). 3. The object-schema field def and the widget bag are the same object at runtime. `ObjectForm` builds its fields from `getObjectSchema` and threads each def to the widget, where `@object-ui/fields` `LookupField` reads `display_field` / `description_field` / `id_field` / `lookup_filters` SNAKE-FIRST, and - `@object-ui/types`' `LookupFieldMetadata` (published, and documented as authorable - in `content/docs/fields/lookup.mdx`) declares all four. Retiring the legs at the + `@object-ui/types`' `LookupFieldMetadata` (published) declares all four — + `content/docs/fields/lookup.mdx` documents three of them as authorable + (`description_field`, `id_field`, `lookup_filters`; `display_field` has zero hits in + all of `content/docs`, which document `reference_field` instead). Retiring the legs at the object-schema consumers while the form widget keeps reading snake-first off the same def would make one stored document render one way in the form and another in the chart, list, filters and action dialogs. @@ -42,7 +53,8 @@ Per site, which way the value would have flipped had the leg been retired: list-view column (`ListColumnSchema`), which refuses BOTH castings of all three keys. A third contract, filed separately rather than half-retired. - `plugin-list` `ListView`, object-def branch — object-schema def proved. No camel leg: - `displayField` and `idField` would have gone `undefined`. + the branch's output `displayField` and `idField` (its own descriptor keys, not spec + spellings) would have gone `undefined`. - `plugin-list` `UserFilters` — `objectDef` is a public prop typed `any` on a publicly exported component; the bag cannot be traced past this package. No camel leg. - `app-shell` `resolveActionParams` — the in-file provenance note is correct; this is @@ -52,7 +64,9 @@ Per site, which way the value would have flipped had the leg been retired: retiring it would show an admin an empty filter list and let a save strand the real filters. -One live bug found and filed, not fixed here: the designer reads +One live bug found and deliberately NOT filed as a card (open PR #7641 flips the +runtime half and retires it on its own; the PM recorded it on objectui#7642 so it +becomes a card the moment #7641 stops being its fix), not addressed here: the designer reads `lookupFilters ?? lookup_filters` (camel first) while the runtime `LookupField` reads `lookup_filters ?? lookupFilters` (snake first), so a document carrying both keys with different values is displayed one way and honoured the other. diff --git a/packages/plugin-charts/src/ObjectChart.tsx b/packages/plugin-charts/src/ObjectChart.tsx index aecc6dbdbd..da65948d49 100644 --- a/packages/plugin-charts/src/ObjectChart.tsx +++ b/packages/plugin-charts/src/ObjectChart.tsx @@ -201,8 +201,12 @@ export async function resolveGroupByLabels( // the server document plus only `normalizeSchemaReferenceKeys` and // `applyFieldWidgetOverrides` — so a stored pre-strict def still arrives here. // And there is NO camel leg below to fall back to: retiring these reads would - // delete the only read of the value, not re-point it. Adding the declared - // `idField`/`displayField` is a separate, contract-shaped change. + // delete the only read of the value, not re-point it. Adding a `displayField` + // leg (the spelling `FieldSchema` declares) is a separate, contract-shaped + // change. `idField` is NOT such a leg: measured on the pinned spec 17.2.0, + // `FieldSchema` refuses `idField` with `unrecognized_keys` exactly as it + // refuses `id_field` (the spec's only `idField` sits on `InlineGridColumnSchema`, + // a different shape), so the id read has no declared spelling to re-point to. const idField: string = fieldDef.id_field || 'id'; try { From 95872adb7856143d28e2897d90d2b7def45588ba Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 02:45:59 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs(app-shell,=20plugin-form,=20changeset)?= =?UTF-8?q?:=20date=20the=20census=20record=20against=20main=20=E2=80=94?= =?UTF-8?q?=20#7641=20merged=20and=20retired=20the=20LookupField=20snake-f?= =?UTF-8?q?irst=20read,=20so=20the=20inversion=20is=20gone=20and=20the=20s?= =?UTF-8?q?plit=20now=20runs=20the=20other=20way?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second contract-review round (PR comment 5548785535) refused the record for asserting, in the present tense, that PR #7641 was open and in flight. It merged 2026-09-04T15:01:32Z as 351eb318, and on origin/main (a3eb5d07) LookupField reads lookupFilters only, LookupFieldMetadata declares the camel members only, and content/docs has 0 hits for all four snake keys. Text only, zero executable tokens moved (transpile-emit and AST-token hashes of all six census files identical before and after, lit control in the report): - changeset finding 3: dated to the base, post-#7641 state added, direction of the split corrected (keeping the six snake legs while the form reads camel-only is what splits a stored document now); KEEP restated on findings 1-2 and the ruling that refused B and made the choke point (A) the prerequisite. - changeset inversion paragraph: retired on main by #7641; nothing left to file. - changeset finding 2: "neither spelling of the id key is declared" scoped to FieldSchema; LookupFieldMetadata.idField is a declared widget-contract key. - ObjectFieldInspector.tsx readLookupFilters comment: the "live inversion" sentence replaced with the post-#7641 fact. - deriveMasterDetail.ts: the second snake read in hydrateColumns is now named by the deriveColumns comment and carries its own marker. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .changeset/lucky-donkeys-shave.md | 58 +++++++++++++------ .../inspectors/ObjectFieldInspector.tsx | 8 +-- .../plugin-form/src/deriveMasterDetail.ts | 3 + 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/.changeset/lucky-donkeys-shave.md b/.changeset/lucky-donkeys-shave.md index 30a7fcc1b8..0eb92d73fc 100644 --- a/.changeset/lucky-donkeys-shave.md +++ b/.changeset/lucky-donkeys-shave.md @@ -26,20 +26,37 @@ path. Three findings moved every site to KEEP: on fully spec-compliant metadata those five sites already ignore a configured `displayField` / `descriptionField` / `lookupFilters` today. `idField` is NOT in that list: measured on the pinned spec 17.2.0, `FieldSchema` refuses `idField` with - `unrecognized_keys` exactly as it refuses `id_field` — neither spelling of the id - key is declared, so the `id_field` reads have no camel leg to gain and their only - route is the ingestion choke point (objectui#7650, option A). + `unrecognized_keys` exactly as it refuses `id_field` — on `FieldSchema` neither + spelling of the id key is declared, so the `id_field` reads have no `FieldSchema` + spelling to gain a leg for and their only route is the ingestion choke point + (objectui#7650, option A). That is a `FieldSchema` statement only: the widget + contract `@object-ui/types` `LookupFieldMetadata` DOES declare `idField` (kept by + PR #7641 as a widget-contract key), and `LookupField` reads it off the same runtime + object finding 3 describes. 3. The object-schema field def and the widget bag are the same object at runtime. `ObjectForm` builds its fields from `getObjectSchema` and threads each def to the - widget, where `@object-ui/fields` `LookupField` reads `display_field` / - `description_field` / `id_field` / `lookup_filters` SNAKE-FIRST, and - `@object-ui/types`' `LookupFieldMetadata` (published) declares all four — - `content/docs/fields/lookup.mdx` documents three of them as authorable - (`description_field`, `id_field`, `lookup_filters`; `display_field` has zero hits in - all of `content/docs`, which document `reference_field` instead). Retiring the legs at the - object-schema consumers while the form widget keeps reading snake-first off the - same def would make one stored document render one way in the form and another in - the chart, list, filters and action dialogs. + widget, `@object-ui/fields` `LookupField`. What that widget reads moved while this + census was under review, so this record is dated. At this branch's base (`1ec291c0`, + 2026-09-04) `LookupField` read `display_field` / `description_field` / `id_field` / + `lookup_filters` SNAKE-FIRST, the published `@object-ui/types` `LookupFieldMetadata` + declared all four snake members, and `content/docs/fields/lookup.mdx` documented + three of them as authorable (`description_field`, `id_field`, `lookup_filters`; + `display_field` 0 hits in all of `content/docs`, which documented `reference_field` + instead). PR #7641 (merged 2026-09-04T15:01:32Z as `351eb318`) then converged the + widget contract on the spec's camelCase. Measured on `origin/main` (`a3eb5d07`): + `LookupField` reads `displayField` / `descriptionField` / `idField` / `lookupFilters` + ONLY (its `reference_field` fallback is kept), `LookupFieldMetadata` declares the + camel members only, and `content/docs` has 0 hits for all four snake keys (controls + in the same run: `reference_field` 4, `lookupFilters` 3). On the tree this census + lands in, the split therefore runs the OTHER way: it is KEEPING these six snake + legs, while the form widget reads camel-only, that lets one stored pre-strict + document render one way in the form and another in the chart, list, filters and + action dialogs. KEEP still stands — on findings 1-2 (the serve path delivers the + stored key, and five sites have no camel leg, so retiring the read deletes the only + read) and on the ruling that refused option B and made the ingestion choke point + (option A, objectui#7650) the prerequisite for any retirement. The way to close the + split is A (canonicalise once at ingestion) plus the additive camel legs tracked on + objectui#7435, not a consumer-side deletion. Per site, which way the value would have flipped had the leg been retired: @@ -64,9 +81,14 @@ Per site, which way the value would have flipped had the leg been retired: retiring it would show an admin an empty filter list and let a save strand the real filters. -One live bug found and deliberately NOT filed as a card (open PR #7641 flips the -runtime half and retires it on its own; the PM recorded it on objectui#7642 so it -becomes a card the moment #7641 stops being its fix), not addressed here: the designer reads -`lookupFilters ?? lookup_filters` (camel first) while the runtime `LookupField` reads -`lookup_filters ?? lookupFilters` (snake first), so a document carrying both keys with -different values is displayed one way and honoured the other. +One live bug found during the census and deliberately NOT filed as a card, not +addressed here: at this branch's base the designer (`ObjectFieldInspector`, +`readLookupFilters`) read `lookupFilters ?? lookup_filters` (camel first) while the +runtime `LookupField` read `lookup_filters ?? lookupFilters` (snake first), so a +document carrying both keys with different values was displayed one way and honoured +the other. It is RETIRED on `main`: PR #7641 (merged 2026-09-04T15:01:32Z, `351eb318`) +made `LookupField` read `lookupFilters` only, so both halves now honour the camel key +and there is nothing left to file. The fallback the PM recorded on objectui#7642 (a +card the moment #7641 stopped being its fix) is moot — #7641 landed. The designer's +snake leg survives as a read of a STORED pre-strict document, which is the ground of +its KEEP above, not as one side of a competing read order. diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx index 6eb67bd524..496843310c 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx @@ -1146,10 +1146,10 @@ function readLookupFilters(def: Record): LookupFilter[] { // reads camel FIRST and writes camel back (`patchDef({ lookupFilters })`), so // its snake leg is purely a read of a STORED pre-strict document. Retiring it // would show an admin an EMPTY filter list for such a document and let a save - // strand the real filters. NOTE the live inversion it participates in: the - // runtime reads `lookup_filters ?? lookupFilters` (snake first, - // `@object-ui/fields` LookupField) while this designer reads camel first — a - // document carrying both keys is displayed one way and honoured the other. + // strand the real filters. The runtime widget (`@object-ui/fields` LookupField) + // reads `lookupFilters` ONLY since objectui#7641 (merged 2026-09-04), so both + // halves honour the camel key: this snake leg is the designer's read of a stored + // document, not one side of a competing read order. const raw = def.lookupFilters ?? (def as Record).lookup_filters; return Array.isArray(raw) ? (raw as LookupFilter[]) : []; } diff --git a/packages/plugin-form/src/deriveMasterDetail.ts b/packages/plugin-form/src/deriveMasterDetail.ts index 0904bc7042..8c0f47ddb3 100644 --- a/packages/plugin-form/src/deriveMasterDetail.ts +++ b/packages/plugin-form/src/deriveMasterDetail.ts @@ -251,6 +251,7 @@ export function deriveColumns( // but `deriveColumns` is a PUBLIC export of `@object-ui/plugin-form`, so an // external caller's `childSchema` cannot be traced from here. There is also no // camel `d?.displayField` leg: retiring this read deletes the only read. + // The same read recurs in `hydrateColumns` below; this verdict covers both. col.displayField = d?.display_field || d?.reference_field; } if (col.type === 'file') applyFileColumnProps(col, d); @@ -305,6 +306,8 @@ export function hydrateColumns( if (type === 'select' && options && !next.options) next.options = options; if (type === 'lookup') { if (next.reference == null) next.reference = d?.reference; + // objectui#7642 CENSUS — verdict KEEP, same bag and same missing camel leg as + // `deriveColumns` above. if (next.displayField == null) next.displayField = d?.display_field || d?.reference_field; } if (type === 'file') applyFileColumnProps(next, d);