Skip to content

fix(examples): correct the invented keys the live SchemaExample fixtures teach - #6248

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-6157-schemaexample-invented-keys
Aug 25, 2026
Merged

fix(examples): correct the invented keys the live SchemaExample fixtures teach#6248
yinlianghui merged 1 commit into
mainfrom
claude/issue-6157-schemaexample-invented-keys

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#6157

The five live SchemaExample fixtures behind three component docs pages taught keys and values the shipped @object-ui/types surface does not declare — directly beside prose that #6143 round 2 had just corrected. A reader copies the demo more readily than an interface block.

Verification union run at 22f71cb1b (the final commit on this branch).


⚠️ RIDER ① — every variant occurrence on the two toast fixtures, classified BEFORE any was changed

A blanket find-and-replace over destructive in these files breaks two working buttons. Enumerated by indentation (2-space = top-level, 4-space = nested inside onClick), each anchor proven unique in its file:

filelineJSON pathgoverning declarationverdictaction
components-feedback-toast/destructive.json4$.variantButtonSchema.variantform.d.ts:30, zod/form.zod.js:153'destructive'IS a memberKEPT UNCHANGED
components-feedback-toast/destructive.json7$.onClick.variantToastSchema.variantfeedback.d.ts:123, zod/feedback.zod.js:59not a member'error'
components-feedback-toast/error-toast.json4$.variantButtonSchema.variant'destructive'IS a memberKEPT UNCHANGED
components-feedback-toast/error-toast.json7$.onClick.variantToastSchema.variantnot a member'error'

The sed used was anchored to ^ "variant": "destructive",$ (4-space only). Proven on disk after the edit: injected ^ "variant": "error",$ → 1 per file; removed ^ "variant": "destructive",$ → 0 per file; untouched^ "variant": "destructive",$ → still 1 per file. The first describe block of the new test exists solely so a future blanket replace turns this file red.

⚠️ RIDER ② — the toast change alters rendered styling, and that is the declared intent

'destructive''error' is intended to change what the demo renders, not a cosmetic edit. The toast renderer (packages/components/src/renderers/feedback/toast.tsx:17-21) maps variant onto sonner's functions: 'error'toast.error (red/destructive styling); an unrecognised value such as 'destructive' falls through to plain toast() (neutral). A reviewer seeing a colour change in a toast demo is seeing the intended correction.

