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
2 changes: 2 additions & 0 deletions .changeset/10872-held-public-block-arms.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@
- `record:line_items` is still refused at `type`: the spec has no row for it yet.

⚠️ **Dated note, 2026-10-01 — props written flat on the node — objectui#10872 batch 10.** Later in this same release every member of a public block's row written directly on the node is refused by name on both faces, at its own path, with a message naming `properties.KEY`. So the bullet that says the tolerant face passes such a key unjudged, and that the flat `{ "type": "action:button", "label": "Open details", "actionType": "url", "target": "/users/ada" }` "validates through `objectui validate`", no longer describes the release as a whole: `objectui validate` refuses its `actionType` and `target` by name, as the strict face and `@objectstack/spec`'s page component do, and keeps the node-level `label`, which the spec's page component also declares. The bag spelling still passes both faces. The rest of this entry is kept as the reading of this change.

⚠️ **Dated note, 2026-10-03 — `record:line_items` armed — objectui#10872.** Later in this same release `@objectstack/spec` 17.6.0 carries a `ComponentPropsMap` row for `record:line_items`, and the block is armed from that row (`.changeset/10872-record-line-items-arm.md`). So the bullet "`record:line_items` is still refused at `type`: the spec has no row for it yet" no longer describes the release as a whole. The rest of this entry is kept as the reading of this change.
2 changes: 2 additions & 0 deletions .changeset/10872-public-block-zod-arms.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@
⚠️ **Dated note, 2026-09-30 — the four `page:` containers' child list — objectui#10872 batch 6.** Later in this same release the arms of `page:card`, `page:section`, `page:footer` and `page:sidebar` refuse a node-level `children` and restate `body`, both with a message that names `properties.children`, the member their spec row declares (`.changeset/10872-container-children-channel.md`). So the bullet on a prop written directly on the node, where it says `children` (`page:card`, `page:section`, `page:footer`, `page:sidebar`) is judged against the base's own type and `body` (`page:card`) is refused by the base's objectui#6771 retirement, no longer describes the release as a whole: on those four, both keys are refused by the arm's own refusal. The rest of this entry is kept as the reading of this change.

⚠️ **Dated note, 2026-10-01 — props written flat on the node — objectui#10872 batch 10.** Later in this same release every member of a public block's row written directly on the node is refused by name on both faces, at its own path, with a message naming `properties.KEY`. So the bullet on a prop written directly on the node, where it says such a key "is not judged against the block's row", that `{ "type": "record:details", "columns": "2" }` "passes the tolerant face", and that `disabled` (`element:button`) and `visible` (`record:alert`) are judged against the base's own types, no longer describes the release as a whole: all of those are refused, on both faces, and the message names the bag member. `label` (`element:button`) is the exception, because `@objectstack/spec`'s page component declares a node-level `label` of its own. The rest of this entry is kept as the reading of this change.

⚠️ **Dated note, 2026-10-03 — `record:line_items` armed — objectui#10872.** Later in this same release `@objectstack/spec` 17.6.0 carries a `ComponentPropsMap` row for `record:line_items`, and the block is armed from that row (`.changeset/10872-record-line-items-arm.md`). So the "Not armed in this release" bullet's `record:line_items` item ("the spec has no row for it yet"), and the 2026-09-30 note's "only `record:line_items` is still refused at `type`", no longer describe the release as a whole: none of the held public blocks is still refused at `type`. The rest of this entry is kept as the reading of this change.
17 changes: 17 additions & 0 deletions .changeset/10872-record-line-items-arm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@object-ui/types': minor
---

