fix(components): spell the shipped tabs default items content, the key TabItemSchema declares (objectui#9941) - #9988
Conversation
…e key `TabItemSchema` declares
All three items in the `ui:tabs` registration's `defaultProps` spelled `body`
and omitted `content`, so the default an author drops on a canvas was refused
by the validator this repository publishes for it:
invalid_type ["content"] expected nonoptional, received undefined
`TabItemSchema` declares `content` with no `.optional()` and declares no `body`
member at all, so the key the seed carried was stripped by the parse and the
key the parse required was absent.
No rendered output moves. `tabs.tsx` reads `item.content` first and falls back
to `body` through an `any` cast, so the same nodes move from the fallback arm
onto the primary one; the repair is at the parse. Both halves are asserted, not
assumed, by the new pin.
The pin parses whatever the registry actually carries through the published
schema rather than checking the spelling, so a re-spelling, a fourth item that
omits `content`, or a moved seed all fail it. Its control A feeds the pre-fix
item to the same call and requires a refusal naming `content`, so a parse that
cannot refuse anything cannot pass as green.
The `|| (item as any).body` fallback and `TabItemSchema` itself are untouched:
widening the published accept set to admit `body` would grow an accept set that
is already shipped and pre-empt the dialect question that is still open.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
…ped to the emission criterion
The three `tabs.tsx` `defaultProps` items respelled in the previous commit were
the whole producer table under objectui#9871's criterion, so it is now empty and
four live-tree claims across the two scan test files went red. Each of them
carried an instruction from its own author for exactly this event — re-point the
block and say so in those terms, do not delete the claim — and that is what this
does.
Measured both sides with the unchanged scan against the same uninstalled tree:
base producers=3 filesScanned=5152 readers=4 hits=1069 failedC2=313 unclassified=5
all three carrier 'item', channel 'default-props',
disposition 'unruled:item-carrier', at tabs.tsx :88 :89 :90
head producers=0 filesScanned=5153 readers=4 hits=1068 failedC2=313 unclassified=5
Each re-pointed claim says, and does not exceed:
1. the table is empty UNDER objectui#9871's EMISSION criterion;
2. its last subject was the three tabs.tsx items, respelled to `content`
because `TabItemSchema` declares `content` required and declares no
`body` — a conformance fix, not a dialect migration;
3. this is NOT the repo-wide finish line and no block here claims it is.
`failedC2` held at 313 and `unclassified` at 5 across the same change, so
318 rows outside what the criterion scores as emission did not move, and a
published doc is still teaching the spelling. Emitted and taught are
different verbs; this scan reads only the first;
4. the instrument is not blind. The DIFFERENTIAL test plants `ITEM_PRODUCER`
in a synthetic root and still asserts carrier ['item'], line 6, channel
'default-props', and it passed in the same run that reported the reds.
Every re-pointed block names that fixture, because it is what makes
standing down a live-tree control safe rather than blind.
Two assertions that now pass over an empty set are marked vacuous in place
rather than left silent: the string-carried `toBe(0)` and the disposition loop
over item-carried producers. Both are kept, because each is the claim that fires
the moment a producer returns.
Nothing was deleted, no assertion was skipped, and the scan itself is untouched:
its behaviour is right, its subject moved. Totals are unchanged at 46 tests
across the two files (was 2 failed | 44 passed, now 46 passed), and
packages/components/ holds at 283 files / 2752 tests.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
✅ Console Performance Budget
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
Size Limits
|
⛔
|
| step | ||
|---|---|---|
| 7 | Install dependencies | ✅ success |
| 8 | Run tests (shard 1/4) | ✅ success |
| 9 | Run built-artifact pins (dist project) | ⛔ cancelled |
⇒ ⭐ the test body ran and passed. What was cut off is a later step. ⛔ No assertion in this PR failed, and ⛔ nothing in the shard reported a red test.
Why it is ⛔ not this PR's
| shard | conclusion | started → completed | duration |
|---|---|---|---|
| 4/4 | success | 04:17:10 → 04:34:18 | 17m08s |
| 2/4 | success | 04:17:11 → 04:36:05 | 18m54s |
| 3/4 | success | 04:17:10 → 04:34:53 | 17m43s |
| 1/4 | cancelled | 04:17:10 → 04:37:24 | 20m14s |
The cancelled shard is the longest, by more than a minute over its nearest sibling — and Test (shard 1/4)'s duration is already a carded problem in another lane: objectui#9499 (domain:devx, pm:awaiting-maintainer) measures it at 18.0–19.6 min against its own timing budget. This run put it at 20m14s.
timeout-minutes cap cancelled it is an inference from 「longest shard, cut off mid-step, run conclusion cancelled」, ⛔ not a reading of a cap. The inference is not load-bearing: 「the test body passed and a later step was cut off」 is enough on its own to say this is not an assertion failing in this PR.
The one re-run, spent here
POST …/actions/runs/35420871004/rerun-failed-jobs — accepted. ⭐ That is the one re-run this PR gets. ⛔ 「Flake」 is not a root cause and a second cancellation would be treated as real, not retried.
⛔ Nothing was skipped, disabled or quarantined to get here, and ⛔ no empty commit was pushed to kick CI.
For the record, since this PR's own history invites the question
This PR's earlier red at head 4e2b7a615 was genuine and this PR's own: body-dialect-producer-scan.test.ts and body-dialect-census.test.ts asserted a producer table this change empties. That was root-caused, ruled (objectui#9941 comment 5739237546) and fixed in commit 2b807400c, with Tests 2 failed | 44 passed (46) → Tests 46 passed (46) — identical denominator, so nothing was removed to reach green. ⛔ This cancellation is a different thing entirely and ⛔ does not re-open that.
domain:ui#2 execution seat · session_018HrVaotisyhgmot9o2MLRq · the job's step list, the four shard durations, the run's conclusion and objectui#9499's recorded range were all read from the API in this act.
Generated by Claude Code
Fixes #9941
Clause-②: yes —
packages/componentspublishessrc/, and this moves theui:tabsregistration's
defaultProps, which is the metadata a designer palette writes into a newnode. Declared as
.changeset/9941-tabs-default-items-content.md(
@object-ui/components:patch);node scripts/check-changeset-presence.mjsexits 0 andnames it.
What was wrong, re-taken at source on this branch's base (
1db82373f)The contract,
packages/types/src/zod/layout.zod.ts—:427-:433on this base, not the:420-:426the triage audit cites::432carries no.optional(), socontentis REQUIRED, and the object declares nobodymember at all. The seed,
packages/components/src/renderers/layout/tabs.tsx:88-:90, spelledall three items the other way — and the defaults were unmoved at the numbers the card gives.
The parse, before and after
Measured against the BUILT schema (
packages/types/dist/zod/index.zod.js, frompnpm --filter @object-ui/types build), not the source module:TabItemSchema.safeParse{ label, value, body: [...] }success: falseinvalid_typeat path["content"],expected: "nonoptional"{ label, value, content: [...] }success: truevalue,label,contentOne step up, the whole registration seed as a node —
TabsSchema.safeParse({ type: 'tabs', ...defaultProps })— went from three
invalid_typeissues at["items",0,"content"],["items",1,"content"],["items",2,"content"]tosuccess: true.Control A quoted — the refusal, verbatim from the run
TabItemSchemais astripping
z.object, so the undeclaredbodythe seed carried was silently DROPPED by theparse rather than rejected. The pin records that as its own row rather than leaving a reader to
assume a strict face.
Control B — the UI did not move
tabs.tsx:67readscontentfirst,bodyas a fallback through ananycast — so the seeded nodes were alreadydrawing, from the fallback arm. Respelling moves the same nodes onto the primary arm. Asserted,
not assumed: the pin renders the seed both ways through the real registry renderer and compares
document.body.textContentandinnerHTML(withid/aria-controls/aria-labelledbyblanked, the only attributes Radix mints per mount). Both are equal, and a lit control requires
Content for Tab 1to be in the text so that two EMPTY renders cannot pass as equal.The relation pin was reachable, and it parses rather than spell-checks
packages/components/src/renderers/layout/__tests__/tabs-default-items-parse-9941.test.tsx.A test that checks the three items spell
contentis satisfied forever by a copy-paste. This onereads the seed off
ComponentRegistry.getMeta('tabs', 'ui')— the object a designer paletteconsumes — and the contract off
@object-ui/types/zod— the object a validator consumes — andfeeds one through the other. Nothing in it is a copy of either face. It fails the day anyone
re-spells a member, adds a fourth item that omits
content, or moves the seed.Its rows, and which of them discriminate:
defaultPropsseeditemsarraycontentTabItemSchematabsnodeThe controls being green in BOTH worlds is the claim, not a gap: the repair is at the parse, so a
control row that moved with it would be evidence the repair had been visual after all.
Reverse verification — on-disk proof, then restore proved by hash
Run from the COMMITTED state, mutation written by an anchored replace whose anchor had to match
exactly once per item, with a
trap ... EXIT INT TERMrestore:The three reds are exactly the three subject rows; the failure text is the one an author would
want:
items[0]: invalid_type [content], and the same for 1 and 2. Restore is proved by the blobhash matching
HEADand bygit diff HEADbeing empty — not by the exit code of the checkout.What is fenced out of this change
packages/types/src/zod/layout.zod.tsis untouched. WideningTabItemSchemato admitbodywould turn a misspelled default into an already-published accept set growing, and it would
answer the dialect question ahead of the maintainer.
|| (item as any).bodyfallback at:67is untouched. Retiring that tolerance belongs tothe dialect family — objectui#9871 and objectui#9910, both open and both awaiting a user
decision.
equalityrow is the one row in the pin that reads that arm; when thefallback is retired, that row retires WITH it, and its own docblock says so. The subject rows and
the durable
stillRendersrow do not depend on the arm.ui:tabs. Whether a registration'sdefaultPropsmay diverge fromits published face repo-wide is the open question on objectui#4631, and the sibling pin in the
same directory (
registration-defaults-match-renderer-8229.test.ts) documents at length whydeciding that by gate rather than by ruling is the trap.
TabItem'scanonical child key is
content, notchildren, so that remedy names a key this face does nothave.
Verification
pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' buildpnpm exec vitest run packages/components/src/renderers/layout/__tests__/tabs-default-items-parse-9941.test.tsxTest Files 1 passed,Tests 9 passedpnpm exec vitest run packages/components/(under the shared verify lock)Test Files 283 passed (283),Tests 2752 passed (2752)pnpm --filter @object-ui/components type-checktsc --noEmitandtsconfig.test.json)pnpm --filter @object-ui/components linttabs.tsx(:21twice,:67) are pre-existing and untouched; the new test file produces nonenode scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjs,pnpm check:changeset-claims,pnpm check:pending-changeset-literalspnpm check:control-bytes,check:new-line-citations,check:component-surface-parity,check:test-path-roots,check:unreferenced-sourcesnode scripts/check-governed-queue-guard.mjs --testover the three pathspnpm check:sdui-registration-pinsexits 2(PREREQUISITE NOT MET, ⛔ not a failure) — it weighs
apps/console/dist/assets, and building theconsole needs the whole workspace build closure. That is a repo-level run CI owns. This diff
changes a literal inside an existing registration's
defaultPropsand adds no registration, so itcannot move that gate's verdict; CI is the place that reads it.
The cross-file line addresses above are deliberately in this body and ⛔ not in the source: the
pin cites
TabItemSchemaby SYMBOL, per objectui#8875, andnode scripts/check-new-cross-file-line-citations.mjsexits 0 on this branch.⭐ Seat addendum — the second commit, and why it is in this PR at all
Written by the
domain:ui#2seat at 2026-09-19T04:20Z under the standing clause that a dev writes a PR body once and the seat writes later corrections. Ruling and four-axis reasoning: objectui#9941 comment5739237546.auto-adjudicated, 代裁 channel, with the 否决窗口 open.Respelling the three seed items to
contentemptied thebody-dialect producer table, and two scan test files assert that table is non-empty. The implementer stopped at the face boundary and measured both sides before anything was edited — that measurement is what decided the route.1db82373f4e2b7a615producers{item: 3}{default-props: 3}filesScannedhitsfailedC2unclassifiedreadersThe three BASE rows in full:
tabs.tsx:88,:89,:90, each carrieritem, channeldefault-props, dispositionunruled:item-carrier. Zero node-carried and zero string-carried producers on either side. The deltas reconcile exactly —filesScanned+1 is the new pin file,hits−3 fromtabs.tsxand +2 frombodymentions inside that pin which bucket as test corpus and fail C2 — so nothing but this change moved.⇒ ⭐ those three lines were the tree's last producers of any carrier, not merely its last item-carried ones. That is why re-pointing only the
carrier === 'item'assertions was not enough:producers.length > 0would have stayed red in both files.The affected assertions, including the ones that never reported
producer-scan.test.ts:556carrier === 'item'toBe(0)producer-scan.test.ts:557files.sizeproducer-scan.test.ts:608producers.lengthproducer-scan.test.tstoBe(0)census.test.ts:393producers.lengthcensus.test.ts:405carrier === 'item'census.test.ts:487disposition loop⛔ What the re-pointed claims deliberately do NOT say
Each block now states that the table is empty under objectui#9871's emission criterion, that its last subject was a conformance fix against
TabItemSchemarather than a dialect migration, and — explicitly — that ⛔ this is not objectui#9590's finish line and this PR does not claim it is. The evidence they carry for that refusal:failedC2313 andunclassified5 identical on both sides (318 rows outside what the criterion scores as emission, none of them moved), and objectui#9989 — a published doc still teaching the spelling. ⭐ Emitted and taught are different verbs; this scan reads only the first.Each block also names the DIFFERENTIAL fixture at the top of
producer-scan.test.ts, which plantsITEM_PRODUCERin a synthetic root and still asserts carrier['item'], line 6, channel'default-props'— it passed in the run that reported these reds. ⭐ That is what makes standing down a live-tree control safe rather than blind: the SHAPE stays reachable, only its live occupancy is zero.> 0totoBe(0), so they are now ratchets in the opposite direction — a returning producer goes red, with a message naming what a non-zero means. That follows the file's own idiom rather than introducing one. It is also why objectui#9590's record is where these blocks now point.Nothing was skipped to reach green
Test Files 2 failed (2)/Tests 2 failed | 44 passed (46)→Test Files 2 passed (2)/Tests 46 passed (46). ⭐ 2 + 44 = 46 = 46, so the denominator is identical. Confirmed structurally as well as by the totals:expect(94 → 94 and 92 → 92,it(24 → 24 and 22 → 22, andit.skip/describe.skip/.todo/.onlyreturn nothing across both files.packages/components/stayed at 283 files / 2752 tests across all three runs.⛔
scripts/body-dialect-producer-scan.mjsis untouched — the scan's behaviour is right, its subject moved. ⛔TabItemSchemais untouched. ⛔ The first commit's work is byte-identical:git diff HEAD -- packages/ .changeset/against4e2b7a615is empty.scripts/**has no type-check in this repo — noscripts/tsconfig*.json, and roottype-checkisturbo run type-check, per package. These two files are TypeScript executed by vitest without type-checking, so eslint plus execution is the whole local type surface for them.Acceptance notes
Out of scope for this pull request, ⛔ not ridden along, each measured:
content/docs/guide/schema-rendering.md, the "Lazy Loading" JSON fence, teaches atabsnode as{ "type": "tabs", "lazyLoad": true, "tabs": [ { "title": ..., "body": ... } ] }.Copied verbatim it is refused by the published
TabsSchema:invalid_typeat path["items"],expected array, received undefined— measured against the built schema.tabs,title,bodyand
lazyLoadare all undeclared on that face and are stripped, so the node renders an empty tabbar. Same class as this card (a shipped sample its own schema refuses), different carrier.
Dedupe words:
schema-rendering lazy loading tabs example·tabs key not items·doc example fails TabsSchema·tab title body undeclared·lazyLoad undeclared on tabs.examples/schema-catalog/src/schemas/components-layout-tabs/basic-tabs.jsonalready spelled
items[].contentcorrectly, so the catalog and the registration disagreed witheach other and the catalog was the one that was right. Nothing to repair; recorded because it is
the reason nothing downstream had to change with this seed. Handler: none — no pull request or
person is heading for that file.
Generated by Claude Code
Generated by Claude Code