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
42 changes: 42 additions & 0 deletions .changeset/object-grid-data-view-data-converged.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
---
"@objectstack/spec": minor
---

feat(spec): `ComponentPropsMap['object-grid'].data` converges onto `ViewDataSchema` (#12039, objectui#6207 Option A)

**BREAKING** accept-set change on one props-map entry, shipped as `minor` under
the repo's launch-window convention for breaking changes.

Two spec authorities disagreed on the KIND of `object-grid`'s `data`:
`ViewDataSchema` — the authority objectui#5090 ruled the registry declaration
against, and what `ObjectGridSchema.data` resolves to — is an object
discriminated on `provider` (`object` / `api` / `value` / `schema`), while
`ComponentPropsMap['object-grid'].data` said `z.array(z.unknown())`. Measured
on `@objectstack/spec@17.2.0`: `{ provider: 'value', items: [] }` — the
pinned-legal form — was refused by the props-map entry while the bare array
parsed. Maintainer ruling (2026-08-25, objectui#6207, Option A): the props-map
entry converges onto `ViewDataSchema`; the bare-array form is the deprecated
`staticData` shortcut the objectui#4648 carve-out already refuses to publish.

Migration — FROM → TO, one wrapping object:

```ts
// before (refused now)
data: [{ id: 1, title: 'Inline row' }]
// after
data: { provider: 'value', items: [{ id: 1, title: 'Inline row' }] }
```

The ruled migration check ran with the change: the sweep of generated
artifacts, templates and first-party corpora (examples/, skills/,
create-objectstack, spec fixtures) found zero bare-array `data` authors, so no
rewrite ships. `staticData` (the legacy bare-array shortcut the renderer still
reads) keeps its shape but is not the prescription.

`ComponentPropsMap['element:number'].filter` (the sibling key of #12039 /
objectui#6206) is NOT changed here: the ruling's binding measurement-first
precondition measured the pinned adapter/runtime refusing the raw
`ViewFilterRule[]` form on the element's primary (analytics) read path, which
forks that key back to triage. objectui#6206 remains open.

<!-- adr-0087: registered object-grid-data-view-data-converged -->
4 changes: 2 additions & 2 deletions content/docs/references/ui/component.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,8 +302,8 @@ const result = AIChatWindowProps.parse(data);
| **showColumnTypeIcons** | `boolean` | optional | Show field-type icons in column headers |
| **exportOptions** | `any` | optional | Export config (`{ formats, streaming }`) |
| **operations** | `any` | optional | Operation toggles (`{ export: false, … }`) |
| **data** | `any[]` | optional | Static inline rows — bypasses the object query |
| **staticData** | `any[]` | optional | Alternate spelling of `data` the renderer also reads |
| **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }` | optional | Data source binding (ViewDataSchema — discriminated on `provider`: object \| api \| value \| schema). Static inline rows live at `{ provider: 'value', items: [...] }`; the bare-array shortcut is refused — see migration `object-grid-data-view-data-converged` |
| **staticData** | `any[]` | optional | Deprecated bare-array static-rows shortcut the renderer still reads. Prefer `data: { provider: 'value', items: [...] }` |


---
Expand Down
1 change: 1 addition & 0 deletions packages/spec/api-surface/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -234,6 +234,7 @@
"ObjectFormProps (type)",
"ObjectFormPropsSchema (const)",
"ObjectGridProps (type)",
"ObjectGridPropsParsed (type)",
"ObjectGridPropsSchema (const)",
"ObjectKanbanProps (type)",
"ObjectKanbanPropsSchema (const)",
Expand Down
1 change: 1 addition & 0 deletions packages/spec/export-origins/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -234,6 +234,7 @@
"ObjectFormProps": "src/ui/component.zod.ts#ObjectFormProps (type)",
"ObjectFormPropsSchema": "src/ui/component.zod.ts#ObjectFormPropsSchema (const)",
"ObjectGridProps": "src/ui/component.zod.ts#ObjectGridProps (type)",
"ObjectGridPropsParsed": "src/ui/component.zod.ts#ObjectGridPropsParsed (type)",
"ObjectGridPropsSchema": "src/ui/component.zod.ts#ObjectGridPropsSchema (const)",
"ObjectKanbanProps": "src/ui/component.zod.ts#ObjectKanbanProps (type)",
"ObjectKanbanPropsSchema": "src/ui/component.zod.ts#ObjectKanbanPropsSchema (const)",
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import type { SemanticMigration } from '../../types.js';

export const entry: SemanticMigration = {
id: 'object-grid-data-view-data-converged',
surface:
"`object-grid` component props — `data` (the KIND: bare array `z.array(z.unknown())` "
+ 'vs the `ViewDataSchema` provider object)',
replacement:
"`ViewDataSchema` — the provider-discriminated object (`provider: 'object' | 'api' | "
+ "'value' | 'schema'`). Static inline rows move from `data: [...]` to "
+ "`data: { provider: 'value', items: [...] }` — the same rows, wrapped in the one "
+ 'arm that means "hardcoded data array". The other three arms are unchanged '
+ '`ViewDataSchema` semantics; `staticData` (the deprecated bare-array shortcut the '
+ 'renderer still reads) keeps its shape but is not the prescription',
reason:
'Two entries of one contract disagreed on the KIND (objectui#6207, contract-vs-'
+ "contract): `ComponentPropsMap['object-grid'].data` said bare array ('Static inline "
+ "rows — bypasses the object query') while `ViewDataSchema` — the authority "
+ 'objectui#5090 ruled the registry declaration against, pinned by '
+ '`gridDataInputContract.test.ts`, and what `ObjectGridSchema.data` resolves to — is '
+ "an object discriminated on `provider`. Measured on @objectstack/spec@17.2.0: "
+ "`{ provider: 'value', items: [] }` — the pinned-legal form — was REFUSED by the "
+ 'props-map entry (`expected array, received object`) while the bare array parsed. '
+ 'Whichever authority a value satisfied, the other refused it, and the objectui '
+ 'parity gate had to carry the reasoned exemption `object-grid.data:object` to look '
+ 'away. The maintainer ruling (2026-08-25, batch adjudication batch 4; verbatim: '
+ '「同意」, Option A) converged the props-map entry onto `ViewDataSchema`; the '
+ 'bare-array form is the deprecated `staticData` shortcut the objectui#4648 '
+ 'carve-out already refuses to publish. The ruled migration check ran with the '
+ 'change: the sweep of generated artifacts, templates and first-party corpora '
+ '(examples/, skills/, create-objectstack, spec fixtures) found ZERO bare-array '
+ '`data` authors, so no rewrite ships — this entry carries the prescription for '
+ 'authors outside the repo.',
acceptanceCriteria:
"`ComponentPropsMap['object-grid'].safeParse({ data: { provider: 'value', items: [] } })` "
+ 'succeeds (and the other `ViewDataSchema` arms parse through the same entry); a '
+ 'bare-array `data: [...]` is refused at the `data` path. An author carrying '
+ "`data: [...]` writes `data: { provider: 'value', items: [...] }` — same rows, "
+ 'one wrapping object. Downstream (objectui, after a released spec version reaches '
+ 'the pin): the `object-grid.data:object` exemption entry in '
+ '`registry-inputs-spec-parity.test.ts` becomes deletable, which is what closes '
+ 'objectui#6207.',
};
41 changes: 41 additions & 0 deletions packages/spec/src/migrations/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6058,6 +6058,47 @@ const step18: MigrationStep = {
+ 'set stays exactly `DEFAULT_METADATA_TYPE_REGISTRY` plus item-population growth, '
+ 'before and after.',
},
{
id: 'object-grid-data-view-data-converged',
surface:
"`object-grid` component props — `data` (the KIND: bare array `z.array(z.unknown())` "
+ 'vs the `ViewDataSchema` provider object)',
replacement:
"`ViewDataSchema` — the provider-discriminated object (`provider: 'object' | 'api' | "
+ "'value' | 'schema'`). Static inline rows move from `data: [...]` to "
+ "`data: { provider: 'value', items: [...] }` — the same rows, wrapped in the one "
+ 'arm that means "hardcoded data array". The other three arms are unchanged '
+ '`ViewDataSchema` semantics; `staticData` (the deprecated bare-array shortcut the '
+ 'renderer still reads) keeps its shape but is not the prescription',
reason:
'Two entries of one contract disagreed on the KIND (objectui#6207, contract-vs-'
+ "contract): `ComponentPropsMap['object-grid'].data` said bare array ('Static inline "
+ "rows — bypasses the object query') while `ViewDataSchema` — the authority "
+ 'objectui#5090 ruled the registry declaration against, pinned by '
+ '`gridDataInputContract.test.ts`, and what `ObjectGridSchema.data` resolves to — is '
+ "an object discriminated on `provider`. Measured on @objectstack/spec@17.2.0: "
+ "`{ provider: 'value', items: [] }` — the pinned-legal form — was REFUSED by the "
+ 'props-map entry (`expected array, received object`) while the bare array parsed. '
+ 'Whichever authority a value satisfied, the other refused it, and the objectui '
+ 'parity gate had to carry the reasoned exemption `object-grid.data:object` to look '
+ 'away. The maintainer ruling (2026-08-25, batch adjudication batch 4; verbatim: '
+ '「同意」, Option A) converged the props-map entry onto `ViewDataSchema`; the '
+ 'bare-array form is the deprecated `staticData` shortcut the objectui#4648 '
+ 'carve-out already refuses to publish. The ruled migration check ran with the '
+ 'change: the sweep of generated artifacts, templates and first-party corpora '
+ '(examples/, skills/, create-objectstack, spec fixtures) found ZERO bare-array '
+ '`data` authors, so no rewrite ships — this entry carries the prescription for '
+ 'authors outside the repo.',
acceptanceCriteria:
"`ComponentPropsMap['object-grid'].safeParse({ data: { provider: 'value', items: [] } })` "
+ 'succeeds (and the other `ViewDataSchema` arms parse through the same entry); a '
+ 'bare-array `data: [...]` is refused at the `data` path. An author carrying '
+ "`data: [...]` writes `data: { provider: 'value', items: [...] }` — same rows, "
+ 'one wrapping object. Downstream (objectui, after a released spec version reaches '
+ 'the pin): the `object-grid.data:object` exemption entry in '
+ '`registry-inputs-spec-parity.test.ts` becomes deletable, which is what closes '
+ 'objectui#6207.',
},
{
id: 'object-index-unknown-keys-refused',
surface: 'object `indexes[]` entries (`IndexSchema`) — undeclared keys',
Expand Down
19 changes: 14 additions & 5 deletions packages/spec/src/type-alias-convention.pin.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -267,7 +267,7 @@ import type * as M170 from './ui/component.zod.js';
import type * as M183 from './api/sortability.zod.js';

// ---------------------------------------------------------------------------
// 835 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared.
// 834 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared.
//
// That number is machine-checked, not hand-kept. The runtime companion at the
// bottom of this file recomputes the pin count from the source and asserts that
Expand DownExpand Up@@ -1568,9 +1568,11 @@ export type Iso849 = Assert<Eq< z.input< typeof M170.RecordHistoryProps >, z.inf
// The object-* block family (#7751) — deliberately default-free in its first,
// warning-tier step ("the author said nothing" must stay distinguishable from
// "the author asked for the renderer's fallback"), so input === infer holds.
// A default added to any of these six goes red here, and the fix is the ADR's:
// A default added to any of these goes red here, and the fix is the ADR's:
// declare the XParsed alias and delete the pin line.
export type Iso839 = Assert<Eq< z.input< typeof M170.ObjectGridPropsSchema >, z.infer< typeof M170.ObjectGridPropsSchema > >>;
// `ObjectGridPropsSchema` (Iso839) left the family exactly that way on the
// ui#6207 convergence: its `data` now carries `ViewDataSchema`, whose own
// input ≠ infer, so `ObjectGridPropsParsed` is declared and the pin deleted.
export type Iso840 = Assert<Eq< z.input< typeof M170.ObjectMetricPropsSchema >, z.infer< typeof M170.ObjectMetricPropsSchema > >>;
export type Iso841 = Assert<Eq< z.input< typeof M170.ObjectKanbanPropsSchema >, z.infer< typeof M170.ObjectKanbanPropsSchema > >>;
export type Iso842 = Assert<Eq< z.input< typeof M170.ObjectCalendarPropsSchema >, z.infer< typeof M170.ObjectCalendarPropsSchema > >>;
Expand DownExpand Up@@ -1670,7 +1672,7 @@ describe('ADR-0122 type-alias convention', () => {
// this title and the section header above the pin list — are now asserted
// against the recomputed count below, so neither can go stale without a red
// test naming it.
it('still declares all 835 isomorphic pins', () => {
it('still declares all 834 isomorphic pins', () => {
// The truth of each pin is proved by tsc, not here — an `Assert<Eq<...>>`
// that stops holding is a compile error with the alias named. What tsc
// cannot notice is a pin that was DELETED: removing the assertion removes
Expand DownExpand Up@@ -1970,9 +1972,16 @@ describe('ADR-0122 type-alias convention', () => {
// `.pipe()` anywhere in either tree, so the two shapes coincide and
// ADR-0122 gives each a pin rather than an `XParsed`. Ids `Iso857`/
// `Iso858`, the next free ones — ids are claims about pins, not positions.
//
// 835 -> 834 is #12039's ui#6207 convergence: `ObjectGridPropsSchema.data`
// now carries `ViewDataSchema`, whose own input ≠ infer (measured — the
// Eq probe answers false on ViewDataSchema alone), so `object-grid` left
// the default-free object-* family exactly the way that family's comment
// prescribes: `ObjectGridPropsParsed` declared, the Iso839 pin deleted.
// -1 converted to an `XParsed` pair; the Iso number stays vacant.
const self = readFileSync(fileURLToPath(import.meta.url), 'utf8');
const pins = self.match(/^export type Iso\d+ = Assert</gm) ?? [];
expect(pins).toHaveLength(835);
expect(pins).toHaveLength(834);

// The count is stated in PROSE twice as well — this case's title and the
// section header above the pin list — and until #6605 nothing read either
Expand Down
28 changes: 28 additions & 0 deletions packages/spec/src/ui/component.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2121,6 +2121,34 @@ describe('#7751 — object-* block props schemas', () => {
expect(parsed.filter).toEqual([['owner_id', '=', '{current_user_id}']]);
});

it("object-grid `data` takes the ViewDataSchema provider object — the ui#6207 convergence (Option A)", () => {
// The #5090-pinned authority: static inline rows are `{ provider: 'value',
// items }`. Before the 2026-08-25 ruling this exact value was REFUSED by
// this entry ("expected array, received object") while being the
// pinned-legal form of the authority the objectui declaration is held to.
const inline = ComponentPropsMap['object-grid'].safeParse({
data: { provider: 'value', items: [] },
});
expect(inline.success).toBe(true);
// A second arm of the union, to prove the whole discriminated authority is
// reachable through this entry rather than one hardcoded branch.
const bound = ComponentPropsMap['object-grid'].safeParse({
data: { provider: 'object', object: 'showcase_task' },
});
expect(bound.success).toBe(true);
});

it('the bare-array `data` — the deprecated `staticData` shortcut — is REFUSED at the `data` path', () => {
// Reverse verification of the convergence: the value this entry used to
// accept (`z.array(z.unknown())`) no longer parses. The #4648 carve-out
// already refuses to publish the bare-array author; this closes the spec
// entry that still advertised it. Migration:
// `object-grid-data-view-data-converged`.
const r = ComponentPropsMap['object-grid'].safeParse({ data: [{ id: 1 }] });
expect(r.success).toBe(false);
expect(r.error!.issues.some((i) => i.path[0] === 'data')).toBe(true);
});

it('`defaultFilters` stays HONOURED — it is a read legacy fallback, not an inert spelling', () => {
// ObjectGrid.tsx reads it and lowers it to `$filter` when `filter` is
// absent (the routed finding on #7751 verified the read point). Only the
Expand Down
29 changes: 26 additions & 3 deletions packages/spec/src/ui/component.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

import { z } from 'zod';
import { FilterConditionSchema } from '../data/filter.zod';
import { ViewFilterRuleSchema } from './view.zod';
import { ViewFilterRuleSchema, ViewDataSchema } from './view.zod';
import { InlineActionSchema, ActionLocationSchema } from './action.zod';
import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod';
import { FeedItemType, FeedFilterMode } from '../data/feed.zod';
Expand DownExpand Up@@ -2263,11 +2263,34 @@ export const ObjectGridPropsSchema = lazySchema(() => strictObject({
showColumnTypeIcons: z.boolean().optional().describe('Show field-type icons in column headers'),
exportOptions: z.unknown().optional().describe('Export config ({ formats, streaming })'),
operations: z.unknown().optional().describe('Operation toggles ({ export: false, … })'),
data: z.array(z.unknown()).optional().describe('Static inline rows — bypasses the object query'),
staticData: z.array(z.unknown()).optional().describe('Alternate spelling of `data` the renderer also reads'),
/**
* Data source binding — `ViewDataSchema`, the #5090-pinned authority the
* objectui registry declares against (`plugin-grid/src/index.tsx:225`
* `type: 'object'`, held by `gridDataInputContract.test.ts`). Until the
* ui#6207 ruling (2026-08-25, Option A: 「同意」) this entry said
* `z.array(z.unknown())` — the bare-array spelling of the deprecated
* `staticData` shortcut — so the two spec authorities refused each other's
* legal values: this entry accepted `data: [{…}]` and refused
* `{ provider: 'value', items: [] }`, while `ViewDataSchema` (what
* `ObjectGridSchema.data` resolves to, and what the designer publishes)
* ruled the opposite. Static inline rows live at
* `{ provider: 'value', items: [...] }`; the migration prescription is the
* `object-grid-data-view-data-converged` semantic entry.
*/
data: ViewDataSchema.optional()
.describe("Data source binding (ViewDataSchema — discriminated on `provider`: object | api | value | schema). Static inline rows live at `{ provider: 'value', items: [...] }`; the bare-array shortcut is refused — see migration `object-grid-data-view-data-converged`"),
staticData: z.array(z.unknown()).optional().describe("Deprecated bare-array static-rows shortcut the renderer still reads. Prefer `data: { provider: 'value', items: [...] }`"),
}));
/** Author state (ADR-0122: the bare name is the author state). */
export type ObjectGridProps = z.input<typeof ObjectGridPropsSchema>;
/**
* ADR-0122: the parsed state differs from the authored state on exactly one
* key — `data` carries `ViewDataSchema` (the ui#6207 convergence), whose own
* input ≠ infer. So `object-grid` leaves the type-alias convention pin's
* default-free family (the Iso839 line deleted with this alias), taking the
* `RecordAlertPropsParsed` route its comment prescribes.
*/
export type ObjectGridPropsParsed = z.infer<typeof ObjectGridPropsSchema>;

/**
* `object-metric` (objectui `plugin-dashboard/src/ObjectMetricWidget.tsx` @
Expand Down
Loading