Skip to content

Commit f7bd4e2

Browse files
qq9340100claude
andauthored
feat(spec)!: ADR-0049 enforce-or-remove —— ui/widget + ui/i18n 家族 13 退 1 留 (#5055) (#6385)
* refactor(spec)!: retire the widget-registration + doorless i18n vocabularies, keep FieldWidgetProps (#5055) ADR-0049 enforce-or-remove for the 14 sites #4001 批 16 measured as `no door`. Disposed per site against a re-measurement on origin/main: 13 REMOVED, 1 KEPT. Removed — 10 defs, 26 exported names: ui/widget.zod.ts WidgetManifest, WidgetLifecycle, WidgetEvent, WidgetProperty, WidgetSource (npm/remote/inline union) ui/i18n.zod.ts I18nObject, PluralRule, NumberFormat, DateFormat, LocaleConfig None had a carrier key: nothing under packages/spec/src imported widget.zod at all, every live import of i18n.zod names I18nLabelSchema or AriaPropsSchema, the BFS from all 24 metadata-type roots plus defineStack reached none of them (with PageSchema / ObjectListViewSchema resolving `direct` in the same run and a synthetic carrier flipping all of them), and no repo ever parsed one. So a published vocabulary described a widget-registration and locale-formatting capability the platform does not have — the #3950 shape. Kept — FieldWidgetProps. Its evidence differs and had been overtaken one day before 批 16 measured. It is a React props contract, never authorable (absent from authorable-surface/ and json-schema.manifest/ — onChange is a z.function()), so having no parse is its design; and objectui PR #3289 (2026-08-03) renamed @object-ui/fields' validation slot onto its `error` with no alias and pinned it as a deliberate compile-time tripwire. Re-verified on objectui origin/main. Route 3 ("nothing parses it → neither"), as #4988 / #4834: no retiredKey() tombstone and no D2 conversion, because there is no shape to tombstone and no source to rewrite. The declared record is the D3 SemanticMigration ui-widget-i18n-family-retired plus 10 RETIRED_DEFS_BY_MAJOR entries, which the #4725 manifest-deletion gate reads. WidgetManifest.performance's own tombstone (#3896) is subsumed by the removal of the shape that carried it. Baselines moved deliberately: json-schema.manifest/ui.json -10 (the #2978 ratchet fires first), authorable-surface/ui.json -65 (adjudicated by the #4650 gate's path 3), api-surface/ui.json -26. variant-docs.json's `type:inline|npm|remote` entry goes with the union it described (#5552's failure mode). Reference docs, meta.json, skill indexes and the strictness-ledger counts regenerated; the `no door` bucket goes 14 -> 1. 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 — FieldWidgetProps, AriaProps and I18nLabel, all three of which live in the two files being emptied. The #5056 clone-overlap fixture is rebuilt locally so the instrument's measured 2-of-19 bound survives its subject. No runtime behaviour changes; that impossibility is the reason for the removal. Refs #5055, #4001, #4988, ADR-0049, ADR-0087 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XUAV11R31jhzq6v1wb6SuJ * chore(changeset): declare the ADR-0087 disposition on the #5055 retirement (#6148) The new #6148 gate requires every changeset that declares a breaking change to answer the ledger question in writing. This one's answer is the first of the four accepted forms — the entry exists: the retirement is registered as the D3 SemanticMigration `ui-widget-i18n-family-retired` in packages/spec/src/migrations/registry.ts, alongside its ten RETIRED_DEFS_BY_MAJOR entries. Comment-only; no schema, artifact or test is affected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XUAV11R31jhzq6v1wb6SuJ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e8dc61e commit f7bd4e2

33 files changed

Lines changed: 873 additions & 1486 deletions
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 -->

content/docs/getting-started/quick-reference.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Presentation layer - views, forms, dashboards, and themes.
5252
| **[Component](/docs/references/ui/component)** | `component.zod.ts` | PageComponent variants | Reusable UI components |
5353
| **[Chart](/docs/references/ui/chart)** | `chart.zod.ts` | Chart, ChartType | Chart definitions |
5454
| **[Theme](/docs/references/ui/theme)** | `theme.zod.ts` | Theme, ColorPalette | Theming and branding |
55-
| **[Widget](/docs/references/ui/widget)** | `widget.zod.ts` | WidgetManifest | Custom widget definitions |
55+
| **[Widget Contract](/docs/protocol/objectui/widget-contract)** | `widget.zod.ts` | FieldWidgetProps | Props a custom field widget receives |
5656

5757
## Kernel Protocol (17 schemas)
5858

content/docs/protocol/objectui/concept.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ function renderField(field: FieldDefinition) {
703703
### For Architects
704704

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

709709
### For Developers

content/docs/protocol/objectui/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ mobile_renderer:
546546
Building a renderer for ObjectUI?
547547
548548
- [Component Reference](/docs/references/ui/component) - Widget contract and standard props
549-
- [Widget Reference](/docs/references/ui/widget) - Dashboard widget schema
549+
- [Dashboard Reference](/docs/references/ui/dashboard) - Dashboard widget schema
550550
- [Widget Contract](/docs/protocol/objectui/widget-contract) - Standard props and events
551551
552552
### For Users

0 commit comments

Comments
 (0)