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
142 changes: 142 additions & 0 deletions .changeset/ui-widget-i18n-family-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
"@objectstack/spec": major
---

refactor(spec)!: retire the widget-registration vocabulary and five doorless i18n shapes — and KEEP `FieldWidgetProps`, which has a live consumer (#5055)

`@objectstack/spec/ui` published two vocabularies nothing in the protocol carried.
Both are removed — **10 emitted defs, 26 exported names** — and the generated
`references/ui/widget` page with them.

| file | removed |
|---|---|
| `ui/widget.zod.ts` | `WidgetManifest`, `WidgetLifecycle`, `WidgetEvent`, `WidgetProperty`, `WidgetSource` (its `npm` / `remote` / `inline` union) |
| `ui/i18n.zod.ts` | `I18nObject`, `PluralRule`, `NumberFormat`, `DateFormat`, `LocaleConfig` |

There was no carrier key for any of them. No schema declared a key whose value was
a widget manifest or a locale config, so no metadata document could reach these
shapes and nothing ever parsed one.

Three measurements, each re-run on `origin/main` immediately before the removal,
each with its controls passing in the same run:

1. **Static** — nothing under `packages/spec/src` imported `widget.zod` at all,
and every live import of `i18n.zod` names `I18nLabelSchema` or
`AriaPropsSchema` (both kept). `field.widget` is a `z.string()` naming a
component the **renderer** has registered; it has never referenced
`WidgetManifest`.
2. **Graph** — a BFS over the in-memory Zod graph from all 24 metadata-type roots
plus `defineStack`'s `ObjectStackSchema` reached **none** of them, while
`PageSchema` and `ObjectListViewSchema` resolved `direct` in the same run and a
synthetic carrier flipped every one of them. So "unreachable" was a fact about
the graph, not a broken walker.
3. **Call sites** — zero `.parse()` / `.safeParse()` in objectstack, objectui or
cloud outside these files' own unit tests. objectui's widget registry has
always carried its own runtime manifest (`RuntimeWidgetManifest` /
`RuntimeWidgetSource` in `@object-ui/types`, objectui#3161 / #4115), which
models different keys and never derived from these.

Business ruling (2026-08-06; window moved from protocol 18 to 17 on 2026-08-07):
widget **registration** belongs to the renderer, not to the protocol — the
protocol's job is the props contract a widget implements. Localisation is already
delivered the other way: `I18nLabelSchema` documents that translation keys are
generated by the framework at registration time and translations live in
translation files, and the live translation surface is `system/translation.zod.ts`,
which uses none of these shapes.

FROM → TO:

| removed | what to do instead |
|---|---|
| `WidgetManifest` / `WidgetLifecycle` / `WidgetEvent` / `WidgetProperty` / `WidgetSource` | nothing to author — name a widget with the string `field.widget` (or the view's `widget` override) and register the component with the renderer. In objectui that is `@object-ui/core`'s `WidgetRegistry` and `RuntimeWidgetManifest`. |
| `I18nObject` | write the default-language string on `label` / `description`; the framework generates the translation key. Translations go in translation files (`system/translation.zod.ts`). |
| `PluralRule` | not a protocol surface — plural forms live in the translation files your i18n runtime reads. |
| `NumberFormat` / `DateFormat` / `LocaleConfig` | nothing to author — no formatter ever read one. Use `Intl.NumberFormat` / `Intl.DateTimeFormat` directly, as `packages/formula`'s template engine already does. |

**No metadata document needs editing.** A stack that parsed before parses
byte-for-byte the same after: none of these was writable in the first place, and
`field.widget: my_picker` is untouched. The break is a TypeScript one — every
removed name is `TS2305` on `@objectstack/spec` and `@objectstack/spec/ui` after
upgrade.

## One of the nine widget sites is deliberately KEPT

`FieldWidgetProps` / `FieldWidgetPropsSchema` / `FieldWidgetPropsParsed` **stay**,
and the reason is worth reading, because the issue that scheduled this batch
listed the site for removal on evidence that had been overtaken one day earlier.

- It is a **React props contract**, not authorable metadata. It never appeared in
`authorable-surface/` or `json-schema.manifest/` at all — its `onChange` is a
`z.function()`, so no JSON Schema is emitted — so ADR-0049's question about a
declared-but-unenforced *authorable key* never applied to it. Having no
`.parse()` is its design, not its defect: a props contract is enforced by `tsc`
in the repo that implements it.
- It has a **live cross-repo consumer**. objectui PR #3289 (merged 2026-08-03)
renamed `@object-ui/fields`' validation slot from `errorMessage` onto this
contract's `error` with no alias, made the form renderer produce it, and pinned
the result in `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` against
`import type { FieldWidgetProps } from '@objectstack/spec/ui'` — deliberately,
so that "the day the spec stops exporting `FieldWidgetProps`, this file stops
compiling and the rename's reason is up for re-triage". Re-verified on objectui
`origin/main` 2026-08-07.

`AriaPropsSchema` and `I18nLabelSchema` are likewise untouched. `AriaProps` is the
one **real door** in `i18n.zod.ts` — carried as `aria:` on ~30 live shapes under
six metadata-type roots and closed by #4001 批 16.

## ⚠️ objectui needs a companion PR in the same window

Two objectui surfaces respond to this removal **by design**, not by accident:

- `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` asserts the spec
*still owns* `WidgetManifest` and `WidgetSource`, with the comment "if the spec
RETIRES one of these, the local dialect can take the natural name back… a
workaround should not outlive its reason (objectui#3169)". That assertion is
meant to go red exactly here.
- `packages/types/src/widget.ts`'s "Renamed off the spec's `WidgetManifest` /
`WidgetSource` name" docblocks now point at names that no longer exist.

Neither is collateral damage — both are the tripwire firing as specified. The
objectui side is tracked separately; this repo cannot land it.

The retirement kit:

- **No `retiredKey()` tombstone, deliberately** — route 3 of the retirement
playbook ("nothing parses it → neither"), as used by #4988 (the ui/
interaction-config family), #4834 / PR #4878 (kernel plugin-runtime family) and
#4938 / PR #5293 (`HttpServerConfig`). A tombstone is a message to whoever
writes the key; with no carrier key there is no shape for one to sit on and no
author who could ever receive it.
- **No ADR-0087 D2 conversion**, for the same reason: there is no source to
rewrite, because the keys were unwritable. The registered record is the D3
`SemanticMigration` `ui-widget-i18n-family-retired`, with the protocol-17 step's
rationale extended, plus the ten `RETIRED_DEFS_BY_MAJOR` entries the #4725
manifest-deletion gate reads.
- **`WidgetManifest.performance`'s tombstone is subsumed, not deleted in
isolation** — the #4657/#4834 shape. It goes with the shape that carried it,
which is strictly stronger: there is no longer a manifest to author the key
into, so the prescription an author needs is no longer "delete this key".
- **Whole-file deletion was rejected per file, not assumed.** Unlike #4988, both
files here keep a live occupant, so this is a shape retirement and the files
stay. That is asserted, not just intended.
- Baselines updated deliberately: `json-schema.manifest/ui.json` (−10, the #2978
ratchet fires first and demands each deletion), `authorable-surface/ui.json`
(−65, adjudicated by the #4650 gate's path 3 "def no longer emitted by this
build"), `api-surface/ui.json` (−26). Reference docs, `references/ui/meta.json`,
the skill reference indexes and the strictness-ledger counts regenerated — the
`no door` bucket goes 14 → 1.
- `packages/spec/variant-docs.json`'s `type:inline|npm|remote` entry is deleted
with the discriminated union it described. A ledger row whose union has left the
source is the #5552 failure mode; `pnpm check:variant-docs` is the gate.
- **Pins are bidirectional.** `ui/widget-i18n-retirement.test.ts` asserts absence
across every public entry by resolved symbol identity *and* the survival of the
three shapes a too-wide sweep would take — all three of which live in the two
files being emptied. It also pins the exact `error` slot objectui#3289 depends
on, so a change that would silently break that repo goes red in this one first.
- The #5056 clone-overlap regression fixture was rebuilt rather than re-pointed:
`door-reachability.testkit.test.ts` constructs the same 2-of-19 shared-leaf
shape locally, so the instrument's measured bound survives its subject.

No runtime behaviour changes. That impossibility is the reason for the removal.

<!-- adr-0087: registered ui-widget-i18n-family-retired -->
2 changes: 1 addition & 1 deletion content/docs/getting-started/quick-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Presentation layer - views, forms, dashboards, and themes.
| **[Component](/docs/references/ui/component)** | `component.zod.ts` | PageComponent variants | Reusable UI components |
| **[Chart](/docs/references/ui/chart)** | `chart.zod.ts` | Chart, ChartType | Chart definitions |
| **[Theme](/docs/references/ui/theme)** | `theme.zod.ts` | Theme, ColorPalette | Theming and branding |
| **[Widget](/docs/references/ui/widget)** | `widget.zod.ts` | WidgetManifest | Custom widget definitions |
| **[Widget Contract](/docs/protocol/objectui/widget-contract)** | `widget.zod.ts` | FieldWidgetProps | Props a custom field widget receives |

## Kernel Protocol (17 schemas)

Expand Down
2 changes: 1 addition & 1 deletion content/docs/protocol/objectui/concept.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ function renderField(field: FieldDefinition) {
### For Architects

- [ObjectQL Integration](/docs/protocol/objectql) - How ObjectUI leverages ObjectQL schemas
- [Widget Reference](/docs/references/ui/widget) - Widget contract for building renderers
- [Widget Contract](/docs/protocol/objectui/widget-contract) - The props a custom field widget receives
- [Sharing & Permissions](/docs/references/ui/sharing) - Field-level and view-level access

### For Developers
Expand Down
2 changes: 1 addition & 1 deletion content/docs/protocol/objectui/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ mobile_renderer:
Building a renderer for ObjectUI?

- [Component Reference](/docs/references/ui/component) - Widget contract and standard props
- [Widget Reference](/docs/references/ui/widget) - Dashboard widget schema
- [Dashboard Reference](/docs/references/ui/dashboard) - Dashboard widget schema
- [Widget Contract](/docs/protocol/objectui/widget-contract) - Standard props and events

### For Users
Expand Down
Loading
Loading