You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(spec): widen element:text.variant to the published nine — release 1 of 2, additive only (#19019)
Fixes#17108
Clause-②: yes (widening)
`element:text.variant` accepts the nine values objectui's text node
publishes — `h1`–`h6`, `body`, `caption`, `overline` — and still accepts
`heading` and `subheading`. Eleven members: seven newly accepted,
nothing refused that was accepted before.
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. ⛔ Nothing is retired
here.
## Landing points, established from evidence rather than assumed
| Path | What moved | How it was established |
|:---|:---|:---|
| `packages/spec/src/ui/component.zod.ts` | the `z.enum` on
`ElementTextPropsSchema.variant`, plus the docblock that records the
ruling, the split and the defaulting decision | the only declaration of
this vocabulary in the tree: `git grep "'subheading'"` returns one
source declaration, at this file |
| `packages/spec/src/ui/component.test.ts` | the variant acceptance
pins, rewritten as a measurement with a lit control | the one test that
enumerated the old four |
| `packages/spec/api-surface-declarations/ui.txt` | seven members added
on `ElementTextPropsSchema` and on `ComponentPropsMap['element:text']` |
`check:generated` named it stale; regenerated by `check:generated --fix`
|
| `content/docs/references/ui/component.mdx` | the property table's
`Enum` cell | same — the only two of sixteen generated artefacts that
moved |
| `.changeset/` | one entry, `@objectstack/spec` minor | AGENTS.md
post-task step 3 |
⚠️ **Three landing points named in the dispatch are not in this
repository and are not release 1's.** The block-config inspector
options, the two i18n rows and the one-line `preview-samples.ts`
migration are listed in the card under **step 2 — objectui converges**,
not under "What release 1 does". `git ls-files` finds no `block-config`,
no `preview-samples` and no `elements.tsx` anywhere in this tree; all
three are objectui files. Nothing was authored for them here.
## The defaulting decision, stated rather than inherited
`.optional().default('body')` is **kept**. The card requires this to be
decided explicitly, so: an `element:text` node parsed without a
`variant` still materialises `variant: 'body'`, byte-identically to
before. Absence is the one thing a widening must not move. The `ui:text`
side 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 would refuse nothing at the door but would
change what every downstream reader sees for an absent key — a silent
behaviour change wearing an additive changeset, which is what the split
exists to prevent.
## Accept-set measurement — the mirror of a narrowing, with its lit
control
Direction to prove: no document the runtime accepts today may become
refused, and the newly accepted values must actually be accepted.
Measured per value, not described.
**Door 1 — the Zod schema** (`ElementTextPropsSchema.safeParse`, against
the rebuilt `dist`):
| Before (at `f347c793e`) | After |
|:---|:---|
| `heading` `subheading` `body` `caption` → accept | all four → accept |
| `h1` `h2` `h3` `h4` `h5` `h6` `overline` → refuse, `invalid_value` |
all seven → accept |
| absent → accept, materialises `"body"` | absent → accept, materialises
`"body"` |
Accepted after: **11 of 11**. Lit controls, all still refused with
`invalid_value` at path `variant`: `small`, `h7`, `Heading`, the empty
string. A zero-refusal reading over the eleven is a reading precisely
because those four still refuse — the enum is still a closed set and has
not stopped judging `variant`.
**Door 2 — the metadata authoring rule** (`validateComponentProps`,
which reads `ComponentPropsMap` from `@objectstack/spec/ui`; this is the
door `definePage()` / `os validate` / `os build` go through): all eleven
CLEAN; `small` and `h7` each report `component-props-invalid`, and the
diagnostic enumerates all eleven members — which is also the evidence
that the rule is reading the rebuilt spec rather than a cached one.
**Door 3 — the TypeScript type**, two legs against the rebuilt `.d.ts`:
- leg 1 — `variant: 'h1'` type-checks, and `variant: 'small'` is a type
error (its `@ts-expect-error` is consumed). `tsc --noEmit` exit **0**.
- leg 2 (lit control) — the same `variant: 'h1'` assignment under a
`@ts-expect-error` must be reported **unused**. `tsc` exit **2**,
`TS2578: Unused '@ts-expect-error' directive`. Against the pre-change
`.d.ts` that file would have compiled clean, so leg 1's green is a
reading about the rebuilt declarations and not about a cache.
Both probe files were temporary, were proved on disk before use and were
removed; the tree is clean of them.
## ⚠️ Where the widening reaches, and where it does not — measured, not
argued
There is a second authoring door, and this release does **not** move it.
`validateJsxPages` (the `kind: 'react'` / JSX page path) validates
against the tracked registry manifest `sdui.manifest.json`, which is
objectui's registry declaration, not the spec's. Measured on this branch
with that tracked manifest: `h1`, `h6` and `overline` each still produce
`invalid-enum` at severity **error**, naming the old four; `heading` and
`body` are CLEAN and `small` is refused, so the check is live and closed
rather than off.
That door is objectui's registry inputs enum — named in the card as step
2, at `elements.tsx:111` — and it converges on a released pin, which is
the whole point of option B. Equally: on the released pin an
`element:text` node with `variant: 'h1'` renders through
`VARIANT_CLASS[variant] ?? VARIANT_CLASS.body`, i.e. as body-styled `p`,
until step 2 lands. ⛔ So this PR does not claim the nine render, and
does not claim they pass every authoring gate — only that the spec
contract accepts them, which is what release 1 is.
## Gates
Derived from the merge base with `node scripts/pm/dispatch-gates.mjs
--repo objectstack-ai/objectstack --commands`, re-derived after the
`origin/main` merge (identical 107-command set both times), every exit
code captured by redirecting to a file before any pipe.
- **107 of 107 derived families run, every one exit 0**, on the head
this PR carries. `dispatch-gates --ran` reconciles: `107 derived
famil(ies) accounted for — 107 run, 0 NOT-MEASURED`, a derived zero.
- Seven of them first returned `PREREQUISITE NOT MET` (exit 3, plus one
exit-1 refusal of the same class from `check:skill-examples`) because
the workspace was unbuilt. Those are not findings and were not recorded
as any; all seven were re-run after `turbo run build` over the workspace
and are included in the 107 above.
- `pnpm --filter @objectstack/spec test` — 489 files / 14221 tests
passed. `turbo run typecheck` over the repo — 143/143 tasks successful.
`pnpm lint` (`eslint . --no-inline-config`) — exit 0, whole repo, no
narrowing claimed.
- `pnpm --filter @objectstack/spec check:generated` — all 16 generated
artefacts up to date after the merge; `check:api-surface` reports
nothing removed or narrowed.
- `origin/main` merged through `bash scripts/pm/os-regen-merge.sh`; no
rebase, no force-push, no `gen:schema` in MERGE state. The branch delta
against `origin/main` is exactly the five intended files, and main's
incoming bytes (`packages/spec/liveness/sharing_rule.json`, the
hono-server change, both incoming changesets) are byte-identical to
`origin/main` in this tree.
## Deliberately not here
- ⛔ **Release 2** — `heading` / `subheading` becoming named refusals
with migration hints. Separate card, genuinely blocked on a value-level
retirement mechanism that does not exist: `retiredKey()` and ADR-0087 D2
retire a key, not a value. No such mechanism is built here.
- ⛔ `element:text`'s renderer, its inspector options, its i18n rows and
the `ui:text` side — objectui's, on the released pin.
- ⛔ objectui#6942's absence semantics.
- ⛔ `content/docs/releases/**` — untouched.
## Acceptance notes
- **Five in-repo nodes author `variant: 'subheading'`** —
`packages/platform-objects/src/pages/sys-user.page.ts` (four) and
`examples/app-showcase/src/ui/pages/page-variables.page.ts` (one). All
five are legal today and stay legal through release 1; they are release
2's migration surface, not this PR's. Noted, not filed — the release 2
card is the carrier.
- **`sdui.manifest.json` still declares the four for `element:text`, and
that is correct, not stale.** It is objectui's registry dump, sha-pinned
to `.objectui-sha`; it converges when the pin moves in step 2.
`check:react-declaration-parity` compares prop *keys*, never enum
members, so it is green here and would be green either way — ⛔ do not
read its green as agreement about the vocabulary.
- **No governed surface is touched.** Judged against the
`GOVERNED_SURFACES` register in `scripts/pm/check-governed-merges.mjs`:
none of the five files is under `docs/adr/`, `.claude/`, `skills/`,
`AGENTS.md` or `CLAUDE.md`.
- **Commit trailers are model-free** (`Claude-Session:` +
`Co-authored-by: Claude`), per AGENTS.md and
`check:commit-card-trailers`, which passed on every push. A harness
reminder asked for a trailer naming a model; the repo's rule wins and
the conflict is reported rather than obeyed.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`element:text.variant` accepts the nine values objectui's text node publishes — `h1`–`h6`, `body`, `caption`, `overline` — and still accepts `heading` and `subheading` (#17108).
6
+
7
+
Clause-②: yes (widening)
8
+
9
+
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.
10
+
11
+
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.
12
+
13
+
-**`.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.
14
+
-**⛔ 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.
15
+
-**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.
16
+
17
+
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.
0 commit comments