`safeValidateSchema`, and so `objectui validate`, accepts `record:line_items`, the last ADR-0080 public block it refused at `type` (objectui#10872). `@objectstack/spec` 17.6.0 carries the block's `ComponentPropsMap` row, measured at the reads of `LineItemsPanel`, and the arm reads that row by reference.

**Clause-②: yes.** The accept set of `AnyComponentSchema` widens by one `type` literal, and `@object-ui/types/zod` exports one new schema, `RecordLineItemsBlockSchema`. Nothing that parsed before is refused now: every document naming `record:line_items` was refused before.

**What it was.** `record:line_items` is registered by `@object-ui/plugin-form` and curated as platform contract by ADR-0080, and `AnyComponentSchema` carried no arm for it. Every document naming it was refused with `invalid_union` at `type`, whatever the rest of the node said. The arm was held until the spec carried a row for the block.

**What changed, in observable terms.**

- `@object-ui/types/zod` exports `RecordLineItemsBlockSchema`, a member of `PublicBlockComponentSchema`. The strict authoring face (`StrictAnyComponentSchema`) accepts the same documents and is closed to undeclared keys, as on every other arm.
- The block's props live in `properties`, and the bag is the spec row: its members, value types and strictness are the spec's. `relationshipField` and `columns` are required there, and `childObject` is optional. A key the row does not declare is refused inside the bag, by name.
- A row member written on the node instead of in `properties` is refused by name on both faces, at its own path, with a message naming `properties.KEY`, as on every other public block.
- The node's `dataSource` binding is declared as the spec's `ElementDataSourceSchema`, read by reference. The block's registration wraps `ElementDataSourceGate`, which puts `dataSource.object` into `childObject`, so a bound node without `childObject` passes both faces. A binding that the spec refuses is refused at `dataSource`.
- A node-level `children` or `body` is refused by name on both faces. The block renders the grid of child records and reads neither key, and `@objectstack/spec`'s page component refuses a node-level `children` on it too.
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,11 @@ const REFUSED_AT_TYPE = 0;
* it. objectui#11440 armed seven bare keys and none of their namespaced twins
* (`ARMED_11440` maps each to its twins), so its first pull request did not
* move this pin; its second took it down by one, `plugin-report:spec-report`,
* the namespaced twin of the `spec-report` key it unregistered.
* the namespaced twin of the `spec-report` key it unregistered. objectui#10872:
* minus `record:line_items`, armed from the row `@objectstack/spec` 17.6.0
* gave it, the last public block held.
*/
const NAMESPACED_REFUSED_AT_TYPE = 361;
const NAMESPACED_REFUSED_AT_TYPE = 360;

/** The bare registry keys — the population the card measured. */
const BARE_KEYS = KNOWN_SCHEMA_TYPES.filter((key) => !key.includes(':'));
Expand All @@ -170,6 +172,9 @@ const ARMED_PUBLIC_BLOCKS_10872_BATCH_4 = [
'action:button', 'action:icon', 'action:group', 'action:menu', 'element:definition-list', 'element:repeater',
] as const;

/** The last public block objectui#10872 held, armed once `@objectstack/spec` 17.6.0 carried its row. */
const ARMED_PUBLIC_BLOCKS_10872_LINE_ITEMS = ['record:line_items'] as const;

/**
* The thirty bare keys objectui#10859 batch 8 RETIRED (the seat's ruling on the
* card), named so the row below says which keys left the population and how:
Expand Down Expand Up @@ -549,6 +554,16 @@ describe('registered NAMESPACED component types refused at `type` — a ratchet
}
});

it('counts `record:line_items` armed — the last public block held (objectui#10872)', () => {
for (const key of ARMED_PUBLIC_BLOCKS_10872_LINE_ITEMS) {
expect(NAMESPACED_KEYS, key).toContain(key);
expect(refusedAtType(key), key).toBe(false);
}
// Registered with `skipFallback: true`, so the namespaced key is its only
// spelling: it left the namespaced count and no bare twin moved.
expect(BARE_KEYS).not.toContain('line_items');
});

it('counts `cloud:plan-status` armed — it registered WITH its arm (objectui#10919)', () => {
// One registry key (`skipFallback: true`, so no bare `plan-status`), armed in
// `@object-ui/types/zod` in the same change, so the pin above did not move.
Expand Down
41 changes: 40 additions & 1 deletion packages/types/src/__tests__/any-component-union-fanout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@
* unknown `type` refusal 14,855 -> 2,178 chars
* refused node 4 deep 19,311 -> 4,330 chars
*
* ⚠️ objectui#10872 moved the unknown-`type` bound, and its reading is added here
* rather than written over the one above. The discriminated refusal's message
* carries the discriminator's `options` list, which names every `type` literal
* the union claims, so the whole message grows by one literal per arm that
* lands: 2,178 chars when the row above was taken, 4,007 at 204 literals, past
* the 4,000 bound. So that bound now reads the message LESS its `options` list.
* The flat union carries no `options` at all. Same document, zod 4.6.5, a flat
* `z.union` over the 20 members `AnyComponentSchema` discriminates today (with
* its `Invalid input` message map) -> the discriminated union:
*
* unknown `type` refusal, less `options` 14,126 -> 193 chars (20 arm lists -> none)
*
* A bound that also passed on the flat union would assert nothing, which is the
* failure mode this card was most exposed to. ⚠️ The reason it was exposed has
* since changed and this paragraph is corrected in place rather than deleted:
Expand Down Expand Up @@ -75,6 +87,28 @@ function issueNodeCount(issues: readonly { errors?: readonly (readonly unknown[]
}, 0);
}

/**
* A refusal's message with the discriminator's `options` list taken out
* (objectui#10872). Zod builds the message as the issue list printed as JSON
* with a two-space indent, so it is parsed back, each issue's `options` is
* dropped, and the rest is printed the same way. The round trip is asserted
* first, so the reading is of the message itself and not of a re-serialisation
* that drifted from it.
*/
function messageLessOptions(message: string): string {
const issues = JSON.parse(message) as Record<string, unknown>[];
expect(JSON.stringify(issues, null, 2)).toBe(message);
return JSON.stringify(
issues.map((issue) => {
const rest = { ...issue };
delete rest.options;
return rest;
}),
null,
2,
);
}

/** The `type` literals a schema declares to Zod's discriminator dispatch. */
function literalsOf(schema: unknown): string[] {
const values = (schema as { _zod?: { propValues?: { type?: Set<string> } } })._zod?.propValues
Expand Down Expand Up @@ -153,7 +187,12 @@ describe('AnyComponentSchema — a refusal costs one arm, not every arm', () =>
const result = AnyComponentSchema.safeParse(FOREIGN_DOCUMENT);
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.message.length).toBeLessThanOrEqual(4_000);
// objectui#10872: the bound is on the message LESS the discriminator's
// `options` list. That list grows by one literal with every arm that lands,
// by design, while the rest of the message does not; the flat union carries
// no `options` and reads far above the bound (the docblock's readings), so
// the assertion still tells the two shapes apart.
expect(messageLessOptions(result.error.message).length).toBeLessThanOrEqual(4_000);
// The literals stay ON the issue, which is where `@object-ui/cli`'s
// `union-arm-diagnostics` reads them to build the CAPPED candidate list the
// 2026-09-02 maintainer ruling requires. Only the default MESSAGE — which
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/__tests__/flat-props-refusal-10872.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ const VALID_BAG: Readonly<Record<string, Record<string, unknown>>> = {
'record:quick_actions': { actionNames: ['edit'] },
'record:reference_rail': { entries: [{ objectName: 'contact', relationshipField: 'account' }] },
'record:alert': { severity: 'warning', title: 'Overdue', visible: true },
'record:line_items': {
childObject: 'order_line',
relationshipField: 'order',
columns: [{ name: 'qty', type: 'number' }],
title: 'Lines',
readonly: false,
limit: 50,
},
'element:text': { content: 'Hello' },
'element:number': { object: 'order', aggregate: 'count' },
'element:button': { label: 'Go', disabled: true },
Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/__tests__/imported-defaults-8317.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ import {
RecordQuickActionsProps as SpecRecordQuickActionsProps,
RecordReferenceRailProps as SpecRecordReferenceRailProps,
RecordAlertProps as SpecRecordAlertProps,
// objectui#10872 — the row `@objectstack/spec` 17.6.0 carries.
RecordLineItemsProps as SpecRecordLineItemsProps,
ElementTextPropsSchema as SpecElementTextPropsSchema,
ElementButtonPropsSchema as SpecElementButtonPropsSchema,
// objectui#10872 batch 4 — the six rows `@objectstack/spec` 17.5.0 carries.
Expand Down Expand Up @@ -329,6 +331,9 @@ const IMPORTED: Array<readonly [string, z.ZodType]> = [
['RecordQuickActionsProps', SpecRecordQuickActionsProps],
['RecordReferenceRailProps', SpecRecordReferenceRailProps],
['RecordAlertProps', SpecRecordAlertProps],
// objectui#10872: the `record:line_items` arm reads its row as its
// `properties` bag, crossed through this boundary like every other row.
['RecordLineItemsProps', SpecRecordLineItemsProps],
['ElementTextPropsSchema', SpecElementTextPropsSchema],
['ElementButtonPropsSchema', SpecElementButtonPropsSchema],
// objectui#10872 batch 4: the six held blocks' arms read their rows as their
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/__tests__/public-block-arms-10872.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ const VALID_BAG: Readonly<Record<string, Record<string, unknown>>> = {
'record:quick_actions': { actionNames: ['edit'] },
'record:reference_rail': { entries: [{ objectName: 'contact', relationshipField: 'account' }] },
'record:alert': { severity: 'warning', title: 'Overdue' },
// objectui#10872 — the row `@objectstack/spec` 17.6.0 carries; its own
// readings are pinned in `./record-line-items-arm-10872.test.ts`.
'record:line_items': { childObject: 'order_line', relationshipField: 'order', columns: [{ name: 'qty' }] },
'element:text': { content: 'Hello' },
// objectui#10872 batch 2 — the bag that names its own `object`; the
// `dataSource` waiver is pinned in `./element-number-arm-10872.test.ts`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ const DECLARES_DATA_SOURCE: ReadonlySet<string> = new Set([
// node level, beside their bags.
'object-pivot',
'embeddable-form',
// objectui#10872: the gate-wrapped `record:line_items`, armed with the binding
// at node level beside its bag — `ElementDataSourceGate` lands
// `dataSource.object` on the row's optional `childObject`.
'record:line_items',
]);

/**
Expand Down
Loading
Loading