One honest qualification, measured rather than assumed: on this tree the key sits inside a button.onClickaction payload, and I could find no dispatcher in packages/** that reads onClick.action — see "What I measured that the card did not" below. So today the visible effect is on the JSON source the demo publishes; the styling difference above is what lands the moment that payload is wired.

⚠️ RIDER ③ — the radio-group change is inert until #6158 lands, and that is expected

packages/components/src/renderers/form/radio-group.tsx on main reads neitherdirection nor orientation (grepped; no match for either). So the vertical and horizontal demos still render identically after this PR. #6158 is the renderer half. This was not a reason to skip, delay, or wait — the fixture is now correct against the declaration, and the renderer half lands separately.

⛔ The cross-card assertion neither PR can make

Triage asked that "the horizontal demo's markup actually diverges once both land" be asserted. Neither PR can assert it.#6158 changes the renderer but not the fixtures; this PR changes the fixtures but not the renderer. The divergence becomes observable only once both #6157 and #6158 have landed, and neither PR asserts it — no fixture-shaped stub was written to fake a green. Naming the gap is deliberate; the new test's header comment records it in the same words so the next reader does not mistake the silence for coverage.


⚠️ Trap ① — why a green CI run is NOT evidence these corrections are right

Nothing in CI parses these files as schemas. check-doc-component-types reads type literals out of docs code blocks (green here, and it would be green whatever key I wrote); check-doc-snippet-types compiles ts/tsx fences and never opens a .json fixture. objectui#5250 already records that hole. The gates going green means I broke nothing — it is not evidence the corrections are correct. The per-key measurement below is the verification, and examples/schema-catalog/test/component-fixture-declared-keys.test.ts is what makes it repeatable in CI.

Per-key measurement against the INSTALLED shipped surface — every probe carries a control

packages/types built fresh from this tree (pnpm --filter @object-ui/types build, exit 0) before any judgement. The five keys do not share one rejection class, which the card's single "shipped says" column flattens:

#keyzod verdict.d.ts verdictclass
1toast payload variant: "destructive" (×2)REDinvalid_value, expected one of "default"|"success"|"warning"|"error"|"info"RED — TS2322value rejection
2shortcut on CommandItem (×3 entries, 1 key)GREEN but silently STRIPPED (bare z.object)RED — TS2353 'shortcut' does not exist in type 'CommandItem'key rejection, TS only
3direction on radio-group (×2)GREENBaseSchema is .passthrough() (zod/base.zod.js:171)GREENBaseSchema carries [key: string]: anyrefused by neither

Controls, all of which parsed/compiled green as predicted — without them these would be measurements of a schema that refuses everything rather than key-by-key results:

  • ToastSchema.safeParse({… variant: 'error' …}) → GREEN (control for row 1)
  • RadioGroupSchema.safeParse({… orientation: 'vertical' …}) → GREEN (control for row 3)
  • CommandGroupSchema.safeParse(item without shortcut) → GREEN, and byte-identical to the parse output of the item withshortcut — which is exactly how the strip was detected
  • const b7: ButtonSchema = { type: 'button', variant: 'destructive' }compiles GREEN — the control that pins RIDER ①
  • const b2: RadioGroupSchema = { … orientation: 'vertical' … } / const b4: CommandItem = { value, label, icon } → compile GREEN

One prediction of mine was wrong and is worth recording: I predicted direction on a RadioGroupSchema object literal would raise TS2353. It does notBaseSchema's index signature accepts it. So row 3 is not a "type rejects it" case at all; the authority is that RadioGroupSchema declares orientation (form.d.ts:377, zod/form.zod.js:263), that direction appears nowhere in its shape, and that nothing reads it. The new test asserts that structurally, and includes an explicit probe showing .success stays true for the invented key — because an assertion on .success here would assert nothing.

Liveness — each fixture is rendered by a SchemaExample on a docs page

fixturedocs pageline
components-feedback-toast/destructivecontent/docs/components/feedback/toast.mdx18 (under ## Variants)
components-feedback-toast/error-toastcontent/docs/components/feedback/toast.mdx53 (under ### Error Message)
components-form-command/command-palette-with-shortcutscontent/docs/components/form/command.mdx44
components-form-radio-group/vertical-layoutcontent/docs/components/form/radio-group.mdx21
components-form-radio-group/horizontal-layoutcontent/docs/components/form/radio-group.mdx22

SchemaExamplegetExample(id)InteractiveDemo, so the JSON is both rendered and published as copyable source. None of the five is dead fixture data.

Census re-derived on today's main — one delta

The card says "5 fixture keys across 4 pages". Re-derived at e3354ba08: 5 fixture keys across 5 files on 3 docs pages (toast, command, radio-group). The "4" does not reproduce — #6143 round 2 corrected prose on four pages, and the fixture half lands on three of them. Key count and file list are otherwise exactly as measured in the card; nothing moved under tonight's docs PRs.

I also swept the whole fixture tree for the same three spellings rather than trusting the list. "direction" appears in 14 files — 12 are legitimate (flex.direction, resizable.direction, a sort descriptor's direction), and only the two radio-group fixtures carry it on a radio-group node; components-form-radio-group/form-field.json matched the grep but carries direction on its outer flex, correctly, and was left alone. "shortcut" appears in one other file (menubar) — filed separately, see below, not touched here.


What I measured that the card did not

ButtonSchema.safeParse() on either toast fixture is RED, and not because of any variant:

{"code":"invalid_type","expected":"function","path":["onClick"],"message":"Invalid input: expected function, received object"}

ButtonSchema.onClick is declared as () => void | Promise<void> (form.d.ts:56, zod/form.zod.js), while every toast demo hangs an { action: 'toast', … } object off it, and no dispatcher anywhere in packages/** reads onClick.action. The whole button→action-payload idiom these demos teach is undeclared and, on this tree, inert. That is out of scope here and filed separately; it is also why RIDER ②'s styling statement is qualified above. ToastSchema remains the right authority for the nested key — the payload's other keys (title, description, duration) are exactly ToastSchema's.

Changeset — the gate's own verdict, not my inspection

$ node scripts/check-changeset-presence.mjs
Compared the working tree with e3354ba08 (merge-base with origin/main): 6 file(s) changed,
0 of them published source of a package the release covers, 0 under a package changesets
ignores, 0 changeset(s) added.
✅ No source of a released package changed in this range, so no changeset is owed.

No changeset added. check-changeset-no-major also green (vacuously — no changeset to declare a bump).


The new test, and the ablation that proves it bites

examples/schema-catalog/test/component-fixture-declared-keys.test.ts (+201 lines) — 16 tests, all green. Every assertion block carries a paired counter-probe so the file cannot pass by asserting nothing.

Ablation (fixtures reverted to their pre-#6157 content via git checkout e3354ba08 -- <5 paths>, run under trap … EXIT INT TERM, restored automatically; git diff HEAD --stat empty afterwards). Direction predicted before running: 7 red / 9 green, naming which seven. Observed, exactly:

× components-feedback-toast/destructive payload parses green under ToastSchema
× components-feedback-toast/error-toast payload parses green under ToastSchema
× item 0 loses no authored key when parsed
× item 1 loses no authored key when parsed
× item 2 loses no authored key when parsed
× components-form-radio-group/vertical-layout declares orientation=vertical and no direction
× components-form-radio-group/horizontal-layout declares orientation=horizontal and no direction
Test Files 1 failed (1)
Tests 7 failed | 9 passed (16)

Mutation proven on disk before the run by grepping the injected text and separately the removed text (nested "variant": "destructive" → 1 per file; "variant": "error" → 0; "shortcut" → 3; "direction" → 1 per radio file; "orientation" → 0), with the landing site printed. No rebuild step is involved in this ablation and none was skipped: the test imports ../src/index.js, which imports the JSON fixtures directly, so the mutation reaches the run without a dist hop. @object-ui/types (which the test parses against) was built once, before any measurement, and is not what the ablation mutates.

Which assertions would still pass on a revert

Stated plainly, because most of the file would:

  • 9 of the 16 tests survive a revert — every control and every counter-probe. ButtonSchema declares destructive; both toast fixtures keep a top-level destructive button (that key is not part of the revert); ToastSchema refuses 'destructive'; CommandItemSchema silently strips an undeclared key; RadioGroupSchema declares orientation and not direction; .passthrough() accepts direction. Those are facts about the shipped types, not about this diff, and they are supposed to hold either way — that is what makes them controls.
  • 7 tests fail on a revert — the ones listed above, one per corrected key.
  • Every CI gate would still pass on a full revert: check-doc-component-types, check-doc-snippet-types, check-control-bytes, check-changeset-presence, type-check, lint. This is Trap ① restated as a property of the tree: before this PR nothing in CI could tell the difference, which is precisely why the new test file is part of the change rather than the fixture edits alone.

Verification run (all at 22f71cb1b)

whathowresult
new testnpx vitest run examples/schema-catalog/test/component-fixture-declared-keys.test.tsfrom repo rootTests 16 passed (16)
whole catalog suitenpx vitest run examples/schema-catalog/test --maxWorkers=2from repo rootTest Files 13 passed (13), Tests 1784 passed (1784)
catalog type-checkpnpm --filter @object-ui/example-schema-catalog type-check (tsc --noEmit && tsc -p tsconfig.test.json)exit 0
dependency closurepnpm --workspace-concurrency=2 --filter '@object-ui/example-schema-catalog^...' buildexit 0, run before the type-check
check-changeset-presencenode scripts/…✅ No source of a released package changed in this range, so no changeset is owed.
check-changeset-no-majornode scripts/…✅ No changeset declares a 'major' bump.
check-control-bytesnode scripts/…✅ check-control-bytes: OK (scanned 5152 tracked text file(s); skipped 85 binary).
check-doc-component-typesnode scripts/…✅ Every documented component type is registered.
check-type-check-coveragenode scripts/…✅ test type-check coverage: 41/41 packages compile their tests, 0 declared debt…

All vitest runs were issued from the repo root: pnpm --filter <pkg> exec vitest and cd packages/x && pnpm exec vitest hit objectui#3378, which silently runs apps/console's 22 files and reports them green while running none of the target's. The 16/16 and 13-file counts above match this package's own file and test counts exactly, which is the check that the guard was not hit.

Lint was narrowed, and here is the proof the narrowing excluded nothing. Instead of the repo-wide pnpm lint, I ran eslint on the changed files only. (1) The universe comes from eslint's own configuration, not my guess: of the six files in this diff, the five .json fixtures are outside it — eslint reports File ignored because no matching configuration was supplied for them — leaving exactly one file in scope. (2) The count is read from --format json: 1 file linted, errorCount: 0, warningCount: 0. (3) Invariance for untouched files: eslint.config.js configures no projectService, no project, and no parserOptions, so type-aware linting is not enabled and nothing in this diff can move the verdict on a file it does not contain. CI runs the full farm regardless.

Boundary with #6158 — checked, not assumed

packages/components/src/renderers/form/radio-group.tsx and its registry meta entry are #6158's and are not touched here (git diff --name-only is six files, all under examples/schema-catalog/). Checked in the other direction too: the last commit to touch examples/schema-catalog/src/schemas/components-form-radio-group/ is 9986f9114, long predating #6158, and #6158's own diff is two files under packages/components/src/renderers/form/. The boundary was respected in both directions.


Generated by Claude Code

…res teach
The five fixtures behind the toast, command and radio-group component demos
carried keys and values the shipped @object-ui/types surface does not declare,
directly beside prose that #6143 round 2 had just corrected. A reader copies
the demo more readily than an interface block.
- toast: the variant nested in the onClick toast payload 'destructive' -> 'error'
(ToastSchema, feedback.d.ts:123 / zod/feedback.zod.js:59). The TOP-LEVEL
"variant": "destructive" on both files is a genuine ButtonSchema member
(form.d.ts:30) and is deliberately untouched.
- command: drop the invented `shortcut` key from the three CommandItem entries
(form.d.ts:1329 declares value/label/icon only; the renderer reads neither).
- radio-group: `direction` -> `orientation`, the declared slot (form.d.ts:377).
Adds test/component-fixture-declared-keys.test.ts, which measures each of the
three distinct rejection classes with a paired counter-probe — the docs gates
are fence-blind to JSON fixture keys, so their green is not evidence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Two angle-bracket fragments in the PR body above were eaten by GitHub's body sanitizer on write. Restating them here, where bare text survives, rather than leaving the body reading as if I wrote them that way:

  1. Under "What I measured that the card did not", the body now reads "ButtonSchema.onClick is declared as () => void | Promise". The declaration at form.d.ts:56 is onClick?: () => void | Promise&lt;void&gt; — the &lt;void&gt; was stripped.

  2. In the paragraph on objectui#3378, the body now reads "pnpm --filter exec vitest". The intended text is pnpm --filterPKGexec vitest — the placeholder in angle brackets was stripped.

Neither changes a measurement; both are recorded so the next reader does not read the gap as sloppiness or as a different claim.


Generated by Claude Code

Sign up for freeto 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.

finding(examples): the live SchemaExample fixtures on four component pages teach the same invented keys the prose blocks just stopped teaching

2 participants

@yinlianghui@os-litant