diff --git a/.changeset/lucky-donkeys-shave.md b/.changeset/lucky-donkeys-shave.md new file mode 100644 index 0000000000..0eb92d73fc --- /dev/null +++ b/.changeset/lucky-donkeys-shave.md @@ -0,0 +1,94 @@ +--- +--- + +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. 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` / `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` — 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, `@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: + +- `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: + 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 + 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 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/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..496843310c 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. 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-charts/src/ObjectChart.tsx b/packages/plugin-charts/src/ObjectChart.tsx index ed802ebf49..da65948d49 100644 --- a/packages/plugin-charts/src/ObjectChart.tsx +++ b/packages/plugin-charts/src/ObjectChart.tsx @@ -194,6 +194,19 @@ 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 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 { diff --git a/packages/plugin-form/src/deriveMasterDetail.ts b/packages/plugin-form/src/deriveMasterDetail.ts index b952177ada..8c0f47ddb3 100644 --- a/packages/plugin-form/src/deriveMasterDetail.ts +++ b/packages/plugin-form/src/deriveMasterDetail.ts @@ -246,6 +246,12 @@ 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. + // 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); @@ -300,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); 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;