From 17a151f5112b0301b0c763e8734e8d12191579e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 10:28:03 +0000 Subject: [PATCH 1/2] feat(spec): widen `element:text.variant` to the published nine, additive only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `element:text.variant` accepted `heading` / `subheading` / `body` / `caption` and refused `h1`-`h6` and `overline` with `invalid_value`. objectui's text node publishes nine values, so the seven it publishes and the spec refuses could not be authored at all — the authoring gate was the refusing party. The enum now declares the published nine plus the two spellings it has always accepted: eleven members, seven newly accepted, nothing refused that was accepted before. `.optional().default('body')` is kept deliberately, so an absent `variant` still materialises `'body'` — absence is the one thing a widening must not move. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude --- packages/spec/src/ui/component.test.ts | 52 +++++++++++++++++++++++--- packages/spec/src/ui/component.zod.ts | 47 ++++++++++++++++++++++- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index 09cb9352bde..20d4d8195f5 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -1369,11 +1369,53 @@ describe('ElementTextPropsSchema', () => { expect(props.align).toBe('center'); }); - it('should accept all variants', () => { - const variants = ['heading', 'subheading', 'body', 'caption'] as const; - variants.forEach(variant => { - expect(() => ElementTextPropsSchema.parse({ content: 'Test', variant })).not.toThrow(); - }); + /** + * The accept set, measured rather than described. Release 1 of the + * objectui#7450 convergence (maintainer 2026-09-09, option B) is additive + * only, so the assertion has two halves and BOTH are load-bearing: the nine + * published values are accepted, and the two legacy spellings are STILL + * accepted. A pin that only checked the nine would stay green through the + * release-2 retirement this card explicitly does not carry. + */ + const PUBLISHED_NINE = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'caption', 'overline'] as const; + const STILL_ACCEPTED = ['heading', 'subheading'] as const; + + it.each(PUBLISHED_NINE)('accepts the published variant %s', variant => { + const parsed = ElementTextPropsSchema.safeParse({ content: 'Test', variant }); + expect(parsed.success).toBe(true); + expect(parsed.success && parsed.data.variant).toBe(variant); + }); + + it.each(STILL_ACCEPTED)('release 1 refuses nothing — %s is still accepted', variant => { + const parsed = ElementTextPropsSchema.safeParse({ content: 'Test', variant }); + expect(parsed.success).toBe(true); + expect(parsed.success && parsed.data.variant).toBe(variant); + }); + + /** + * The lit control for the two tests above: the enum is still a CLOSED set, + * so a zero-refusal reading on the eleven is a reading and not a schema that + * stopped judging `variant` at all. + */ + it('still refuses a value outside the eleven, with invalid_value', () => { + const parsed = ElementTextPropsSchema.safeParse({ content: 'Test', variant: 'small' }); + expect(parsed.success).toBe(false); + expect(parsed.success ? [] : parsed.error.issues.map(issue => issue.code)).toContain('invalid_value'); + expect(parsed.success ? [] : parsed.error.issues.map(issue => issue.path.join('.'))).toContain('variant'); + }); + + /** + * Absence is the one thing this widening must not move (objectui#6942 keeps + * the `ui:text` side from synthesising `body`; the spec side always has). + * `.optional().default('body')` is kept deliberately, so an absent `variant` + * still materialises `'body'` — pinned here as well as in the minimal-props + * test above, because that test would keep passing if the default moved to + * some other member of the widened enum. + */ + it('leaves absence exactly where it was — no variant materialises body', () => { + const parsed = ElementTextPropsSchema.safeParse({ content: 'Test' }); + expect(parsed.success).toBe(true); + expect(parsed.success && parsed.data.variant).toBe('body'); }); it('should reject without content', () => { diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index dd960bdc46d..a657d7ec7f6 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -1817,7 +1817,52 @@ export const ElementTextPropsSchema = lazySchema(() => strictObject({ * pages. */ content: I18nLabelSchema.describe('Text or Markdown content — a plain string, or an inline locale map'), - variant: z.enum(['heading', 'subheading', 'body', 'caption']) + /** + * Text style variant, declared as the PUBLISHED NINE plus the two spellings + * this declaration has always accepted. + * + * objectui#7450's ruling (director batch #71, 2026-09-07, maintainer + * verbatim 「其他同意」) converges `element:text` on the nine values + * `@object-ui/types` publishes for its text node — `h1`-`h6`, `body`, + * `caption`, `overline` — with `heading` / `subheading` becoming named + * refusals carrying migration hints. The maintainer then split the landing + * (2026-09-09, option B): release 1 widens and refuses NOTHING, so + * out-of-repo authors converge on a released pin before any spelling stops + * working; release 2 carries the refusals and waits on a value-level + * retirement mechanism that does not exist yet (`retiredKey()` / ADR-0087 D2 + * retire a KEY, not a VALUE). This entry is release 1. So the accepted set + * GROWS by seven and loses nothing: `h1`-`h6` and `overline` were refused + * here with `invalid_value` on the 17.3.0 pin, measured, and `heading` / + * `subheading` stay accepted. + * + * Why the widening is authored HERE rather than in objectui: this + * declaration is the authoring gate, and it already refused the seven. The + * accurate statement of the defect the ruling names is 「the renderer + * swallows what the authoring gate already refuses」 — objectui declaring + * the nine against a spec that refuses them is the consumer-side widening + * AGENTS.md #0.1 bans, and objectui's own per-PR registry↔spec parity gate + * catches it. + * + * ⚠️ `.optional().default('body')` is KEPT, deliberately, not inherited. + * Absence is the one thing a widening must not move: a parsed + * `element:text` node with no `variant` materialises `variant: 'body'` + * today, and it still does — identical bytes in, identical bytes out. The + * `ui:text` side of the platform deliberately does NOT synthesise `body` + * for an absent `variant` (objectui#6942, protecting unannotated corpus + * nodes); that asymmetry is pre-existing, is not this card's to resolve, + * and is left exactly where it was. Removing the default here would refuse + * nothing and break nothing at the door, but it WOULD change what every + * downstream reader sees for an absent key — a silent behaviour change + * wearing an additive changeset, which is what the ruling's split exists to + * prevent. + */ + variant: z.enum([ + // The published nine (`@object-ui/types` `TextProps['variant']`). + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'caption', 'overline', + // Accepted since this shape was declared; release 2 turns these two into + // named refusals with migration hints, ⛔ not release 1. + 'heading', 'subheading', + ]) .optional().default('body').describe('Text style variant'), align: z.enum(['left', 'center', 'right']) .optional().default('left').describe('Text alignment'), From 2cb64508569d7efb095cd8e9c3714f62dc46740d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 11:00:36 +0000 Subject: [PATCH 2/2] chore(spec): regenerate the projections the widened `element:text.variant` moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `api-surface-declarations/ui.txt` gains the seven newly accepted members on `ElementTextPropsSchema` and on `ComponentPropsMap['element:text']`; `content/docs/references/ui/component.mdx`'s property table widens with them. Both produced by `check:generated --fix`, which named exactly these two stale — `check:api-surface`, `check:authorable-surface` and the other 14 were already current, so nothing was regenerated on principle. Plus the changeset: `@objectstack/spec` minor, `Clause-②: yes (widening)`. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude --- ...17108-element-text-variant-published-nine.md | 17 +++++++++++++++++ content/docs/references/ui/component.mdx | 2 +- packages/spec/api-surface-declarations/ui.txt | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 .changeset/17108-element-text-variant-published-nine.md diff --git a/.changeset/17108-element-text-variant-published-nine.md b/.changeset/17108-element-text-variant-published-nine.md new file mode 100644 index 00000000000..e9d090c5b97 --- /dev/null +++ b/.changeset/17108-element-text-variant-published-nine.md @@ -0,0 +1,17 @@ +--- +'@objectstack/spec': minor +--- + +`element:text.variant` accepts the nine values objectui's text node publishes — `h1`–`h6`, `body`, `caption`, `overline` — and still accepts `heading` and `subheading` (#17108). + +Clause-②: yes (widening) + +Release 1 of 2 for the objectui#7450 convergence (director batch #71, 2026-09-07, maintainer verbatim 「其他同意」), split across two releases by the maintainer's decision of 2026-09-09, option B. This release is **additive only**: the accepted set grows by seven and nothing is refused that was accepted before, so an out-of-repo author can converge on a released pin before any spelling stops working. + +Measured on the 17.3.0 declaration, per value, through `ElementTextPropsSchema.safeParse`: `h1`–`h6` and `overline` were refused with `invalid_value`; they are accepted now. `heading`, `subheading`, `body` and `caption` were accepted and are accepted now. A value outside the eleven — `small` — is still refused with `invalid_value` at path `variant`, so the enum remains a closed set rather than having stopped judging `variant` at all. + +- **`.optional().default('body')` is kept, deliberately.** An `element:text` node parsed without a `variant` still materialises `variant: 'body'`, exactly as before. Absence is the one thing a widening must not move, and the `ui:text` side of the platform deliberately does *not* synthesise `body` for an absent `variant` (objectui#6942) — that asymmetry is pre-existing and is left where it was. +- **⛔ Nothing is retired.** `heading` and `subheading` become named refusals carrying migration hints in **release 2**, which is a separate card and is blocked on a value-level retirement mechanism that does not exist yet: `retiredKey()` and ADR-0087 D2 retire a *key*, not a *value*. Authors who want to move early can write `h2` for `heading` and `h3` for `subheading`; neither spelling stops working in this release. +- **No renderer changes here.** `element:text`'s renderer, its designer inspector options and its i18n rows are objectui's, on the released pin, and land on objectui's side of the sequence. + +Generated projections follow the declaration: `api-surface-declarations/ui.txt` gains the seven members on `ElementTextPropsSchema` and on `ComponentPropsMap['element:text']`, and the `content/docs/references/ui/component.mdx` property table widens. `check:api-surface` reports nothing removed or narrowed. diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index f199a3f3276..d46e124c272 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -287,7 +287,7 @@ Sort field and direction pair | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **content** | `string \| Record` | ✅ | Text or Markdown content — a plain string, or an inline locale map | -| **variant** | `Enum<'heading' \| 'subheading' \| 'body' \| 'caption'>` | optional (default: `"body"`) | Text style variant | +| **variant** | `Enum<'h1' \| 'h2' \| 'h3' \| 'h4' \| 'h5' \| 'h6' \| 'body' \| 'caption' \| 'overline' \| 'heading' \| 'subheading'>` | optional (default: `"body"`) | Text style variant | | **align** | `Enum<'left' \| 'center' \| 'right'>` | optional (default: `"left"`) | Text alignment | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | diff --git a/packages/spec/api-surface-declarations/ui.txt b/packages/spec/api-surface-declarations/ui.txt index 6046c1c8b74..fb1bc263216 100644 --- a/packages/spec/api-surface-declarations/ui.txt +++ b/packages/spec/api-surface-declarations/ui.txt @@ -3666,6 +3666,13 @@ declare const ComponentPropsMap: { heading: "heading"; caption: "caption"; subheading: "subheading"; + h1: "h1"; + h2: "h2"; + h3: "h3"; + h4: "h4"; + h5: "h5"; + h6: "h6"; + overline: "overline"; }>>>; align: z.ZodDefault>>; align: z.ZodDefault