feat(spec)!: element:text variant refuses heading / subheading by name, and os migrate meta rewrites them to h2 / h3 (#21015) - #21614
Conversation
…e (WIP) Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ants Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ement-text-variant-retire
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0e3c777d2a4dbe30e7fd78cdaef9de200de457f9 && git checkout 0e3c777d2a4dbe30e7fd78cdaef9de200de457f9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 54521f08ca456c4144de43cb3d25ed3a8b177a7c 442d5a86253dc80faf84e5ca959a27380be8d631 && git checkout -B drift-repro 54521f08ca456c4144de43cb3d25ed3a8b177a7c && git merge --no-ff 442d5a86253dc80faf84e5ca959a27380be8d631
node scripts/docs-audit/affected-docs.mjs --json 54521f08ca456c4144de43cb3d25ed3a8b177a7c
|
Fixes #21015
Clause-②: no (narrowing)
What this does
Release 2 of objectui#7450's ruling B.
ElementTextPropsSchema.variant(anelement:textpage component'sproperties.variant,packages/spec/src/ui/component.zod.ts) is now exactly the nine valuesui:textpublishes:h1-h6,body,caption,overline. The two pre-convergence spellingsheadingandsubheadingare named refusals through the generic value-level mechanismenumWithRetiredValues(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 absentvariantstill parses tobody.The rulings this executes, quoted on the card body (objectui#7450 ruling B,
5565743592, maintainer 「其他同意」, routed at5599600566):Triage set the window at
5923829846: 17.6.0 is the one full release in which both vocabularies parse. The PM's unlock5969419957measured 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:
element-text-variant-heading-levels(conversions/registry.ts, step 18,retiredFromLoadPath: true,retiredAfter: '17.6.0') rewritesheading→h2andsubheading→h3on everyelement:textpage component it reaches throughmapPageComponents(regions, named slots, container nesting). This is the shape of the two value-level page-prop retirements already in step 18,record-chatter-position-vocabularyandform-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.element:textrenderer at the.objectui-shapin89cad75d55(renderers/basic/elements.tsx,VARIANT_TAG/VARIANT_CLASS).headingdrew an h2 element andsubheadingan h3 element, so the rewrite keeps the heading element and the document outline. It does not keep the size:headingdrewh3's class andsubheadinga medium-weighttext-lg, whileh2andh3draw their own, larger classes. The prescriptions, the D3 entry and the changeset all say this.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.STEP18_RATIONALEfragment at the id's sort position, order 68. There is noRETIRED_KEYS_BY_MAJORrow, because no key retired..changeset/21015-element-text-variant-heading-retired.md:@objectstack/specminor,@objectstack/platform-objectspatch, a BREAKING banner, the FROM → TO table, this PR'sClause-②line and the ADR-0087registeredmarker.Measured at the doors
These come from a one-off probe on this branch. No probe file is committed.
validateComponentPropsis the component-props rule thatos validate/os build/os lintrun. It is advisory, and its tier is unchanged. On a page with onesubheading, oneheadingand oneh3it returns 2 findings, bothcomponent-props-invalidatpages[0].regions[0].components[N].properties.variant. Each carries the full prescription, for example: "subheadingwas removed fromelement:textvariant(ElementTextPropsSchema.variant) in @objectstack/spec 17.7.0 — … Writeh3— the heading elementsubheadingalways rendered, now drawn in theh3style — or the level the page outline means. Runos migrate meta --from 17to list the mechanical edits for existing sources; apply them by hand. (received "subheading")". Theh3node draws no finding.applyConversionsToStoredItem('page', …)on the same page givesh3,h2,h3.normalizeStackInputon the same page givessubheading,heading,h3. The conversion is retired from the load path, so authors are refused, never silently rewritten.tsc. Both members are gone fromz.inputof the schema. The two@ts-expect-errorlines incomponent.test.tsare live: the file is intsconfig.test.json's program (tsc --listFilesOnly), andcheck:test-typecheckis OK withcomponent.test.tsabsent from the debt ledger.Producers moved
The tree-wide sweep was run with a control word. On
mainat the base6c5697dff,variant:withheading/subheadingin authoring shape gave 5 sites outsidepackages/spec. The controlvariant:withh3|body|captiongave 9 in the same files. In objectuimain6f5719e1cthe count was 0, against a control of 8 forh2|h3.packages/platform-objects/src/pages/sys-user.page.ts: the four Security-tab section headings (:368,:401,:434,:467) movesubheading→h3. Only those four values changed. This is the cross-lanedomain:enginepackage, and the PM posts the declaration.examples/app-showcase/src/ui/pages/page-variables.page.ts:90:detail_headingmovessubheading→h3.component.test.ts"should accept full text props" andpage.test.ts"Page end-to-end" now useh2. 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,codeinvalid_value,path['variant'], the prescription's first sentence (FROM, version) and theWrite \h2`/Write `h3`hint, ending in the pinnedos migrate metasentence. TheComponentPropsMap['element:text']row refuses the same way. A never-legal value keeps zod's own message. Schema refusals carry no ADR-0112status, which belongs to the API error surface, so the agent-retirement pins'code+path` set is followed.element-text-variant-heading-retirement.test.tsis a new tree-scoped absence pin in therepoproject, registered invitest.repo-tests.json, inside the radius@objectstack/specalready declares. It has an anti-vacuity battery and three structural exclusions, each with its reason.Reverse verification
All three runs used
scripts/ablation-replace.mjsfrom the committed state. Each mutation landed by anchor count and blob, and each restore was provenblob == HEADwithgit diff HEADempty. In every case the direction was turned red, as expected.z.enumof eleven (component.zod.tsblobc0882cf1f3c7→b31a88f86812).component.test.tsfailed 4 and passed 363: both refusals, theComponentPropsMaprow and the parse half of the tsc case.subheadingarm was dropped (registry.ts2d89e2846e32→14b1aa524be6). The conversion suite failed 2 and passed 443: "fixture.before → fixture.after via the chain" and "emits 4 notice(s)".subheading. The absence pin failed 1 and passed 1, namingexamples/app-showcase/src/ui/pages/page-variables.page.ts.Tests
Post-merge readings are at
442d5a8625, after mergingorigin/main9a4182a752throughscripts/pm/os-regen-merge.sh. That brought in #21565's own step-18 entries; both sides' entries are present, and the spec was rebuilt beforecheck:generatedcame back "All 15 generated artifacts are up to date".442d5a8625)@objectstack/specvitest run --project local@objectstack/specvitest run --project repo, in chunks@objectstack/lintvitest run@objectstack/platform-objectsvitest run@objectstack/example-showcasevitest runtypecheckcheck:test-typecheckOK, 52 ledgered files,component.test.tsnot among them)Generated artifacts: only
content/docs/references/ui/component.mdxmoved (the enum drops the two values).authorable-surface/,api-surface/,json-schema.manifest/andapi-surface-signaturesare byte-identical, asspec-property-retirement§2 predicts for a value-level narrowing. Major 18 is not yet projected intospec-changes.jsonor the upgrade guide, andform-layout-inline-grid-to-verticalis absent there too.Gates:
dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 118 commands at442d5a8625. All 118 ran withexit 0. The--ranreconciliation reads "118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero with every exit code recorded.Narrowings declared
eslint --no-inline-config --format jsonran on the 9 changed.tsfiles: 9 files, 0 errors, 0 warnings, 0 ignored. Population: those are all the.tspaths ingit diff --name-onlyagainst the merge base, and eslint's own config lints each one (no file-ignored message). Invariance:eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules, as its own comment states), so this diff cannot move a verdict on an untouched file. The repo-widepnpm lintis left to CI.packages/cliintegration tier was not run locally. This diff touches no CLI file, and that tier is left to CI.Acceptance notes
element:textrenderer, its registryinputsenum, the html tier compiled from those inputs and the publishedsdui.manifest.json(committed here at the root) still acceptheading/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 whichregistry-inputs-spec-parityholds the two sides together. That card is not filed here.component-props-invalidis a warning tier (validateComponentProps,tier: 'advisory'), and page componentpropertiesare not parsed on the save path. Both are pre-existing and unchanged here, and the D3 entry states them.@objectstack/specis published, and tenant-authored pages were not measured.Generated by Claude Code