Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .changeset/lucky-donkeys-shave.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions packages/app-shell/src/utils/resolveActionParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,14 @@ const LOOKUP_OPERATORS: Array<{ value: string; label: string }> = [
type LookupFilter = { field?: string; operator?: string; value?: unknown };

function readLookupFilters(def: Record<string, unknown>): 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<string, unknown>).lookup_filters;
return Array.isArray(raw) ? (raw as LookupFilter[]) : [];
}
Expand Down
13 changes: 13 additions & 0 deletions packages/plugin-charts/src/ObjectChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions packages/plugin-form/src/deriveMasterDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions packages/plugin-list/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2710,6 +2710,14 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
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,
Expand All @@ -2733,6 +2741,11 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
// 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,
}));
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-list/src/UserFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading