Skip to content

Commit 36ad321

Browse files
feat(spec)!: element:text variant refuses heading / subheading by name, and os migrate meta rewrites them to h2 / h3 (#21015) (#21614)
Fixes #21015 Clause-②: no (narrowing) ## What this does Release 2 of objectui#7450's ruling B. `ElementTextPropsSchema.variant` (an `element:text` page component's `properties.variant`, `packages/spec/src/ui/component.zod.ts`) is now exactly the nine values `ui:text` publishes: `h1`-`h6`, `body`, `caption`, `overline`. The two pre-convergence spellings `heading` and `subheading` are **named refusals** through the generic value-level mechanism `enumWithRetiredValues` (`shared/retired-key.ts`, the helper #17109 added; no one-off refinement on this enum). Each refusal carries the ruled hint: `heading` → `h2`, `subheading` → `h3`, or the level the page means. `.optional().default('body')` is kept byte for byte, so an absent `variant` still parses to `body`. The rulings this executes, quoted on the card body (objectui#7450 ruling B, `5565743592`, maintainer 「其他同意」, routed at `5599600566`): > "**B — split across two releases.** spec widens to the nine **first** (additive, nothing refused), objectui converges on that released pin, and `heading`/`subheading` are retired in a **later** spec release once out-of-repo authors have had a window." > "`heading` and `subheading` become **named refusals** carrying the migration hint (`heading` → `h2`, `subheading` → `h3`, or pick the level you mean)." Triage set the window at `5923829846`: 17.6.0 is the one full release in which both vocabularies parse. The PM's unlock `5969419957` measured 17.6.0 published at 2026-10-02T03:03Z. ### The ADR-0087 disposition: stored rows convert, authors are refused This came out of the playbook and its precedents. It was not a guess, and they left no real fork: - **D2 conversion `element-text-variant-heading-levels`** (`conversions/registry.ts`, step 18, `retiredFromLoadPath: true`, `retiredAfter: '17.6.0'`) rewrites `heading` → `h2` and `subheading` → `h3` on every `element:text` page component it reaches through `mapPageComponents` (regions, named slots, container nesting). This is the shape of the two value-level page-prop retirements already in step 18, `record-chatter-position-vocabulary` and `form-layout-inline-grid-to-vertical`: an enum refuses the old value at parse, and a load-path-retired rewrite replays it over stored rows. The analytics precedent (`cube-metric-expression-types-retired`) has no D2 only because "no rewrite can say which aggregate the author meant". Here the ruling names the rewrite. - **What the rewrite keeps.** I measured the `element:text` renderer at the `.objectui-sha` pin `89cad75d55` (`renderers/basic/elements.tsx`, `VARIANT_TAG` / `VARIANT_CLASS`). `heading` drew an h2 element and `subheading` an h3 element, so the rewrite keeps the heading element and the document outline. It does not keep the size: `heading` drew `h3`'s class and `subheading` a medium-weight `text-lg`, while `h2` and `h3` draw their own, larger classes. The prescriptions, the D3 entry and the changeset all say this. - **D3 semantic entry `element-text-variant-heading-subheading-retired`** (`conversionIds: ['element-text-variant-heading-levels']`) holds the judgement the chain cannot make: whether the rewritten level is the one the page means. - **A `STEP18_RATIONALE` fragment** at the id's sort position, order 68. There is no `RETIRED_KEYS_BY_MAJOR` row, because no key retired. - **Changeset** `.changeset/21015-element-text-variant-heading-retired.md`: `@objectstack/spec` minor, `@objectstack/platform-objects` patch, a BREAKING banner, the FROM → TO table, this PR's `Clause-②` line and the ADR-0087 `registered` marker. ### Measured at the doors These come from a one-off probe on this branch. No probe file is committed. - **Authoring door.** `validateComponentProps` is the component-props rule that `os validate` / `os build` / `os lint` run. It is advisory, and its tier is unchanged. On a page with one `subheading`, one `heading` and one `h3` it returns 2 findings, both `component-props-invalid` at `pages[0].regions[0].components[N].properties.variant`. Each carries the full prescription, for example: "`subheading` was removed from `element:text` `variant` (`ElementTextPropsSchema.variant`) in @objectstack/spec 17.7.0 — … Write `h3` — the heading element `subheading` always rendered, now drawn in the `h3` style — or the level the page outline means. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. (received "subheading")". The `h3` node draws no finding. - **Stored-row seam.** `applyConversionsToStoredItem('page', …)` on the same page gives `h3,h2,h3`. - **Authoring funnel.** `normalizeStackInput` on the same page gives `subheading,heading,h3`. The conversion is retired from the load path, so authors are refused, never silently rewritten. - **`tsc`.** Both members are gone from `z.input` of the schema. The two `@ts-expect-error` lines in `component.test.ts` are live: the file is in `tsconfig.test.json`'s program (`tsc --listFilesOnly`), and `check:test-typecheck` is OK with `component.test.ts` absent from the debt ledger. ### Producers moved The tree-wide sweep was run with a control word. On `main` at the base `6c5697dff`, `variant:` with `heading`/`subheading` in authoring shape gave 5 sites outside `packages/spec`. The control `variant:` with `h3|body|caption` gave 9 in the same files. In objectui `main` `6f5719e1c` the count was 0, against a control of 8 for `h2|h3`. - `packages/platform-objects/src/pages/sys-user.page.ts`: the four Security-tab section headings (`:368`, `:401`, `:434`, `:467`) move `subheading` → `h3`. Only those four values changed. This is the cross-lane `domain:engine` package, and the PM posts the declaration. - `examples/app-showcase/src/ui/pages/page-variables.page.ts:90`: `detail_heading` moves `subheading` → `h3`. - Fixtures were triaged as the playbook says. Two were respelled: `component.test.ts` "should accept full text props" and `page.test.ts` "Page end-to-end" now use `h2`. One was replaced whole: the release-1 pin "release 1 refuses nothing — %s is still accepted" pinned exactly the branch this PR removes, and is now the refusal pins below. ### Pins - `component.test.ts`: the nine are accepted. Each retired spelling is refused with exactly one issue, `code` `invalid_value`, `path` `['variant']`, the prescription's first sentence (FROM, version) and the `Write \`h2\`` / `Write \`h3\`` hint, ending in the pinned `os migrate meta` sentence. The `ComponentPropsMap['element:text']` row refuses the same way. A never-legal value keeps zod's own message. Schema refusals carry no ADR-0112 `status`, which belongs to the API error surface, so the agent-retirement pins' `code` + `path` set is followed. - `element-text-variant-heading-retirement.test.ts` is a new **tree-scoped absence pin** in the `repo` project, registered in `vitest.repo-tests.json`, inside the radius `@objectstack/spec` already declares. It has an anti-vacuity battery and three structural exclusions, each with its reason. ### Reverse verification All three runs used `scripts/ablation-replace.mjs` from the committed state. Each mutation landed by anchor count and blob, and each restore was proven `blob == HEAD` with `git diff HEAD` empty. In every case the direction was **turned red**, as expected. 1. The enum was reverted to a plain `z.enum` of eleven (`component.zod.ts` blob `c0882cf1f3c7` → `b31a88f86812`). `component.test.ts` failed 4 and passed 363: both refusals, the `ComponentPropsMap` row and the parse half of the tsc case. 2. The conversion's `subheading` arm was dropped (`registry.ts` `2d89e2846e32` → `14b1aa524be6`). The conversion suite failed 2 and passed 443: "fixture.before → fixture.after via the chain" and "emits 4 notice(s)". 3. The showcase producer was put back to `subheading`. The absence pin failed 1 and passed 1, naming `examples/app-showcase/src/ui/pages/page-variables.page.ts`. ### Tests Post-merge readings are at `442d5a8625`, after merging `origin/main` `9a4182a752` through `scripts/pm/os-regen-merge.sh`. That brought in #21565's own step-18 entries; both sides' entries are present, and the spec was rebuilt before `check:generated` came back "All 15 generated artifacts are up to date". | package | command | reading (all at `442d5a8625`) | | --- | --- | --- | | `@objectstack/spec` | `vitest run --project local` | 608 files, 18017 passed, 1 todo | | `@objectstack/spec` | `vitest run --project repo`, in chunks | 52 files, 881 passed | | `@objectstack/lint` | `vitest run` | 119 files, 5620 passed | | `@objectstack/platform-objects` | `vitest run` | 59 files, 949 passed | | `@objectstack/example-showcase` | `vitest run` | 31 files, 394 passed | | all four | `typecheck` | exit 0 (spec: `check:test-typecheck` OK, 52 ledgered files, `component.test.ts` not among them) | Generated artifacts: only `content/docs/references/ui/component.mdx` moved (the enum drops the two values). `authorable-surface/`, `api-surface/`, `json-schema.manifest/` and `api-surface-signatures` are byte-identical, as `spec-property-retirement` §2 predicts for a value-level narrowing. Major 18 is not yet projected into `spec-changes.json` or the upgrade guide, and `form-layout-inline-grid-to-vertical` is absent there too. Gates: `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 118 commands at `442d5a8625`. All 118 ran with `exit 0`. The `--ran` reconciliation reads "118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero with every exit code recorded. ### Narrowings declared - **Lint.** `eslint --no-inline-config --format json` ran on the 9 changed `.ts` files: 9 files, 0 errors, 0 warnings, 0 ignored. Population: those are all the `.ts` paths in `git diff --name-only` against the merge base, and eslint's own config lints each one (no file-ignored message). Invariance: `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, no typed rules, as its own comment states), so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is left to CI. - **`packages/cli` integration tier** was not run locally. This diff touches no CLI file, and that tier is left to CI. ## Acceptance notes - **The interim seam the card already carries.** objectui's `element:text` renderer, its registry `inputs` enum, the html tier compiled from those inputs and the published `sdui.manifest.json` (committed here at the root) still accept `heading` / `subheading`. Until objectui installs this release, a source-authored html/jsx page can write them past the manifest-driven JSX gate, and the component-props rule does not walk source-authored pages. The stored region cache is still rewritten by the conversion at rehydration. The card body names the carrier: the one-line objectui card, filed when this release is installable, after which `registry-inputs-spec-parity` holds the two sides together. That card is not filed here. - **The authoring refusal is advisory at the CLI door.** `component-props-invalid` is a warning tier (`validateComponentProps`, `tier: 'advisory'`), and page component `properties` are not parsed on the save path. Both are pre-existing and unchanged here, and the D3 entry states them. - **Out-of-repo author population is NOT MEASURED.** `@objectstack/spec` is published, and tenant-authored pages were not measured. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5c9138b commit 36ad321

12 files changed

Lines changed: 597 additions & 53 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
feat(spec)!: `element:text` `variant` refuses `heading` / `subheading` by name — the vocabulary is the nine `ui:text` publishes, and `os migrate meta` rewrites them to `h2` / `h3` (#21015)
7+
8+
**BREAKING** — `heading` and `subheading` leave `ElementTextPropsSchema.variant` (an
9+
`element:text` page component's `properties.variant`). This is the second release of
10+
the ruled two-release convergence on the nine values `ui:text` publishes — `h1`-`h6`,
11+
`body`, `caption`, `overline`. 17.5.0 added the nine and refused nothing; 17.6.0 was
12+
the full release in which both vocabularies parsed; this release refuses the two old
13+
spellings. A heading is a document level, not a text style: `heading` and
14+
`subheading` named a style and left the renderer to pick the level.
15+
16+
### FROM → TO
17+
18+
| removed | what to write instead |
19+
| --- | --- |
20+
| `variant: 'heading'` | `variant: 'h2'` — the heading element `heading` always rendered — or the level the page outline means. |
21+
| `variant: 'subheading'` | `variant: 'h3'` — the heading element `subheading` always rendered — or the level the page outline means. |
22+
23+
**The one-line fix: `heading` → `h2`, `subheading` → `h3`.**
24+
`os migrate meta --from 17` lists the mechanical edits for existing sources.
25+
26+
The rewrite keeps the heading ELEMENT (so the document outline is unchanged) but not
27+
the size: `heading` drew in the `h3` style and `subheading` in a medium-weight small
28+
heading style, and `h2` / `h3` draw their own, larger styles. Where the old look
29+
mattered more than the level, pick the level whose style you want.
30+
31+
Each retired spelling is refused at parse with a prescription naming the level to
32+
write, and in `tsc` (the two members are gone from the input type). Any other unknown
33+
value keeps zod's own message. An `element:text` with no `variant` still parses to
34+
`body`.
35+
36+
### The retirement kit
37+
38+
- **Value-level retirement.** The enum is declared through `enumWithRetiredValues`
39+
(`shared/retired-key.ts`), with the two prescriptions module-private. No authorable
40+
KEY and no def changed, so nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four
41+
surface ratchets (`api-surface`, `authorable-surface`, `json-schema.manifest`,
42+
`api-surface-signatures`) are byte-identical; the generated component reference
43+
page drops the two values.
44+
- **D2 conversion `element-text-variant-heading-levels`** (step 18, retired from the
45+
load path): `heading` → `h2` and `subheading` → `h3` on every `element:text` page
46+
component — regions, named slots and container nesting. Stored `sys_metadata` page
47+
rows replay it at rehydration; one notice per rewritten block.
48+
- **D3 entry `element-text-variant-heading-subheading-retired`** carries the judgement
49+
the conversion cannot make: whether the rewritten level is the one the page means.
50+
- **No further deprecation window**: 17.6.0 was the window the ruling asked for.
51+
52+
### Producers moved in this repository
53+
54+
- `@objectstack/platform-objects`: the four section headings on the `sys_user` record
55+
page's Security tab (`Password & Sign-in`, `Two-Factor Authentication`, `Email
56+
Verification`, `Danger Zone`) move from `subheading` to `h3`. They render the same
57+
h3 element, in the `h3` style.
58+
- `examples/app-showcase`: the `page-variables` detail heading moves to `h3`.
59+
60+
⚠️ **The out-of-repo author population is NOT MEASURED.** `@objectstack/spec` is
61+
published, and tenant-authored pages were not measured. In this repository the five
62+
writers above were the only ones outside `packages/spec`. objectui at `main` authors
63+
neither value; its `element:text` renderer, registry `inputs` enum, html tier and the
64+
published `sdui.manifest.json` still list the two, and drop them once this release is
65+
installable there (the objectui follow-up).
66+
67+
Clause-②: no (narrowing)
68+
69+
<!-- adr-0087: registered element-text-variant-heading-levels, element-text-variant-heading-subheading-retired -->

‎content/docs/references/ui/component.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ Sort field and direction pair
436436
| Property | Type | Required | Description |
437437
| :--- | :--- | :--- | :--- |
438438
| **content** | `string \| Record<string, string>` | ✅ | Text or Markdown content — a plain string, or an inline locale map |
439-
| **variant** | `Enum<'h1' \| 'h2' \| 'h3' \| 'h4' \| 'h5' \| 'h6' \| 'body' \| 'caption' \| 'overline' \| 'heading' \| 'subheading'>` | optional (default: `"body"`) | Text style variant |
439+
| **variant** | `Enum<'h1' \| 'h2' \| 'h3' \| 'h4' \| 'h5' \| 'h6' \| 'body' \| 'caption' \| 'overline'>` | optional (default: `"body"`) | Text style variant |
440440
| **align** | `Enum<'left' \| 'center' \| 'right'>` | optional (default: `"left"`) | Text alignment |
441441
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
442442

‎examples/app-showcase/src/ui/pages/page-variables.page.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const PageVariablesPage = definePage({
8787
visibleWhen: "page.selectedProjectId != ''",
8888
properties: {
8989
content: '✓ Project selected',
90-
variant: 'subheading',
90+
variant: 'h3',
9191
},
9292
},
9393
{

‎packages/platform-objects/src/pages/sys-user.page.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ export const SysUserDetailPage: Page = {
365365
{
366366
type: 'element:text',
367367
properties: {
368-
variant: 'subheading',
368+
variant: 'h3',
369369
content: {
370370
en: 'Password & Sign-in',
371371
'zh-CN': '密码与登录',
@@ -398,7 +398,7 @@ export const SysUserDetailPage: Page = {
398398
{
399399
type: 'element:text',
400400
properties: {
401-
variant: 'subheading',
401+
variant: 'h3',
402402
content: {
403403
en: 'Two-Factor Authentication',
404404
'zh-CN': '两步验证',
@@ -431,7 +431,7 @@ export const SysUserDetailPage: Page = {
431431
{
432432
type: 'element:text',
433433
properties: {
434-
variant: 'subheading',
434+
variant: 'h3',
435435
content: {
436436
en: 'Email Verification',
437437
'zh-CN': '邮箱验证',
@@ -464,7 +464,7 @@ export const SysUserDetailPage: Page = {
464464
{
465465
type: 'element:text',
466466
properties: {
467-
variant: 'subheading',
467+
variant: 'h3',
468468
content: {
469469
en: 'Danger Zone',
470470
'zh-CN': '危险操作',

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7718,6 +7718,147 @@ const translationComponentSubmitLabelRemoved: MetadataConversion = {
77187718
},
77197719
};
77207720

7721+
/**
7722+
* `element:text` `variant`'s two pre-convergence spellings become the heading
7723+
* levels they always rendered — `heading` → `h2`, `subheading` → `h3`
7724+
* (protocol 18, #21015: release 2 of objectui#7450's ruling B, maintainer
7725+
* 「其他同意」 2026-09-07, split across two releases 2026-09-09).
7726+
*
7727+
* Release 1 (#17108, 17.5.0) widened the enum to the nine values `ui:text`
7728+
* publishes — `h1`-`h6`, `body`, `caption`, `overline` — and refused nothing;
7729+
* 17.6.0 was the one full release in which both vocabularies parsed. This
7730+
* release refuses the two old spellings by name (`enumWithRetiredValues`,
7731+
* ui/component.zod.ts), and this entry carries the ruled migration hint as the
7732+
* mechanical edit.
7733+
*
7734+
* **The rewrite keeps the heading element, not the look.** Measured at the
7735+
* `.objectui-sha` pin `89cad75d55` (`renderers/basic/elements.tsx`), the
7736+
* `element:text` renderer drew `heading` as an h2 element and `subheading` as an
7737+
* h3 element, so the document outline a screen reader walks is unchanged. The
7738+
* size is not: `heading` drew `h3`'s style and `subheading` a medium-weight
7739+
* `text-lg`, while `h2` and `h3` draw their own, larger ones. The level IS the
7740+
* ruled meaning ("or pick the level you mean"), so the edit follows the
7741+
* element; the D3 entry `element-text-variant-heading-subheading-retired`
7742+
* carries the judgement the chain cannot make — whether this page wanted that
7743+
* level, or another.
7744+
*
7745+
* **One reach: every page component of type `element:text`**, through
7746+
* {@link mapPageComponents} — regions, named slots and container nesting, the
7747+
* positions the component-props gate judges. `variant` on any other component
7748+
* type is that component's own vocabulary and is never read here.
7749+
*
7750+
* `retiredFromLoadPath`: an author is refused at parse with the prescription
7751+
* rather than silently rewritten; stored rows replay it at rehydration
7752+
* (`applyConversionsToStoredItem`) and `os migrate meta` lists the edit for
7753+
* existing sources. Idempotent by construction: the rewrite's output is
7754+
* outside its own input set.
7755+
*/
7756+
const elementTextVariantHeadingLevels: MetadataConversion = {
7757+
id: 'element-text-variant-heading-levels',
7758+
toMajor: 18,
7759+
retiredFromLoadPath: true,
7760+
retiredAfter: '17.6.0',
7761+
surface: 'page.component.element:text.variant',
7762+
summary:
7763+
"element:text 'variant' spellings 'heading' → 'h2' and 'subheading' → 'h3' (the vocabulary converged "
7764+
+ "on the nine values ui:text publishes; each old spelling already rendered that heading element, "
7765+
+ 'so the outline is unchanged and the heading takes that level\'s style)',
7766+
apply(stack, emit) {
7767+
const VARIANT_REWRITE: Readonly<Record<string, string>> = { heading: 'h2', subheading: 'h3' };
7768+
return mapPageComponents(stack, (component, path) => {
7769+
if (component.type !== 'element:text') return component;
7770+
const properties = component.properties;
7771+
if (!isDict(properties)) return component;
7772+
const variant = properties.variant;
7773+
if (typeof variant !== 'string' || !Object.prototype.hasOwnProperty.call(VARIANT_REWRITE, variant)) {
7774+
return component;
7775+
}
7776+
const to = VARIANT_REWRITE[variant]!;
7777+
emit({ from: variant, to, path: `${path}.properties.variant` });
7778+
return { ...component, properties: { ...properties, variant: to } };
7779+
});
7780+
},
7781+
fixture: {
7782+
before: {
7783+
pages: [
7784+
{
7785+
name: 'text_variant_levels',
7786+
regions: [
7787+
{
7788+
name: 'main',
7789+
components: [
7790+
{ type: 'element:text', properties: { content: 'Overview', variant: 'heading', align: 'center' } },
7791+
{ type: 'element:text', properties: { content: 'Details', variant: 'subheading' } },
7792+
// A published level and an absent `variant` ride through.
7793+
{ type: 'element:text', properties: { content: 'Body copy', variant: 'h3' } },
7794+
{ type: 'element:text', properties: { content: 'Default body' } },
7795+
// `variant` on another component type is that type's own
7796+
// vocabulary, untouched here.
7797+
{ type: 'element:button', properties: { label: 'Go', variant: 'heading' } },
7798+
// Nested one container down.
7799+
{
7800+
type: 'page:card',
7801+
properties: {
7802+
title: 'Card',
7803+
children: [{ type: 'element:text', properties: { content: 'In a card', variant: 'subheading' } }],
7804+
},
7805+
},
7806+
],
7807+
},
7808+
],
7809+
},
7810+
// A slotted page's named slot — the same component, the other authoring shape.
7811+
{
7812+
name: 'text_variant_levels_slotted',
7813+
kind: 'slotted',
7814+
regions: [],
7815+
slots: {
7816+
details: [{ type: 'element:text', properties: { content: 'Title', variant: 'heading' } }],
7817+
},
7818+
},
7819+
],
7820+
},
7821+
after: {
7822+
pages: [
7823+
{
7824+
name: 'text_variant_levels',
7825+
regions: [
7826+
{
7827+
name: 'main',
7828+
components: [
7829+
{ type: 'element:text', properties: { content: 'Overview', variant: 'h2', align: 'center' } },
7830+
{ type: 'element:text', properties: { content: 'Details', variant: 'h3' } },
7831+
{ type: 'element:text', properties: { content: 'Body copy', variant: 'h3' } },
7832+
{ type: 'element:text', properties: { content: 'Default body' } },
7833+
{ type: 'element:button', properties: { label: 'Go', variant: 'heading' } },
7834+
{
7835+
type: 'page:card',
7836+
properties: {
7837+
title: 'Card',
7838+
children: [{ type: 'element:text', properties: { content: 'In a card', variant: 'h3' } }],
7839+
},
7840+
},
7841+
],
7842+
},
7843+
],
7844+
},
7845+
{
7846+
name: 'text_variant_levels_slotted',
7847+
kind: 'slotted',
7848+
regions: [],
7849+
slots: {
7850+
details: [{ type: 'element:text', properties: { content: 'Title', variant: 'h2' } }],
7851+
},
7852+
},
7853+
],
7854+
},
7855+
// One per rewritten `variant`: the region pair, the nested card child and
7856+
// the slotted one — the published level, the absent key and the
7857+
// other component type are untouched.
7858+
expectedNotices: 4,
7859+
},
7860+
};
7861+
77217862
/**
77227863
* The inline grid column's one mechanical respelling, shared by both of its
77237864
* carriers — a relationship field's `inlineColumns`
@@ -14439,6 +14580,7 @@ const MAJOR_18_CONVERSIONS: readonly OrderedConversion[] = [
1443914580
{ conversion: elementFilterRemoved, order: 4 },
1444014581
{ conversion: elementFormRemoved, order: 5 },
1444114582
{ conversion: elementInputTargetVariableRemoved, order: 3 },
14583+
{ conversion: elementTextVariantHeadingLevels, order: 59 },
1444214584
{ conversion: fieldColumnListsCanonicalized, order: 6 },
1444314585
{ conversion: fieldMalformedScalePrecisionRemoved, order: 1 },
1444414586
{ conversion: fieldReferenceToAlias, order: 18 },
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// #21015 — release 2 of objectui#7450's ruling B: `element:text` `variant`
6+
// refuses the pre-convergence spellings `heading` and `subheading` by name
7+
// (`enumWithRetiredValues`). The family's one D3 entry; the D2 half is
8+
// `element-text-variant-heading-levels`, which rewrites each to the heading
9+
// element it always rendered. This entry carries the judgement the chain
10+
// cannot make — whether that level is the one the page means, now that it
11+
// draws in that level's style.
12+
export const entry: SemanticMigration = {
13+
id: 'element-text-variant-heading-subheading-retired',
14+
// No backticks and no pipes in `surface` — build-upgrade-guide.ts renders it
15+
// inside a code span AND a table cell.
16+
surface:
17+
'page components of type element:text — properties.variant authored as heading or subheading '
18+
+ '(ElementTextPropsSchema.variant)',
19+
replacement:
20+
"one of the nine values `ui:text` publishes: `h1`-`h6`, `body`, `caption` or `overline`. 'heading' "
21+
+ "→ 'h2' and 'subheading' → 'h3' (the heading element each one always rendered), or the level the "
22+
+ 'page outline means',
23+
reason:
24+
'The ruling converged `element:text` on the vocabulary `ui:text` already publishes, because a '
25+
+ 'heading is a document level, not a text style: `heading` and `subheading` named a style and '
26+
+ 'left the renderer to pick a level. It landed in two releases so authors outside this repository '
27+
+ 'could move first — 17.5.0 added the nine and refused nothing, and 17.6.0 was a full release in '
28+
+ 'which both vocabularies parsed. The D2 conversion `element-text-variant-heading-levels` makes '
29+
+ 'the ruled edit: `heading` → `h2`, `subheading` → `h3`. That keeps the heading element (the '
30+
+ 'renderer drew `heading` as an h2 element and `subheading` as an h3 element), so the document '
31+
+ 'outline a screen reader walks is unchanged, but not the size: `heading` drew in the `h3` style '
32+
+ 'and `subheading` in a medium-weight small heading style, and `h2` / `h3` draw their own, larger '
33+
+ 'styles. Whether the page wanted that level is the author\'s call — a heading placed for its '
34+
+ 'size rather than its place in the outline may want a deeper level. Nothing is dropped at rest: '
35+
+ 'a stored page replays the rewrite at rehydration; a page component\'s `properties` is not '
36+
+ 'parsed on the save path, and the component-props gate reports an old spelling as an advisory '
37+
+ '`component-props-invalid` finding, carrying the prescription, on `os validate`, `os build` and '
38+
+ '`os lint`. ADR-0087',
39+
acceptanceCriteria:
40+
'No `element:text` page component carries `variant` `heading` or `subheading`; `os validate` '
41+
+ 'reports no `component-props-invalid` finding under `properties.variant` for these blocks. For '
42+
+ 'each rewritten block, open the page and check the heading: it renders the same heading element '
43+
+ 'as before, in its level\'s style. Where the old, smaller look mattered more than the level, '
44+
+ 'pick the level whose style you want and confirm the outline still reads in order. A block that '
45+
+ 'omits `variant` still renders as `body`.',
46+
conversionIds: ['element-text-variant-heading-levels'],
47+
};

0 commit comments

Comments
 (0)