|
| 1 | +--- |
| 2 | +"@objectstack/spec": major |
| 3 | +--- |
| 4 | + |
| 5 | +refactor(spec)!: retire the widget-registration vocabulary and five doorless i18n shapes — and KEEP `FieldWidgetProps`, which has a live consumer (#5055) |
| 6 | + |
| 7 | +`@objectstack/spec/ui` published two vocabularies nothing in the protocol carried. |
| 8 | +Both are removed — **10 emitted defs, 26 exported names** — and the generated |
| 9 | +`references/ui/widget` page with them. |
| 10 | + |
| 11 | +| file | removed | |
| 12 | +|---|---| |
| 13 | +| `ui/widget.zod.ts` | `WidgetManifest`, `WidgetLifecycle`, `WidgetEvent`, `WidgetProperty`, `WidgetSource` (its `npm` / `remote` / `inline` union) | |
| 14 | +| `ui/i18n.zod.ts` | `I18nObject`, `PluralRule`, `NumberFormat`, `DateFormat`, `LocaleConfig` | |
| 15 | + |
| 16 | +There was no carrier key for any of them. No schema declared a key whose value was |
| 17 | +a widget manifest or a locale config, so no metadata document could reach these |
| 18 | +shapes and nothing ever parsed one. |
| 19 | + |
| 20 | +Three measurements, each re-run on `origin/main` immediately before the removal, |
| 21 | +each with its controls passing in the same run: |
| 22 | + |
| 23 | +1. **Static** — nothing under `packages/spec/src` imported `widget.zod` at all, |
| 24 | + and every live import of `i18n.zod` names `I18nLabelSchema` or |
| 25 | + `AriaPropsSchema` (both kept). `field.widget` is a `z.string()` naming a |
| 26 | + component the **renderer** has registered; it has never referenced |
| 27 | + `WidgetManifest`. |
| 28 | +2. **Graph** — a BFS over the in-memory Zod graph from all 24 metadata-type roots |
| 29 | + plus `defineStack`'s `ObjectStackSchema` reached **none** of them, while |
| 30 | + `PageSchema` and `ObjectListViewSchema` resolved `direct` in the same run and a |
| 31 | + synthetic carrier flipped every one of them. So "unreachable" was a fact about |
| 32 | + the graph, not a broken walker. |
| 33 | +3. **Call sites** — zero `.parse()` / `.safeParse()` in objectstack, objectui or |
| 34 | + cloud outside these files' own unit tests. objectui's widget registry has |
| 35 | + always carried its own runtime manifest (`RuntimeWidgetManifest` / |
| 36 | + `RuntimeWidgetSource` in `@object-ui/types`, objectui#3161 / #4115), which |
| 37 | + models different keys and never derived from these. |
| 38 | + |
| 39 | +Business ruling (2026-08-06; window moved from protocol 18 to 17 on 2026-08-07): |
| 40 | +widget **registration** belongs to the renderer, not to the protocol — the |
| 41 | +protocol's job is the props contract a widget implements. Localisation is already |
| 42 | +delivered the other way: `I18nLabelSchema` documents that translation keys are |
| 43 | +generated by the framework at registration time and translations live in |
| 44 | +translation files, and the live translation surface is `system/translation.zod.ts`, |
| 45 | +which uses none of these shapes. |
| 46 | + |
| 47 | +FROM → TO: |
| 48 | + |
| 49 | +| removed | what to do instead | |
| 50 | +|---|---| |
| 51 | +| `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`. | |
| 52 | +| `I18nObject` | write the default-language string on `label` / `description`; the framework generates the translation key. Translations go in translation files (`system/translation.zod.ts`). | |
| 53 | +| `PluralRule` | not a protocol surface — plural forms live in the translation files your i18n runtime reads. | |
| 54 | +| `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. | |
| 55 | + |
| 56 | +**No metadata document needs editing.** A stack that parsed before parses |
| 57 | +byte-for-byte the same after: none of these was writable in the first place, and |
| 58 | +`field.widget: my_picker` is untouched. The break is a TypeScript one — every |
| 59 | +removed name is `TS2305` on `@objectstack/spec` and `@objectstack/spec/ui` after |
| 60 | +upgrade. |
| 61 | + |
| 62 | +## One of the nine widget sites is deliberately KEPT |
| 63 | + |
| 64 | +`FieldWidgetProps` / `FieldWidgetPropsSchema` / `FieldWidgetPropsParsed` **stay**, |
| 65 | +and the reason is worth reading, because the issue that scheduled this batch |
| 66 | +listed the site for removal on evidence that had been overtaken one day earlier. |
| 67 | + |
| 68 | +- It is a **React props contract**, not authorable metadata. It never appeared in |
| 69 | + `authorable-surface/` or `json-schema.manifest/` at all — its `onChange` is a |
| 70 | + `z.function()`, so no JSON Schema is emitted — so ADR-0049's question about a |
| 71 | + declared-but-unenforced *authorable key* never applied to it. Having no |
| 72 | + `.parse()` is its design, not its defect: a props contract is enforced by `tsc` |
| 73 | + in the repo that implements it. |
| 74 | +- It has a **live cross-repo consumer**. objectui PR #3289 (merged 2026-08-03) |
| 75 | + renamed `@object-ui/fields`' validation slot from `errorMessage` onto this |
| 76 | + contract's `error` with no alias, made the form renderer produce it, and pinned |
| 77 | + the result in `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` against |
| 78 | + `import type { FieldWidgetProps } from '@objectstack/spec/ui'` — deliberately, |
| 79 | + so that "the day the spec stops exporting `FieldWidgetProps`, this file stops |
| 80 | + compiling and the rename's reason is up for re-triage". Re-verified on objectui |
| 81 | + `origin/main` 2026-08-07. |
| 82 | + |
| 83 | +`AriaPropsSchema` and `I18nLabelSchema` are likewise untouched. `AriaProps` is the |
| 84 | +one **real door** in `i18n.zod.ts` — carried as `aria:` on ~30 live shapes under |
| 85 | +six metadata-type roots and closed by #4001 批 16. |
| 86 | + |
| 87 | +## ⚠️ objectui needs a companion PR in the same window |
| 88 | + |
| 89 | +Two objectui surfaces respond to this removal **by design**, not by accident: |
| 90 | + |
| 91 | +- `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` asserts the spec |
| 92 | + *still owns* `WidgetManifest` and `WidgetSource`, with the comment "if the spec |
| 93 | + RETIRES one of these, the local dialect can take the natural name back… a |
| 94 | + workaround should not outlive its reason (objectui#3169)". That assertion is |
| 95 | + meant to go red exactly here. |
| 96 | +- `packages/types/src/widget.ts`'s "Renamed off the spec's `WidgetManifest` / |
| 97 | + `WidgetSource` name" docblocks now point at names that no longer exist. |
| 98 | + |
| 99 | +Neither is collateral damage — both are the tripwire firing as specified. The |
| 100 | +objectui side is tracked separately; this repo cannot land it. |
| 101 | + |
| 102 | +The retirement kit: |
| 103 | + |
| 104 | +- **No `retiredKey()` tombstone, deliberately** — route 3 of the retirement |
| 105 | + playbook ("nothing parses it → neither"), as used by #4988 (the ui/ |
| 106 | + interaction-config family), #4834 / PR #4878 (kernel plugin-runtime family) and |
| 107 | + #4938 / PR #5293 (`HttpServerConfig`). A tombstone is a message to whoever |
| 108 | + writes the key; with no carrier key there is no shape for one to sit on and no |
| 109 | + author who could ever receive it. |
| 110 | +- **No ADR-0087 D2 conversion**, for the same reason: there is no source to |
| 111 | + rewrite, because the keys were unwritable. The registered record is the D3 |
| 112 | + `SemanticMigration` `ui-widget-i18n-family-retired`, with the protocol-17 step's |
| 113 | + rationale extended, plus the ten `RETIRED_DEFS_BY_MAJOR` entries the #4725 |
| 114 | + manifest-deletion gate reads. |
| 115 | +- **`WidgetManifest.performance`'s tombstone is subsumed, not deleted in |
| 116 | + isolation** — the #4657/#4834 shape. It goes with the shape that carried it, |
| 117 | + which is strictly stronger: there is no longer a manifest to author the key |
| 118 | + into, so the prescription an author needs is no longer "delete this key". |
| 119 | +- **Whole-file deletion was rejected per file, not assumed.** Unlike #4988, both |
| 120 | + files here keep a live occupant, so this is a shape retirement and the files |
| 121 | + stay. That is asserted, not just intended. |
| 122 | +- Baselines updated deliberately: `json-schema.manifest/ui.json` (−10, the #2978 |
| 123 | + ratchet fires first and demands each deletion), `authorable-surface/ui.json` |
| 124 | + (−65, adjudicated by the #4650 gate's path 3 "def no longer emitted by this |
| 125 | + build"), `api-surface/ui.json` (−26). Reference docs, `references/ui/meta.json`, |
| 126 | + the skill reference indexes and the strictness-ledger counts regenerated — the |
| 127 | + `no door` bucket goes 14 → 1. |
| 128 | +- `packages/spec/variant-docs.json`'s `type:inline|npm|remote` entry is deleted |
| 129 | + with the discriminated union it described. A ledger row whose union has left the |
| 130 | + source is the #5552 failure mode; `pnpm check:variant-docs` is the gate. |
| 131 | +- **Pins are bidirectional.** `ui/widget-i18n-retirement.test.ts` asserts absence |
| 132 | + across every public entry by resolved symbol identity *and* the survival of the |
| 133 | + three shapes a too-wide sweep would take — all three of which live in the two |
| 134 | + files being emptied. It also pins the exact `error` slot objectui#3289 depends |
| 135 | + on, so a change that would silently break that repo goes red in this one first. |
| 136 | +- The #5056 clone-overlap regression fixture was rebuilt rather than re-pointed: |
| 137 | + `door-reachability.testkit.test.ts` constructs the same 2-of-19 shared-leaf |
| 138 | + shape locally, so the instrument's measured bound survives its subject. |
| 139 | + |
| 140 | +No runtime behaviour changes. That impossibility is the reason for the removal. |
| 141 | + |
| 142 | +<!-- adr-0087: registered ui-widget-i18n-family-retired --> |
0 commit comments