Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit b403bb3

Browse files
objectstack-fleet[bot]claudeos-zhuang
authored
feat(types)!: BaseSchema loses its index signature, so a typed node literal refuses an undeclared key; the compile-fail pin, both directions (objectui#8347) (#11607)
Fixes #8347 Clause-②: yes (narrowing) `BaseSchema` no longer declares `[key: string]: any`. A node literal annotated with its node type now refuses a key that no declaration names, a misspelled key included, and the correct spelling still compiles. This executes the objectui#7927 ruling ("the TypeScript face is an executable contract"), with the compile-fail pin that ruling made non-negotiable. `ComponentRendererProps` keeps its own signature, and a row in the pin notices if it is ever removed. The zod faces and every runtime path are unchanged, except `visibleWhen` (below). Governed: this diff touches `AGENTS.md` and two published-skill files (`skills/objectui/guides/architecture.md`, `skills/objectui/rules/protocol.md`), so the PR stays a draft until an authorized approval is on it (`node scripts/check-governed-queue-guard.mjs --test` answers GOVERNED for each of them). Session: `https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ` (PM loop round 5, claim `5977679107`, amended for patch round 3 by `5979679543`). ## The maintainer's one open choice: the `props` alias on the TypeScript face This PR writes **option A**: `props` is not declared on the TypeScript authoring face. The renderer still reads it at runtime (the maintainer's 2026-08-18 ruling binds the runtime and leaves the TS face open). So the two `badge` literals in `components`' `alias-precedence-cross-channel.test.tsx` that author `props` now carry an `@ts-expect-error` naming this choice, and `AGENTS.md` §4's abridged listing drops the `props` member that `BaseSchema` never declared. - **A (written here):** not declared. The dev measurement on objectui#11364 found no non-test TypeScript, tracked JSON or objectstack example that authors a node `props`, and the published skill teaches it as wrong. - **B:** `props?: unknown` on `BaseSchema`. `AuthoringNode` derives from the zod `BaseSchema` shape, so B also needs a zod mirror row and a parity entry. - **C:** declared on `AuthoringNode` only. To flip it, remove the two directives and add the member. Nothing else in this PR depends on the choice. ## What changed, class by class (one commit each) 1. **The two src sites.** `plugin-kanban`: `ObjectKanban` reads the gate-written `sort` (the binding's `dataSource.sort`) through `GateBoundKanbanSchema`, a read type private to the file; `ObjectKanbanSchema` still declares no `sort` (objectui#8174). `plugin-timeline`: `TimelineRenderSchema`, the private hand-off type, declares the `onItemClick` slot `ObjectTimeline` composes; `TimelineSchema` still declares none. Both compile with the signature present or absent. 2. **The mechanical rows.** Two schema-to-record test reads convert through `unknown`; the gantt and map `propertiesBag-10859` fixtures keep `operator` a literal and type the bag node as its block node. 3. **`BaseSchema.visibleWhen`, Q6 = B.** The TS face is the spec's `EvaluatedExpressionInput`, by reference. The zod twin is the same accept set (a string, or the spec's own `EvaluatedExpressionSchema` envelope) with `EvaluatedExpressionInputSchema`'s verdict and without its string transform. 4. **The removal, and everything that can only land with it.** The compile-fail pin; the adjudicated probe and ceiling flips; the two `props` flips; the valueless envelope `metric-card` pinned refused on the TS face (Q3 = A); `envelopeStray` and its `complex.ts` note deleted per the constant's own removal condition; the two `object-view-slot-key-lists` tripwires flipped; `partial-schema-collapse-pin.test.ts` deleted per its header, with `PartialSchema`'s docblock describing the alias as it now works; five mirror-parity ledger entries (below); and the prose in those files that described the signature as live. 5. **Docs.** The 13 listed doc blocks plus the `objectos-integration.mdx` page block (now `regions`, and since round 4 rendered under `SchemaRendererProvider`); the published passages that described the signature as live; the plugin-calendar README note; the plugin-dashboard README widget-key passage; the `examples/dashboard.ts` comment; a dated note on the pending `11564` changeset, whose "until objectui#8347 removes it" sentence this release makes false. 6. **`AGENTS.md`.** §5 #0.1 gains the rule as shipped: the TS authoring face is part of the contract, a refused key is declared by reference or fixed at the producer and never cast past, and the fresh-literal bound is named with the pin that measures it. §4 drops `props`; #4's `events` sentence names the face that still keeps one. 7. **The changeset.** `minor` for `@object-ui/types` with the breaking note; `minor` for `plugin-timeline` (`TimelineRenderSchema`, the `schema` prop type of the exported `TimelineRenderer`, gains one optional member); `patch` for `plugin-kanban`. 8. **Patch round 3, after the merge of `main` at `b508ac50`.** The two published-skill sections (H3 below); the five restored deletion guards and the reworded test strings (Acceptance notes); `main` merged by a merge commit, with the type-check of every package `main` moved since the merge base green afterwards. 9. **Patch round 4, doc and comment bytes only.** The integration guide's App block installs the adapter through `SchemaRendererProvider dataSource` (the renderer reads it from `SchemaRendererContext`; a `dataSource` prop on `SchemaRenderer` reaches only the root component), and its system-objects comment names only the provider. The parity ledger's `complex.zod.ts#DashboardWidgetSchema` `component` entry says the mirror slot's `BaseSchema` fallback stays by objectui#8344's decision, with no open card retiring it, instead of reserving it for this card. ## The compile-fail pin, both directions `packages/types/src/__tests__/base-schema-closed-face-8347.test.ts`, read by `tsc -p tsconfig.test.json`: - objectui#7927's P5 on a FRESH literal: `titleFieldd` on a `CalendarViewSchema` literal is an `@ts-expect-error`, and `titleField` compiles. The same on `BaseSchema` itself (`classNmae` against `className`), on a node nested in a slot (`labell` on a `button` child), and on `PartialSchema`. - P5 as #7927 ran it, on today's plugin-calendar README block, stays green with or without the signature (measured on this card's census): that block is a type alias plus two assignments of declared variables, and tsc never excess-checks those. So the pin uses fresh literals, and the bound itself is pinned as a directive-free row, so nobody reads the file as claiming more. - Ablation, run on the committed head through `ablation-replace.mjs` (anchor hit 1 to 0, on-disk blob moved, trap-restored to blob equal to HEAD, `git diff HEAD` empty): with `[key: string]: any` put back on `BaseSchema`, the types test program exits 2 with 52 errors, and every refusal row of the pin turns red (TS2578 on the four misspelling rows, TS2344 on the two no-index rows). At HEAD the same program exits 0. ## The measurements the dispatch named **H1, the re-census at BASE `2e818d0b5`, before any kept edit.** Instrument: every package built (turbo, forced: 40 of 40, 0 cached); the six `BaseSchema` lines deleted and nothing else; types rebuilt (dist keeps one signature, `ComponentRendererProps`'); then every tsc program the workspace type-check scripts name, one job each (90 jobs, apps/site's generators included, plus the four root programs). | | programs | red | unique errors | src | tests | |---|---|---|---|---|---| | BASE, signature removed alone | 90 | 13 | 81 in 44 files | 3 | 78 | | BASE control, signature present (the same 13) | 13 | 0 | 0 | | | | final head `e0a9af67`, removal kept | 90 | 0 | 0 | 0 | 0 | The 81 fall exactly into the predicted classes: src 3 (`ObjectKanban` `sort` twice, timeline `onItemClick`); tests 67 adjudicated probe and ceiling flips (types 49; charts `xAxisKy` twice and `objectNme`; dashboard `bogusKey` twice and `zzUndeclared7353` four times; grid `bogusKeyRefusedNowhere` and two `Expect` wrappers; list `visibleWhn` twice; map `enableClusterng`; tree `zzplTreeAbsentControl8655`, `objectNaem`; components `content` on `label`), 2 `props` flips, 3 mechanical rows (two schema-to-record casts, one mirror-parity ledger mismatch), and the 6 gantt and map fixture literals. No site needed a contract decision. `check:doc-snippets` under the removal at BASE: 14 of 777 blocks failed, the 13 listed plus the `objectos-integration.mdx` page block (BASE control: 0 failed); doc-examples, doc-types and skill-examples stayed green. **The parity ledger.** The mismatch was five pairs whose member is typed with an `interface` (`AppSchemaRendererNodeSchema.schema`; the `reports` pairs' `report`, `sections`, `chart`) against a `.passthrough()` mirror whose `z.input` carries a string index signature. TypeScript grants an implicit index signature to anonymous and mapped types and never to an interface. Measured: the declared type is refused, and a one-level anonymous copy of it fits the same mirror input. They are ledgered as an artifact of the assignability test, with that reading in each entry's docblock, and the header's derived figures moved with them. **H2, the objectui#11573 headroom pin.** `AnyComponentSchema`'s declaration on the compiler's counter: 12260 before, 12974 after (the `visibleWhen` twin), limit 900000. Read through a trap-deleted copy of the pin that writes its reading. **H3, skill fences.** `check:skill-examples` exits 0 under the removal at BASE and on every head since. Patch round 3 edits the two `skills/**` files whose prose the removal made false: the architecture guide's "JSON Protocol" section and the protocol rule's "Component Schema Structure" section said the published `BaseSchema` declares its members plus an index signature, and that `props` / `events` / `properties` reach a node through it. Both now state the two verdicts a key no member declares gets, one per face (a compile error in a fresh typed literal on the TypeScript face, with the fresh-literal bound said as AGENTS.md §5 #0.1 says it; kept and judged by the named reader on the tolerant zod face), and their teaching fences drop the three keys `BaseSchema` never declared and end on the same no-index-signature line as AGENTS.md §4. `props` keeps one sentence stating option A, so a flip to B or C is a one-sentence edit. Neither fence is a marked example; `check:skill-examples`, `check:skill-eval-tokens` and `check:skills-paths` stay green. **H4.** The flex rows' header and `examples/dashboard.ts` no longer say "until objectui#8347"; the flex test gains the row that removal promised (a `text` entry misspelled inside the list is refused, and the well-formed list's `text` entry compiles). **H5, `visibleWhen`.** `visible-when-spec-input-8347.test.ts`: the TS member is `Equal` to the spec's `EvaluatedExpressionInput`; on 13 probe values the mirror's verdict equals `EvaluatedExpressionInputSchema`'s and `PageComponentSchema.visibleWhen`'s, read in the same run (the probe set holds both verdicts); a blank string is refused with the spec's own sentence; a string parses to the same string, while the spec rewrites it (the control); an envelope parses to itself; and the envelope a spec parse writes is accepted by both component faces, a dialect-less one refused. ⚠️ This is a narrowing as well as a widening on the tolerant face: a blank `visibleWhen` string used to parse and is refused now, as the spec refuses it. ## Gates, on head `e0a9af67` The dev's local readings on that head. The gate verdict on the current head is its own check-runs. - type-check, every program (the H1 instrument): 90 of 90 exit 0, 0 errors. - `pnpm --filter @object-ui/types build`: exit 0. `pnpm --filter @object-ui/types test`: exit 0, `Test Files 353 passed (353)`, `Tests 9419 passed (9419)`. - vitest over the edited plugin packages (kanban, timeline, gantt, map, charts, dashboard, grid, list, tree), on the same file contents: exit 0, `Test Files 819 passed | 1 skipped (820)`. `components`: see the report. - `check:doc-snippets` `777 of 777 block(s) judged, 0 failed.`; `check:doc-examples` `35 compile, 87 fail, 87 of those declared`; `check:doc-types`, `check:skill-examples`, `check:readme-exports`, `check:spec-symbols`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:control-bytes`, `changeset:check`, `type-check:coverage`: exit 0. `check:new-line-citations`: `0 new citation(s)`. - `check-governed-queue-guard.mjs --test` over the changed paths: GOVERNED (`AGENTS.md`; since patch round 3 also the two `skills/**` files). ## Overlap PR #11069 (draft) edits `content/docs/guide/architecture.md`'s paragraph on a button's text key. This PR edits the same file's Type Safety block (the button literal writes `label`). Region-disjoint; whichever lands second merges `main`. ## Acceptance notes (read, not filed) - `ObjectChartBlock`'s pre-gate `schema` prop is `BaseSchema` (ruling `5810232641`). A fresh literal at that prop is now excess-checked against `BaseSchema` itself, so a correctly spelled `objectName` is refused there as much as the `objectNme` probe; in-tree callers pass typed nodes. The flipped probe's comment says so. - plugin-grid's `RemoveIndexSignature` in `ObjectGrid.tsx` strips nothing now, as its own test predicted; retiring it is cleanup left out of this PR. - `object-view-slot-key-lists`' `form` `Pick` list may move back to `Omit`, per its header's removal condition; not done here. - `PartialSchema` has no consumer outside its declaration (a tracked-files `git grep` for the name, with `SchemaByType` as the positive control). The enforce-or-remove card the deleted pin's header asks for is filed: objectui#11608 (`Blocked-by: #8347`). - Source comments, test comments, docs and the published skill that described the signature as live, or said objectui#8347 removes it, are repaired in this PR (patch rounds 2 and 3). The enumerating commands are `git grep -n -i "index signature"`, `git grep -n -F '[key: string]: any'` and `git grep -n 'objectui#8347 remove'` over `packages/`, `apps/`, `examples/`, `content/docs/` and `skills/`, CHANGELOGs and release notes excluded as append-only. What they still find is true at the head: other types' own signatures (`ComponentRendererProps`, `AuthUser`, `FormField`, a widget's own props type, the field-widget `data-` attribute signature, `QueryParams`' statements), the tolerant zod face's `.passthrough()`, and past-tense history. - The deletion guards the removal silenced are restored (patch round 3). While the signature stood, an `@ts-expect-error` over a tombstone or a typed member also failed (TS2578) when its member was deleted; since the removal the undeclared key satisfies the directive. `object-kanban-card-title-9606`, `object-grid-default-sort-tombstone-5861`, `object-grid-show-filters-retired-11068`, `object-grid-declared-keys-11068` and `gantt-flat-config-declared-keys` each gain a type-level row that is a compile error when its member is deleted and pins the declared or tombstone type; each was shown red by a trap-restored deletion of its member and green at HEAD (readings on the card). Three test titles and one assertion message that described the signature as live are reworded, assertion logic untouched. ## 维护者速读(草稿) **改了什么**:`@object-ui/types` 的 `BaseSchema` 去掉了 `[key: string]: any`。从此用 TypeScript 写节点时,拼错或未声明的键会直接编译报错,正确拼写照常通过;新增的钉子测试两个方向都钉住,并用"把签名加回去、钉子变红"的消融证明它真能失败。顺带:`visibleWhen` 改为直接引用 spec 的输入类型(字符串或 `{ dialect, source }` 信封);看板与时间轴两处运行期键改为在各自包内私有类型上声明;一批文档示例与说明改成在新约束下成立;AGENTS.md §5 #0.1 写明"TS 编写面也是契约"。已发布技能包(`skills/objectui`)里两处讲 `BaseSchema` 形状的教学文案同步改写——它们原先说该类型带索引签名、`props`/`events` 由签名承接,去掉签名后已不成立;因此本 PR 的受管面除 AGENTS.md 外还含 `skills/**`,同一份批准覆盖两者。 **为什么改**:这是 objectui#7927 的裁决(TS 面是可执行契约)的落地,也是本卡的全部范围。此前签名让任何拼写都能编译,AI 批量写错元数据时被静默放过。 **风险与代价(含回滚)**:对 TS 使用者是破坏性收窄(按仓规以 `minor` 发布并在 changeset 写明);运行期与 zod 宽容面不变,唯一例外是 `visibleWhen` 现在拒绝空字符串、接受 spec 的信封。已知边界:TS 只对"新鲜对象字面量"做多余键检查,经变量转手的值不会再查,已在 AGENTS.md 与钉子里写明。回滚:还原本 PR 即可,无数据迁移。 **席位意见**: **你要做的**:1)裁决 `props` 别名在 TS 面上的去留:本 PR 按方案 A(不声明)写,B 是在 `BaseSchema` 上声明 `props?: unknown`,C 是只在 `AuthoringNode` 上声明;改选只需删两条 `@ts-expect-error` 并加成员。2)批准后由认领席落地(受管面,草稿态等你的批准)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: os-zhuang <jack@objectstack.ai>
1 parent fd060f0 commit b403bb3

202 files changed

Lines changed: 2243 additions & 1677 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/11564-flex-bag-children-list.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ The type is the flat `FlexSchema` mirror's own `children` member, by reference,
1616
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.
1717

1818
**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.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/plugin-kanban': patch
4+
'@object-ui/plugin-timeline': minor
5+
---
6+
7+
**`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`.

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ Every node in the UI tree follows this shape (`@object-ui/types`):
7474
interface BaseSchema {
7575
type: string; // registry key: 'input', 'grid', 'card'
7676
id?: string; // DOM accessibility / event targeting
77-
props?: Record<string, any>; // element:* config envelope, not a general bag
7877
bind?: string; // data binding path: 'user.address.city'
7978
className?: string; // Tailwind overrides
8079
hidden?: boolean | ExpressionWire; // expression or boolean: "${data.role != 'admin'}"
8180
disabled?: boolean | ExpressionWire; // same wire as hidden
8281
children?: SchemaNode | SchemaNode[]; // layout slots; primitives admitted too
82+
// no index signature: a key no member declares is a compile error (#0.1)
8383
}
8484
```
8585

@@ -90,14 +90,15 @@ interface BaseSchema {
9090
- **#-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.)*
9191
- **#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`.
9292
- **#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.
9394
- **#1 — Protocol-agnostic.** Never hardcode `objectql.find()`. Use the DataSource interface; inject `dataSource` via `<SchemaRendererProvider dataSource={...} />`.
9495
- **#2 — Docs-driven.** For every feature/refactor, update package `README.md` **and** `content/docs/guide/*.md`. Not done until docs reflect the code.
9596
- **#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.
9697
- **#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:
9798
```json
9899
{ "type": "action:button", "properties": { "label": "Open details", "actionType": "url", "target": "/users/ada" } }
99100
```
100-
⛔ 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.
101102
- **#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).
102103
- **#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).
103104
- **#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.

‎content/docs/api/schema-reference.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ One row per declared member, in declaration order, so the list can be checked ag
9696
| `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`. |
9797
| `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. |
9898
| `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. |
100100
| `visibleOn` | `string` | Expression for conditional visibility. **Deprecated** (ADR-0089) — use `visibleWhen`. |
101101
| `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. |
102102
| `hiddenOn` | `string` | Expression for conditional hiding. |
@@ -109,7 +109,7 @@ Two things the table cannot show in a cell:
109109

110110
- **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.
111111
- **⚠️ `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.
113113

114114
---
115115

‎content/docs/components/data-display/list.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ required, so the bind-only list above was refused at `items`. The TypeScript fac
4242
<Callout type="warn">
4343
`wrapperClass` is now typed. The renderer has read it all along — it lands on the
4444
wrapper element around the list — but it went undeclared until objectui#7722,
45-
surviving on `BaseSchema`'s index signature and the zod mirror's `.passthrough()`.
45+
surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's `.passthrough()`.
4646
Declared as `string` on both faces, a **non-string** `wrapperClass` that used to
4747
parse green is now refused by name — on `ListSchema` and through
4848
`safeValidateSchema` alike. Pass the Tailwind class string; a number or an object

‎content/docs/components/form/date-picker.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ interface DatePickerSchema {
4949
<Callout type="warn">
5050
`wrapperClass` is now typed. The renderer has read it all along — it lands on the
5151
wrapper element around the date picker and its label — but it went undeclared until
52-
objectui#7722, surviving on `BaseSchema`'s index signature and the zod mirror's
52+
objectui#7722, surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's
5353
`.passthrough()`. Declared as `string` on both faces, a **non-string**
5454
`wrapperClass` that used to parse green is now refused by name — on
5555
`DatePickerSchema` and through `safeValidateSchema` alike. Pass the Tailwind class

‎content/docs/components/form/select.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface SelectSchema {
2727
<Callout type="warn">
2828
`wrapperClass` is now typed. The renderer has read it all along — it lands on the
2929
wrapper element around the select and its label — but it went undeclared until
30-
objectui#7722, surviving on `BaseSchema`'s index signature and the zod mirror's
30+
objectui#7722, surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's
3131
`.passthrough()`. Declared as `string` on both faces, a **non-string**
3232
`wrapperClass` that used to parse green is now refused by name — on `SelectSchema`
3333
and through `safeValidateSchema` alike. Pass the Tailwind class string; a number or

‎content/docs/components/form/switch.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface SwitchSchema {
2525
<Callout type="warn">
2626
`wrapperClass` is now typed. The renderer has read it all along — it lands on the
2727
wrapper element around the switch and its label — but it went undeclared until
28-
objectui#7722, surviving on `BaseSchema`'s index signature and the zod mirror's
28+
objectui#7722, surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's
2929
`.passthrough()`. Declared as `string` on both faces, a **non-string**
3030
`wrapperClass` that used to parse green is now refused by name — on `SwitchSchema`
3131
and through `safeValidateSchema` alike. Pass the Tailwind class string; a number or

‎content/docs/components/form/textarea.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface TextareaSchema {
2727
<Callout type="warn">
2828
`wrapperClass` is now typed. The renderer has read it all along — it lands on the
2929
wrapper element around the textarea and its label — but it went undeclared until
30-
objectui#7722, surviving on `BaseSchema`'s index signature and the zod mirror's
30+
objectui#7722, surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's
3131
`.passthrough()`. Declared as `string` on both faces, a **non-string**
3232
`wrapperClass` that used to parse green is now refused by name — on `TextareaSchema`
3333
and through `safeValidateSchema` alike. Pass the Tailwind class string; a number or

0 commit comments

Comments
 (0)