Skip to content

test(components): pin the bare text key to ui:text (objectui#7450) - #8795

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7450-element-text-converge-published-nine
Sep 9, 2026
Merged

test(components): pin the bare text key to ui:text (objectui#7450)#8795
os-warren merged 1 commit into
mainfrom
claude/issue-7450-element-text-converge-published-nine

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #7450. #7450 remains open — the standing ruling is deliberately not implemented here, and the measurement below is why.


⛔ The headline: the convergence cannot be declared in this repository

The ruling (5565743592, director seat, decision batch #71, 2026-09-07, maintainer verbatim 「其他同意」) is sound and is not reopened. But the card, the census (5596105170) and the dispatch (5598675699) all rest on one premise, and that premise is false:

the element:text vocabulary was never declared, so nothing could ever refuse a wrong value

It is declared — not in @object-ui/types, but upstream in @objectstack/spec, which AGENTS.md #0 makes the binding contract for this repo. elements.tsx's own file docblock already says so ("Spec-aligned renderers for the element:* component namespace defined by @objectstack/spec").

Source of the declarationframework/packages/spec/src/ui/component.zod.ts:1819, inside ElementTextPropsSchema:

variant: z.enum(['heading', 'subheading', 'body', 'caption'])
  .optional().default('body').describe('Text style variant'),

Measured against the installed pin (@objectstack/spec@17.3.0), through the schema's own verdicts:

element:text variant ElementTextPropsSchema.safeParse
heading subheading body caption ACCEPT
h1 h2 h3 h4 h5 h6 overline REFUSEinvalid_value at variant
nonsense (control) REFUSE — invalid_value
key absent ACCEPT, materialised as variant: 'body'

The repo's own per-PR gate refuses the convergence, mechanically

apps/console/src/__tests__/registry-inputs-spec-parity.test.ts judges an enum arm exactly, by its own header: "An enum ARM is judged differently and exactly, because it is the one arm whose admitted set is FINITE and written down: armAccepts consults the input's own enum list, so every declared member must be a value the spec accepts."

So I ran the convergence as an ablation — swapped the registry inputs enum in elements.tsx for the published nine and nothing else:

BEFORE: old-anchor=1 new-anchor=0 bytes=19663
AFTER : old-anchor=0 new-anchor=1 bytes=19686 delta=23
MUT_BLOB=3e92b4e5c177131948c71c376130788e922f6770  (HEAD_BLOB=498cbdef4e3ea8e3214ab988e023bfd939caeb78)

× element:text declares no arm the spec refuses outright
AssertionError: element:text.variant:enum — the contract refuses the declared
member(s) ["h1","h2","h3","h4","h5","h6","overline"]
Tests  1 failed | 197 passed (198)

RESTORED_BLOB=498cbdef4e3ea8e3214ab988e023bfd939caeb78 · DIFF_HEAD_EMPTY=yes

Baseline on the same file, unmutated: 198 passed.

⇒ Declaring the nine here is a consumer-side widening past the contract, which is exactly AGENTS.md #0.1's ban, and the gate that exists to catch it does catch it. The convergence has to start in @objectstack/spec; this repo follows on a released pin.

The one-line in-repo migration is blocked by the same wall. apps/console/src/preview-samples.ts carries the only authored element:text variant, subheading. That value is spec-legal today; rewriting it to h3 (the value that preserves today's rendered element) would make it spec-illegal. The migration is not a line I can write yet either.

A second correction: "refused by nothing" is true only at the RENDER layer

Re-derived here, not quoted. Rendering element:text through the real registry:

element:text variant element validateTree diagnostics
heading h2 none
subheading h3 none
body / caption p none
h1 / h4 / overline p, class text-sm text-foreground — byte-identical to body invalid-enum / error
absent p none

The renderer's silent swallow is real and reproduces exactly. But packages/sdui-parser's validateTree — the same validator behind the save gate and the JSX-page prop whitelist — already raises invalid-enum at severity error: element:text prop "variant"="h1" is not one of ["heading","subheading","body","caption"]. So the accurate statement is "the renderer swallows what the authoring gate already refuses", not "nothing refuses it". Two authorities, and today they agree with each other; the objectui-only convergence would have made them disagree.

The mapping question, answered out loud (to be carried upstream, not decided here)

The dispatch requires the heading / subheading mapping to be stated rather than chosen silently. My recommendation for the spec-side card:

  • headingh2, subheadingh3 as the migration hint, not as an alias. Those are the elements the renderer hard-codes today (elements.tsx, variant === 'heading' ? 'h2' : variant === 'subheading' ? 'h3' : 'p'), so the hint preserves the rendered element byte-for-byte for anyone who follows it, which is the only property a migration hint owes.
  • The author then picks freely from h1h6. heading was never a level, it was a role; pinning it permanently to h2 would import the old vocabulary's one-size assumption into the new one and re-create the expressiveness gap the ruling exists to close. The hint names h2 because that is what the author's page renders today — it is a "here is where you were", not a "here is where you must stay".
  • ⚠️ The mechanism is a real upstream design question, not a rename. @objectstack/spec has retiredKey() (ADR-0087 D2) for retiring a key; I found no value-level equivalent under packages/spec/src/shared/. "Named refusal carrying a migration hint" for two enum values needs either that mechanism or a per-value refinement, and it is a narrowing of a published, defaulted enum. That is upstream's call to score and sequence.

#6942's constraint was never at risk, and is untouched

ui:text still does not synthesise body for an absent variant; element:text still does (props.variant ?? 'body'). Nothing in this PR moves either. The ruling itself already settles the asymmetry in the same direction ("element:text keeps its ?? 'body' because it is a different component"), so convergence never forced a decision about absence.


✅ What this PR does land: the pin the card owes

The dispatch calls it out directly, and it is independent of everything blocked above.

Two production registrations claim the short name text. renderers/basic/index.ts imports ./text (which registers ui:text) before ./elements (which registers element:text), and Registry.register() claims the bare key for a namespaced registration if (meta?.namespace && !meta?.skipFallback) — its own comment: "the last registration wins for non-namespaced lookups". So load order alone would hand the bare text key to element:text; skipFallback: true is the single line that stops it, and Registry.ts only console.warns on such an overwrite.

The corpus authors the bare text spelling, so that one flag decides how every authored text node renders. Nothing pinned it.

packages/components/src/__tests__/text-bare-key-resolution-7450.test.tsx pins it, in six parts:

  1. Reachability first — both claimants are registered, so no later assertion can pass by absence.
  2. text resolves to ui:text, asserted by component identity and not only by the type string.
  3. The load order is adverse — derived by reading renderers/basic/index.ts at run time, so re-ordering those imports moves the pin with it rather than leaving it stale.
  4. skipFallback: true is present on element:text and absent on ui:text.
  5. A live control on a fresh Registry instance (never the shared singleton): the same pair registered in the same order keeps the bare key on ui with the flag and hands it to element without it. Without this control, "the bare key is ui:text" would also pass if the fallback path had stopped working or element:text had stopped registering.
  6. The consequence — an authored { type: 'text', variant: 'h1' } node renders an h1 element. This is what the flag protects: element:text has no h1 in its four-value vocabulary and falls through to a p.

Ablation — the pin reds when the thing it describes is broken

skipFallback: true deleted from element:text's registration and nothing else, mutation proven on disk:

HEAD_BLOB=498cbdef4e3ea8e3214ab988e023bfd939caeb78
BEFORE: bytes=19663 skipFallback-lines=5
ANCHOR_OCCURRENCES=1
AFTER : bytes=19641 delta=-22 skipFallback-lines=4
MUT_BLOB=bb82663583082b8f82565094d37d42aed04c29dc

× `text` resolves to `ui:text`, by component identity and not only by name
    AssertionError: expected 'element:text' to be 'ui:text'
× `skipFallback: true` on `element:text` is what overrides that order
    AssertionError: expected undefined to be true
× what the flag protects: an authored `text` node keeps its heading element
    AssertionError: expected 'p' to be 'h1'
Tests  3 failed | 3 passed (6)

RESTORED_BLOB=498cbdef4e3ea8e3214ab988e023bfd939caeb78
DIFF_HEAD_EMPTY=yes · RESTORE OK

Restoration proved twice over: an empty git diff HEAD and blob-hash equality with the HEAD blob. Both ablations ran from the committed state, under the shared verify lock, exit codes captured before any pipe.


Verification

All at 12cc07f9d.

run result
vitest run packages/components apps/console 343 files / 3396 tests passed, VERDICT command-exit 0
pnpm --filter @object-ui/components --filter @object-ui/console run type-check both Done, VERDICT command-exit 0
— the new test is inside that program tsc -p tsconfig.test.json --listFiles: 1 hit for the new file, control text-variant-align-6942 also 1
eslint . in packages/components (the whole package, not a narrowed slice) 463 files, 0 errors, 935 pre-existing warnings, 0 of them mine
node scripts/check-changeset-presence.mjs ✅ 1 changeset, empty frontmatter — the documented exemption for a src/__tests__/ change
node scripts/check-control-bytes.mjs ✅ 6996 tracked text files
pnpm check (CLI built first) ✓ All checks passed
lint:coverage / type-check:coverage / check:lint-rule-coverage all ✅
node scripts/check-governed-queue-guard.mjs --test on both changed paths NOT GOVERNED

apps/console is in the sweep deliberately (excluding it is what reddened PR #8763 in the queue).

Changesets

One, with empty frontmatter: this is a test file plus a changeset, and @object-ui/components ships ["dist","README.md","CHANGELOG.md","LICENSE"] — nothing published moves. The changesets owed for @object-ui/components and @object-ui/types belong to the convergence, which is not in this diff.

Handling

⛔ Draft. ⛔ Not flipped ready, ⛔ not enqueued, ⛔ no auto-merge. needs:contract-review is hung per the dispatch's 双载体 rule, though as landed this diff moves no contract: it is a test file and an empty-frontmatter changeset.

Part of #7450 rather than the closing form the dispatch asked for, and that is a deliberate departure I am reporting rather than making quietly: merging a PR that does not implement the ruling must not close the card that carries it. The half left open is the whole convergence; the half delivered is the resolution pin.


Generated by Claude Code

`element:text` is the LATER registration in `renderers/basic/index.ts`, so
`Registry.register()`'s bare-name fallback would hand it the short `text` key
on load order alone; `skipFallback: true` is the one line that stops it and
`Registry.ts` only `console.warn`s on such an overwrite. Nothing pinned the
resolution, so deleting that flag re-pointed every authored `text` node at the
four-value renderer silently.

The load-order half is derived from the barrel at run time, and the mechanism
carries a live control on a fresh `Registry` instance: the same pair without the
flag hands the bare key to `element:text`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3484.2 KB 3512.7 KB
Main entry chunk (gzip) 144.0 KB 350 KB
Entry file index-CNxX34NY.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 26.68KB 8.94KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 245.88KB 62.05KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.39KB 65.32KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 134.25KB 33.52KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 210.67KB 57.19KB
plugin-kanban (index.js) 56.40KB 16.03KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIERVerdict: PASS WITH FINDINGS (audit reading; director seat, summon #18 segment 3, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-09T09:4xZ)

PR objectui#8795 · head 12cc07f9df1ed6120105767940bc5db3faa45146 (re-read at posting 09:41:02Z; unchanged since 09:01Z) · reviewed 09:34Z–09:40Z.

  • Reviewed-by: isolated claude-fable-5-1 subagent, transcript-verified (73 harness model stamps, all claude-fable-5-1, zero residue; positive control 65 assistant / 48 user role tokens), adopted verbatim below. Fed only the card objectui#7450 and its comments, this PR, and the checked-out trees (objectui + objectstack for the spec-side reading).
  • Implemented-by: the spec@objectui seat session_01Jmxdo7bmeqCQHLSfmLVX9w's dev (mode:subagent), branch claude/issue-7450-element-text-converge-published-nine. Distinct sessions ⇒ not a self-review.
  • Clause-② ruling line for the card, as the checklist asks: test(components): pin the bare text key to ui:text (objectui#7450) #8795 as landed: no (pin-only delivery; the card's yes claim covered the convergence that was routed upstream to objectstack#17108/#17109). ⛔ This seat cleared no carrier and touched no PR state; the release and the Part of card handling stay with the spec@objectui seat.

Verdict: PASS WITH FINDINGS

Head reviewed: 12cc07f9df1ed6120105767940bc5db3faa45146 (matches the requested prefix; PR head.sha identical; single commit on top of origin/main).

Clause-② reading: no for this diff — it adds no exported symbol, schema key, closed-set member, registry entry or protocol key; check-widening-tells.mjs --declaration no --diff <pr8795.diff> reports "no widening tell on any declared surface" (exit 0), and check-clause2-carriers.mjs --pair 8795 (read-only, PM_SWEEP_REPO=objectstack-ai/objectui) returns 0: declaration readable in the fixed spelling, both carriers labelled, no tell. The card's live claim (5598675699) says Clause-②: yes because it dispatched the convergence; the delivered diff is the pin only, so the tier review overturns the declaration to no — per contract-review.md:10-12 that is by design and not a seat fault. The PR body carrying no Clause-②: line is not a defect: the declaration limb is the card's claim comment in the fixed spelling (SKILL.md:642; check-clause2-carriers.mjs header, which records PR-body declarations as the misplaced failure mode), not the PR body.

Governed surface: none — .changeset/7450-text-bare-key-resolution-pin.md and packages/components/src/__tests__/text-bare-key-resolution-7450.test.tsx hit none of GOVERNED_SURFACES (scripts/check-governed-queue-guard.mjs:227-233); Governed Surface Queue Guard check green.

CI on head: green — 33 check runs on 12cc07f9df: 30 success (Lint, Type Check, Test shards 1–4, Build & E2E, Build Docs, all changeset gates), 3 skipped (dependabot, coverage variants — path-conditional). No red.

Findings

  • F1 — non-blocking. text-bare-key-resolution-7450.test.tsx:80-102 (blocks 3 and 4) pin mechanism — the barrel import order in renderers/basic/index.ts read from disk via fs, and skipFallback: true on element:text — rather than the contract. The contract pins are blocks 2, 5, 6 (:70-78, :104-121, :123-130). A benign barrel reorder or an alternative way of keeping the bare key on ui:text would fail blocks 3–4 while the contract holds. The header (:34-44) justifies this deliberately (single-line silent failure, Registry.ts only console.warns). Fix: none required; if the maintainer prefers contract-only pins, drop blocks 3–4 and keep the live control (block 5), which already proves the flag is load-bearing without coupling to file text.
  • F2 — non-blocking (nit). :125 renderComponent({ type: 'text', variant: 'h1', content: 'Title' } as never)as never switches the compiler off for the one node the assertion is about. Fix: type the literal as TextSchema (imported from @object-ui/types, as text.tsx does) or widen renderComponent's parameter; behaviour unchanged.
  • F3 — non-blocking (prose only). PR body "Verification" row calls the empty-frontmatter changeset "the documented exemption for a src/__tests__/ change". scripts/check-changeset-presence.mjs:162-165 says the opposite: there is no carve-out for src/__tests__/; the empty-frontmatter changeset is the required declaration for a change under a fixed-group package's src/. The artifact is correct; only the sentence is loose. Fix: nothing in the diff; the ACCEPT comment can record the precise reading.

Acceptance notes

  • Fixes absence is correct, not a defect. The dispatch literally asked for Fixes #7450, but the PR lands only the pin — the ruling's implementation is routed upstream (objectstack#17108 release 1, #17109 mechanism; maintainer picked option B, 5599600566) and the card must survive. review-checklist.md:8 makes Part of #<n> mandatory when only half lands; a Fixes here would have been the defect. Full-body scan per review-checklist.md:9-11: 0 closing-keyword hits adjacent to any issue ref; first line is `Part of #7450`. The departure was reported (os-dev-report open_questions[2], PR body "Handling") and adopted by the PM as a standing rule (5599277303).
  • Scope: declared narrowing, ratified; no silent widening. premise_still_valid: false was reported on both carriers, and I independently confirmed both legs of the falsification on the trees: /home/user/objectstack/packages/spec/src/ui/component.zod.ts:1819-1820 declares variant: z.enum(['heading','subheading','body','caption']).optional().default('body') on spec 17.3.0; on objectui origin/main, apps/console/src/__tests__/registry-inputs-spec-parity.test.ts:536 lists 'element:text' in the gate's covered set with the refusal message at :1539/:1800, and packages/sdui-parser/src/validate.ts:304 emits code: 'invalid-enum'. The card, census and dispatch premise ("declared nowhere, refused by nothing") is false on main; implementing the ruling here would be the consumer-side widening AGENTS.md #0.1 bans. The pin was explicitly owed by the dispatch ("This PR should pin that resolution"); the diff touches nothing beyond it.
  • Assertions match origin/main. Registry.ts:457-514: bare-key entry stores type: fullType (so getConfig('text').type === 'ui:text' is what main does) and the fallback claim is gated by if (meta?.namespace && !meta?.skipFallback) (:492); renderers/basic/index.ts imports ./text (line 10) before ./elements (line 20); elements.tsx:96-98 registers text under element with skipFallback: true; text.tsx VARIANT_TAG.h1 = 'h1' and wraps when a variant class is present, so block 6's <h1> is correct. test-utils.tsx:22-30 renderComponent resolves via bare ComponentRegistry.get(schema.type) — the resolution path, not SchemaRenderer — which is the right seam for this pin. No prior test pins the bare key (text-variant-align-6942.test.tsx:211 uses getConfig('text', 'ui'), namespaced); not a duplicate.
  • Changeset is required and in the right form. @object-ui/components is public (private unset, files: ["dist","README.md","CHANGELOG.md","LICENSE"]) and in the 40-package fixed group; src/** edits owe a declaration (AGENTS.md:162); empty frontmatter is the correct objectui form (review-checklist.md:22), no major (AGENTS.md:240). Exact precedent: .changeset/14026-list-import-mappings-pin.md with identical "Test only; no package is released by this change." wording. Bump level: none — correct; nothing published moves (tests are excluded from dist per tsconfig.test.json's own header).
  • Docs: nothing stale, nothing to reconcile. content/docs/** has zero mentions of element:text or ui:text; content/docs/guide/plugin-development.md:342 documents skipFallback consistently with the pin; content/docs/components/basic/text.mdx:10-12 documents h1h6 rendering the element they name, which block 6 asserts. The element:text docs gap the census noted is out of scope and deliberately deferred to the follow-up on the released pin.
  • Process form. Draft, base main, needs:contract-review on both carriers, report head 12cc07f9d = PR head, real commands with exit codes, mcp_calls: 0. Unguarded registration in block 5 will emit Registry.ts:499-503's console.warn in test output — noise only, CI green.
  • Card state after ACCEPT is the PM's to handle, not the diff's. On PASS: record the one-line 条款② ruling on the card ("test(components): pin the bare text key to ui:text (objectui#7450) #8795 as landed: no"), strip needs:contract-review from both carriers in the same stroke with a provenance comment (contract-review.md:38), and — because this is Part of — the card will not auto-close: pm:dispatched/assignee need manual handling (review-checklist.md:13-14), with finding(components): two text primitives, two variant vocabularies — ui:text's nine are published and now enforced, element:text's four are declared nowhere #7450 parked pending objectstack#17108 and the objectui pin bump.

Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 9, 2026 09:42
@os-warren
os-warren added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 0e3bca4 Sep 9, 2026
35 checks passed
@os-warren
os-warren deleted the claude/issue-7450-element-text-converge-published-nine branch September 9, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants