Skip to content
Merged
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
34 changes: 34 additions & 0 deletions .changeset/6458-retire-undeclared-column-reads.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
---
'@object-ui/plugin-grid': minor
---

Retire four undeclared authored column reads in `ObjectGrid.generateColumns()`
(objectui#6458).

`ListColumnSchema` is a strict object, so an author who wrote `format`,
`options`, `appearance` or `essential` on a list column was **refused at
publish** with `unrecognized_keys` while the grid happily honoured the key at
runtime. That is the `declared != enforced` split AGENTS.md #0.1 exists to
stop, and it was reachable only through `(col as any)`. All four reads are
removed. Each was re-measured on this branch before deletion: **zero authored
occurrences on a column across `examples/` and `apps/`**, per key, with `field`
as the positive control in the same query shape — so no real author's metadata
changes behaviour today.

What each retirement leaves as the only road:

- `format` and `appearance` — the object-field fallback (`objectDefField?.format`
/ `?.appearance`), which is already the road every measured author uses.
- `options` — the object schema's select options. The column-level override was
exactly the shape that let AI-authored metadata drift from the schema it is
supposed to obey; one source for options beats two.
- `essential` — mobile visibility stays positional (`colIndex === 0`).

**Retired for want of authors, not forbidden forever.** If a real request for
semantic mobile-column control arrives, the declare route reopens: declare the
key on `@objectstack/spec` and read it without a cast. What stays ruled out is
the third road — a renderer-side tolerance for a key the schema refuses.

`columnReadBoundary-6458.test.ts` moves with the change: its bound on undeclared
cast reads in that branch is now the **empty set**, so a new one goes red on
arrival rather than accreting quietly.
51 changes: 35 additions & 16 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -571,8 +571,11 @@ function normalizeColumns(
* needs no hold here. Tombstoned only in the sense that nothing writes it.
*
* `essential` is absent from both types on purpose: objectui#6004's suggested
* key list named it, but it is READ off the authored column and turned into a
* `className` — it is never emitted, and it is not a `ListColumn` member either.
* key list named it, but it was READ off the authored column and turned into a
* `className` — never emitted, and not a `ListColumn` member either. That read
* is now RETIRED too (objectui#6458), so mobile visibility is decided purely
* positionally (`colIndex === 0`) and nothing on either side of this producer
* carries the key.
*/

/**
Expand DownExpand Up@@ -1951,7 +1954,10 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
// `any[]`, and `ObjectGridColumnDraft` cannot bite on any member. Naming
// the producer vocabulary here stops `any` at this one boundary.
const baseInferredType: string | null = col.type || objectDefField?.type || inferColumnType({ field: col.field }) || null;
const formatHint = (col as any).format ?? objectDefField?.format;
// objectui#6458 — the column-level `format` read is RETIRED. The
// object-field fallback below is now the only road, which is what
// every measured author already used.
const formatHint = objectDefField?.format;
const inferredType: string | null = baseInferredType
? resolveCellRendererType({ type: baseInferredType, format: formatHint })
: null;
Expand All@@ -1976,13 +1982,11 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
const uniqueValues = Array.from(new Set(data.map(row => row[col.field]).filter(Boolean)));
fieldMeta.options = uniqueValues.map(v => ({ value: v, label: humanizeLabel(String(v)) }));
}
if ((col as any).options) {
fieldMeta.options = translateOptions(schema.objectName, col.field, (col as any).options);
}
// Honor metadata-defined appearance only (col.appearance or
// objectDef field.appearance). When unset, the cell renders
// its default badge style — same as detail / form views.
const explicitAppearance = (col as any).appearance ?? objectDefField?.appearance;
// Honor metadata-defined appearance only — the objectDef FIELD's
// `appearance`, which since objectui#6458 is the only road (the
// column-level read is retired). When unset, the cell renders its
// default badge style — same as detail / form views.
const explicitAppearance = objectDefField?.appearance;
if (explicitAppearance != null) {
fieldMeta.appearance = explicitAppearance;
}
Expand DownExpand Up@@ -2067,11 +2071,26 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
// reads around it, and it threw away `ColumnPrefix`'s own typing, so
// `prefixConfig.field` was `any` at every use below.
//
// The four undeclared reads in this branch — `format`, `options`,
// `appearance`, `essential` — are deliberately NOT touched here. Each
// needs a declare-on-spec vs stop-reading verdict (objectui#6458
// escalates them), the declare leg is `@objectstack/spec` surface, and
// AGENTS.md #0.1 forbids answering either one renderer-side.
// ⭐ The four undeclared reads that used to sit in this branch —
// `format`, `options`, `appearance`, `essential` — are RETIRED
// (objectui#6458, maintainer ruling 2026-08-28, "B on all four").
// `ListColumnSchema` is a strict object that refuses all four at
// publish, so honouring them here was the `declared != enforced`
// split AGENTS.md #0.1 exists to stop. The retirement route (rather
// than declaring them on `@objectstack/spec`) follows the standing
// zero-authors rule: a key with no measured authors is retired at
// once, with no deprecation window. Re-measured on this ref before
// the deletion — zero authored occurrences of any of the four on a
// column across `examples/` and `apps/`.
//
// ⚠️ Retired for want of authors, NOT forbidden forever. If a real
// request for semantic mobile-column control arrives, the declare
// route reopens — objectstack#12715 is the precedent (removed while
// unenforced, re-introduced once demand and enforcement met). What
// is forbidden is the third road: a renderer-side tolerance for a
// key the schema refuses. `columnReadBoundary-6458.test.ts` now
// bounds this branch's undeclared cast reads to the EMPTY SET, so a
// new one goes red on arrival instead of accreting quietly.
const prefixConfig = col.prefix;
if (prefixConfig?.field) {
const baseCellRenderer = cellRenderer;
Expand All@@ -2098,7 +2117,7 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
const inferredAlign = col.align || (effectiveType && numericTypes.includes(effectiveType) ? 'right' as const : undefined);

// Determine if column should be hidden on mobile
const isEssential = colIndex === 0 || (col as any).essential === true;
const isEssential = colIndex === 0;

return {
header,
Expand Down
Loading
Loading