You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .changeset/11564-flex-bag-children-list.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,3 +16,5 @@ The type is the flat `FlexSchema` mirror's own `children` member, by reference,
16
16
What does NOT move: every zod face and every runtime path. `FlexBlockSchema` keeps `z.array(z.unknown())` for the list, because `@objectstack/spec`'s page walk already judges each entry there, once, at its real path, on the tolerant and the strict face (objectui#11223). The one entry kind where the faces now differ is a nested list, which the walk passes through unvisited and the TypeScript face refuses; `@object-ui/types`' mirror-parity ledger records the divergence.
17
17
18
18
**Migration.** Give each list entry its declared node type (or `DeclaredNode`), declare a custom type in `CustomNodeRegistry`, and flatten a nested list into the one list.
19
+
20
+
⚠️ **Dated note, 2026-10-04 — `BaseSchema` loses its index signature — objectui#8347.** At this change, "Node types that extend `BaseSchema` keep its index signature until objectui#8347 removes it, so a misspelled key on one of them still compiles" held. Later in this same release objectui#8347 removed that signature, so a misspelled key on a node type that extends `BaseSchema` no longer compiles either, in the list exactly as in a single child. `.changeset/8347-baseschema-closed-face.md` states what ships. The rest of this entry is kept as the reading of this change.
**`BaseSchema` no longer declares `[key: string]: any`** (objectui#8347, executing the objectui#7927 ruling: the TypeScript face is a contract). Every node type extends `BaseSchema`, so a node literal annotated with its node type now refuses a key that no declaration names, a misspelled key included, where it used to type it `any`. The correct spelling compiles as before.
8
+
9
+
**Clause-②: yes (narrowing)**, shipped as `minor` per this repository's version policy. The removal narrows the TypeScript authoring face of every node type; the `visibleWhen` change below widens both faces to the envelope the spec's own parse writes.
10
+
11
+
-**What does not move.** The zod faces keep their accept sets for every key but `visibleWhen`: the tolerant mirror is still `.passthrough()`, so `safeValidateSchema` keeps an undeclared key, and the derived strict face refuses it as before. `ComponentRendererProps`, the renderer props type, keeps its own index signature. Nothing a renderer draws changes.
12
+
-**The bound.** TypeScript runs its excess-property check only on a fresh object literal. A value that reached its annotation through a variable of a wider type is not re-checked.
13
+
-**`PartialSchema<T>` works as written.** With the signature gone, `keyof T` is the literal member union again, so the alias keeps `T`'s declared members, optional, with `type` required. While the signature stood it declared `type` alone (objectui#6397).
14
+
-**`BaseSchema.visibleWhen` is the spec's `EvaluatedExpressionInput`**, by reference: a predicate string, or the `{ dialect, source }` envelope. The zod twin takes `EvaluatedExpressionInputSchema`'s verdict without its transform, so a string parses to itself. A dialect-less envelope, an unknown dialect and a blank predicate are refused, as the spec refuses them. Both faces read `string` before, which refused the envelope a spec parse writes into this key.
15
+
-**`@object-ui/plugin-kanban`.**`ObjectKanban` reads the `sort` the element data-source gate writes through a read type private to the package. `ObjectKanbanSchema` still declares no `sort` (objectui#8174). Nothing drawn changes.
16
+
-**`@object-ui/plugin-timeline`.**`TimelineRenderSchema`, the `schema` prop type of the exported `TimelineRenderer`, gains one optional member: the `onItemClick` slot `ObjectTimeline` composes. That is a one-member optional widening of an exported prop type. `TimelineSchema`, the authoring face, still declares no `onItemClick`. Nothing drawn changes.
17
+
18
+
**Migration.** Where a literal stops compiling, the key is misspelled (fix it) or not declared on that node type (declare it on the type that reads it, by reference to the `@objectstack/spec` row, or remove it). Do not cast past the error. `props`, the legacy alias of `properties`, is not declared on the TypeScript face; the renderer still reads it, so write `properties`.
id?:string; // DOM accessibility / event targeting
77
-
props?:Record<string, any>; // element:* config envelope, not a general bag
78
77
bind?:string; // data binding path: 'user.address.city'
79
78
className?:string; // Tailwind overrides
80
79
hidden?:boolean|ExpressionWire; // expression or boolean: "${data.role != 'admin'}"
81
80
disabled?:boolean|ExpressionWire; // same wire as hidden
82
81
children?:SchemaNode|SchemaNode[]; // layout slots; primitives admitted too
82
+
// no index signature: a key no member declares is a compile error (#0.1)
83
83
}
84
84
```
85
85
@@ -90,14 +90,15 @@ interface BaseSchema {
90
90
-**#-1 — English-only codebase.** This is an international OSS project. All user-facing text (component labels, buttons, titles, errors), code comments, docs (`README.md`, `docs/*.md`), and console/log messages MUST be English. No Chinese or other non-English in those. *(This rule governs the **codebase**; this instruction file may use Chinese in operational sections.)*
91
91
-**#0 — Strict adherence to `@objectstack/spec`.** All schemas/JSON structures/types MUST follow `@objectstack/spec`. Don't invent schema properties — if the spec says `columns`, don't use `fields`. Check the spec before writing any `interface`/`type`.
92
92
- **#0.1 — Fix the metadata, not the renderer (contract-first).** Corollary to #0. This is a metadata-driven system: `@objectstack/spec` is the contract between producers and this renderer. When a piece of metadata "doesn't render," ask **first**: *is it spec-compliant? is this the long-term-correct direction?* If the metadata is off-spec, fix it at the **producer** (and have it rejected at authoring/publish) — do **not** add a lenient fallback/alias in the renderer (reading both `columns` and `fields`, coercing a malformed shape, `??`-defaulting around bad input) to make non-compliant metadata "work." A tolerant fallback fossilizes the wrong convention into a second de-facto contract, dilutes the spec, and hides the producer's bug — one strict contract beats N dialects. We own both ends, so Postel's "be liberal in what you accept" does **not** apply (that's for untrusted boundaries). Change the **spec** only when it is genuinely wrong — deliberately, in `@objectstack/spec`, never by accreting renderer-side fallbacks.
93
+
- **The TypeScript authoring face is part of the contract (objectui#7927 ruling, executed by objectui#8347).** `BaseSchema` declares no index signature, so a node literal typed with its node type refuses, at the authoring site, a key no declaration names — a misspelling included. When a key you need is refused, this rule applies unchanged: declare it on the node type that reads it, by reference to the spec row, or fix the producer; a key only the host writes at runtime is typed on a package-local hand-off type where its producer and consumer live, never on the authoring face (objectui#6356). ⛔ Never make it compile with `as any`, a cast to `Record<string, unknown>`, a re-added index signature or an open `type: string` arm. Tolerance stays where it is deliberate: renderer props (`ComponentRendererProps` keeps its signature) and the tolerant zod face (`.passthrough()`); the derived strict zod face, `StrictAnyComponentSchema`, refuses an unknown key in metadata that arrives as data. ⚠️ The bound, said per #9: TypeScript excess-checks only a FRESH object literal, so a value that reached its annotation through a wider variable is not re-checked. `packages/types/src/__tests__/base-schema-closed-face-8347.test.ts` pins both directions and that bound.
93
94
-**#1 — Protocol-agnostic.** Never hardcode `objectql.find()`. Use the DataSource interface; inject `dataSource` via `<SchemaRendererProvider dataSource={...} />`.
94
95
-**#2 — Docs-driven.** For every feature/refactor, update package `README.md`**and**`content/docs/guide/*.md`. Not done until docs reflect the code.
95
96
-**#3 — "Shadcn-native" aesthetics.** We are "serializable Shadcn". Follow Shadcn's DOM structure (`CardHeader`/`CardTitle`/`CardContent`). Always expose `className` in schema props so users can override via JSON.
96
97
-**#4 — Action system (objectui#7898, objectui#6497, objectui#11183).** Actions are **data, not functions**, and a control that RUNS something is its own NODE TYPE — `action:button` — never a handler key or an event bag on an ordinary node. The block's props live in the node's `properties` bag, which is the spec's `ComponentPropsMap` row for it: `actionType` names the executor the action runner dispatches to, and the row's other keys carry that executor's arguments. ⛔ Never write them flat on the node: the spec's `PageComponentSchema` is `.strict()` (ADR-0089 D3a) and refuses a node-level `actionType` / `target` as mis-layered, and objectui's strict authoring face refuses the same two keys. The runtime reads both spellings through the `SchemaRenderer``properties` hoist, so the bag costs nothing at render:
⛔ Never author an `events` bag: `BaseSchema` declares no `events` member and no renderer reads `schema.events` — the node is `.passthrough()`, so one authored there is kept, judged by nothing and run by nothing. `ButtonSchema.onClick` is a runtime slot for a host-supplied function and is refused by name for the same reason.
101
+
⛔ Never author an `events` bag: `BaseSchema` declares no `events` member and no renderer reads `schema.events` — a typed literal refuses it (#0.1), but the tolerant zod face is `.passthrough()`, so one that arrives as data is kept, judged by nothing and run by nothing. `ButtonSchema.onClick` is a runtime slot for a host-supplied function and is refused by name for the same reason.
101
102
-**#5 — Layout as components.** Treat `Grid`/`Stack`/`Container` as first-class. Layout schemas declare responsive columns on the node as `columns` — a number, or a breakpoint object (`columns: { xs: 1, md: 2, lg: 4 }`); never `cols`, which nothing reads (objectui#4001).
102
103
-**#6 — Type safety over magic.** No `any` — use strict generics. Map `"type": "button"` → React component via a central `ComponentRegistry`. **No `eval()` / runtime dynamic imports** to load components (security).
103
104
-**#7 — No-Touch zones (Shadcn purity).**`packages/components/src/ui/**/*.tsx` are upstream 3rd-party files overwritten by sync scripts — **never edit their logic/styles**. To change `Button`/`Dialog` behavior: create/edit a wrapper in `packages/components/src/custom/`, import the primitive from `@/ui/...`, and wrap it.
Copy file name to clipboardExpand all lines: content/docs/api/schema-reference.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -96,7 +96,7 @@ One row per declared member, in declaration order, so the list can be checked ag
96
96
|`body`|*retired*| ⛔ Refused by name (objectui#6771). `body` was a second child-list spelling `BaseSchema` declared beside `children`; it is now `never` on the TypeScript face and an alias refusal on the Zod mirror, and the refusal names `children`. |
97
97
|`children`|`SchemaNode \| SchemaNode[]`| Child components rendered inside this component — the child-list key, and since objectui#6771 the only one. Whether a given node type renders a child list at all is still per component; see the note below. |
98
98
|`visible`|`boolean \| string \| { dialect?: string; source: string }`| Visibility control. Accepts a boolean, a predicate expression string, **or** the CEL envelope object (`{ dialect: 'cel', source }` — what `objectstack build` emits for every authored predicate) — the renderer evaluates this key rather than reading it as a boolean. The string-or-envelope half is `ExpressionWire`, the one wire type `visibleWhen` on form fields already carries. |
99
-
|`visibleWhen`|`string`| Canonical conditional-visibility predicate (ADR-0089); the element is shown when it evaluates truthy. Evaluated **before**`visible` and `visibleOn`, and outranks both. |
99
+
|`visibleWhen`|`string \| { dialect, source }`| Canonical conditional-visibility predicate (ADR-0089); the element is shown when it evaluates truthy. Typed as `@objectstack/spec`'s `EvaluatedExpressionInput`: a predicate string, or the envelope the spec's parse writes (`dialect` is `cel`, `cron` or `template`, and `source` is not blank). Evaluated **before**`visible` and `visibleOn`, and outranks both. |
100
100
|`visibleOn`|`string`| Expression for conditional visibility. **Deprecated** (ADR-0089) — use `visibleWhen`. |
101
101
|`hidden`|`boolean \| string \| { dialect?: string; source: string }`| Inverse of `visible` — the node is not rendered. Accepts a boolean, a predicate expression string **or** the CEL envelope object (`ExpressionWire`), which the renderer evaluates rather than reading as a boolean; `hiddenOn` remains the sibling spelling. |
102
102
|`hiddenOn`|`string`| Expression for conditional hiding. |
@@ -109,7 +109,7 @@ Two things the table cannot show in a cell:
109
109
110
110
-**A concrete schema may narrow an inherited member, and its own declaration wins.** Many component schemas restate `label`, `description` or `disabled` more narrowly than `BaseSchema` declares them, so the unions above are what a node gets when its own schema does not restate the key. Check the component's own property table before writing a predicate string or a locale map into an inherited slot.
111
111
- **⚠️ `body` and `children` WERE two channels, not one key with two spellings — and the second one is retired (objectui#6771).** Each renderer read one, the other, both, or neither, and `SchemaRenderer` strips both out of the props bag it spreads — so writing the channel a renderer did not read rendered an EMPTY element, with no error at authoring time, none at validation time and none at render time. That is the defect objectui#8284 named after seven cards had repaired one page of it each, and it was closed per component by measurement: each schema narrows to the channel its renderer actually reads and **tombstones the other as `never`**, refused by name on both published faces (objectui#9254 for the components that read exactly one channel, objectui#9256 for the ones that read neither). objectui#6771 then closed the class at its source: `children` is the one child-list spelling and `body` is refused on `BaseSchema` itself. ⇒ **whether a node type accepts a child list at all is still per component** — check the component's own section, where a node that renders no children tombstones `children` too.
112
-
-**This list is exhaustive for *declared* members, not for *accepted* keys.**`BaseSchema` carries an index signature (`[key: string]: any`) and its Zod mirror is `.passthrough()`, so an undeclared key — a misspelling included — is still accepted by both halves. Absence from this table does not mean a key is rejected.
112
+
-**This list is exhaustive for *declared* members; the two faces differ on *accepted* keys.**`BaseSchema` carries no index signature (objectui#8347), so on the TypeScript face an undeclared key, a misspelling included, is a compile error in a typed literal. Its tolerant Zod mirror is still `.passthrough()`, so the rendering face (and `safeValidateSchema`) keeps an undeclared key; the derived strict face, `StrictAnyComponentSchema`, refuses it. Absence from this table does not mean the tolerant face rejects a key.
0 commit comments