Skip to content

feat(spec)!: element:text variant refuses heading / subheading by name, and os migrate meta rewrites them to h2 / h3 (#21015) - #21614

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21015-element-text-variant-retire
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21015-element-text-variant-retire

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 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.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

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:ui tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 13 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/vitest.repo-tests.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object elementTextVariantHeadingLevels), retiredFromLoadPath (symbol, a field of const object elementTextVariantHeadingLevels))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/vitest.repo-tests.json) — pages documenting those are invisible to this run
  • 12 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 54521f08ca456c4144de43cb3d25ed3a8b177a7c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0e3c777d2a4dbe30e7fd78cdaef9de200de457f9 — the merge of head 442d5a86253dc80faf84e5ca959a27380be8d631 into base 54521f08ca456c4144de43cb3d25ed3a8b177a7c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 54521f08ca456c4144de43cb3d25ed3a8b177a7c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 18:04
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 18:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 36ad321 Oct 3, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21015-element-text-variant-retire branch October 3, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

2 participants