Skip to content

fix(site): SchemaNode crosses to SchemaRenderer through the bridge — the docs site builds again (#4617) - #4621

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4617-site-schemanode-bridge
Aug 14, 2026
Merged

fix(site): SchemaNode crosses to SchemaRenderer through the bridge — the docs site builds again (#4617)#4621
yinlianghui merged 1 commit into
mainfrom
claude/issue-4617-site-schemanode-bridge

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4617.

main is red and has been since PR #4608 merged: Build Docs runs pnpm turbo run build --filter='@object-ui/site', next build type-checks the site, and the site's five SchemaRenderer call sites stopped compiling the moment SchemaNode became a union. Every open PR inherits it.

⚠️ Note on one character in the quoted output below. GitHub's body sanitizer strips a < immediately followed by a letter, treating it as an HTML tag — it would delete < SchemaRenderer … /> and turbo's < NONEXISTENT > marker outright. Every such token here therefore carries one space after the <, following PR #4608's convention. The space is an artifact of the sanitizer, not part of the real text.

Red first — measured against origin/main, not inherited from the card

CI, run 31740683664 (main push, c1d939f7f = the merge of #4608), job Build Docs, step Build Site. The immediately preceding main run 31736774667 was green:

  Creating an optimized production build ...
✓ Compiled successfully in 40s
  Running TypeScript ...
app/components/InteractiveDemo.tsx(74,43): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
app/components/InteractiveDemo.tsx(119,35): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
app/components/LiveSplitDemo.tsx(286,37): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
app/components/SchemaThumbnail.tsx(138,35): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
app/playground/page.tsx(1413,39): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
Failed to type check.

Reproduced locally on a worktree at origin/main — same five, byte-identical:

$ pnpm --filter @object-ui/site types:check
app/components/InteractiveDemo.tsx(74,43): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
… (identical for 119,35 · LiveSplitDemo 286,37 · SchemaThumbnail 138,35 · playground 1413,39)
 ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  @object-ui/site@3.1.0 types:check
Exit status 2

One correction to the card, kept rather than quietly fixed. #4617's body transcribes the fourth error as SchemaThumbnail.tsx(145,35). Both the CI log and the local reproduction say 138,35; apps/site has not changed since c1d939f7f (git log c1d939f7f..HEAD -- apps/site is empty), so 145 is not a stale line number — most likely it came from the filer's own #4615 branch, which edits that file. The other four line numbers match exactly.

The fix — the bridge, at five sites

All five values are typed SchemaNode from @object-ui/core, which since #4608 re-exports @object-ui/types' union BaseSchema | string | number | boolean | null | undefined. SchemaRenderer's prop is narrower on purpose (SchemaRenderer.tsx:226): schema: BaseSchema | string | null | undefined. The sanctioned crossing is toRenderableSchema, which #4608 explicitly preserved for this — quoting its own must-not-change section:

toRenderableSchema in packages/react remains. SchemaRenderer's component-level union deliberately excludes number/boolean (#4548 ruling Q2), so the bridge still normalizes those after reconciliation.

It is imported from @object-ui/react's published surface — packages/react/src/index.ts:10 is export * from './schema-input', so no deep import is involved.

# File : line Expression Was
1 app/components/InteractiveDemo.tsx:74 toRenderableSchema(example.schema) example.schema
2 app/components/InteractiveDemo.tsx:119 toRenderableSchema(schema) schema
3 app/components/LiveSplitDemo.tsx:286 toRenderableSchema(lastValidSchema) lastValidSchema
4 app/components/SchemaThumbnail.tsx:138 toRenderableSchema(schema) schema
5 app/playground/page.tsx:1413 toRenderableSchema(schema) schema

Plus one import line per file (four files). No prop widening in @object-ui/react, no as cast, and toRenderableSchema's implementation is untouched — this PR consumes it.

must-not-change: rendered behavior, and the primitive is genuinely reachable

The bridge is identity for every object schema and for null/undefined; it maps only number/boolean to their text form. That this changes nothing is read from the renderer, not assumed — SchemaRenderer.tsx:483 and :489:

  if (typeof evaluatedSchema === 'string') return < >{evaluatedSchema}< / >;
  // Any other primitive that reached here renders as its text too
  // (objectui#4548). …
  if (typeof evaluatedSchema !== 'object') return < >{String(evaluatedSchema)}< / >;

So 42 took the second branch and rendered String(42); bridged, it takes the first branch as "42". Same text node either way. The two branches are the same function composed in the other order.

Two of the five sites can really receive a bare primitive today, so this is not a formality:

  • LiveSplitDemo.tsx:153setLastValidSchema(parsed as SchemaNode) where parsed = JSON.parse(text) from the live-editable textarea. Typing 42 in that box is valid JSON.
  • playground/page.tsx:1244setSchema(parsed), same shape, from the playground editor.

The other three (InteractiveDemo's two props, SchemaThumbnail's prop) are authored by MDX/gallery callers, where a bare string is the plausible input and passes through untouched. The bridge is total at all five, which is why the ruling's "still the right spelling" holds even where a primitive is unlikely.

The gate gap — why this reached main green

apps/site's type-check script was named types:check, but the turbo task is type-check. Turbo therefore listed the package in the graph with no command to run:

  site entry: @object-ui/site#type-check | command: '< NONEXISTENT >'

Renamed to type-check (nothing else in the repo referenced the old name — one grep hit, its own definition). Measured with turbo run type-check --dry=json, counting entries whose command is not the < NONEXISTENT > marker:

Filter Graph entries Runnable before Runnable after apps/site runs
...@object-ui/react 70 (unchanged) 67 68 no → yes
...@object-ui/types 80 (unchanged) 77 78 no → yes

Those before-counts are worth a second look: 67 and 77 are exactly the "67 / 67 successful" and "77 / 77 successful" that PR #4608's own canary table reported as full green. The canary was honest about what it ran; apps/site simply was not in it, in either direction.

scripts/check-type-check-coverage.mjs carried @object-ui/site in CHECKED_BY_OWN_BUILD. That gate's own ratchet (line 532) requires the entry to die once the package gains a type-check script, so it is deleted and the table is now empty, with a comment recording why it should stay that way:

before: ✅  type-check coverage: 44/46 via `type-check`, 1 via their own build, 0 known-broken (0 errors outstanding), 1 not compiled.
after:  ✅  type-check coverage: 45/46 via `type-check`, 0 via their own build, 0 known-broken (0 errors outstanding), 1 not compiled.

The retired entry's own caveat had predicted this failure in the abstract — "a PR that only touches a workspace package in transpilePackages therefore does not re-check the site until it lands" — and called it a cost/coverage call. #4608 is that PR, and the answer turned out to be ~5 hours of red main. The exemption was accurate about what checked the site and silent about when; "when" was the half that mattered. verifyNoIgnoreBuildErrors retires with it rather than being orphaned: it protected a coverage claim that rested on next build, and coverage no longer rests there.

The remaining "1 not compiled" is @object-ui/example-hello-world, unchanged. (#4617's second half describes apps/site as that "1 not compiled" package; it was in fact the "1 via their own build". No consequence for the fix.)

Green

$ pnpm --filter @object-ui/site type-check
> fumadocs-mdx && next typegen && tsc --noEmit
[MDX] generated files in 20.86880099999999ms
[MDX] generated files in 13.919562000000042ms
Generating route types...
✓ Types generated successfully
### exit: 0 ###

Build Docs' exact step, run verbatim on this branch:

$ pnpm turbo run build --filter='@object-ui/site' --concurrency=2
@object-ui/site:build: Route (app)
@object-ui/site:build: ┌ ○ /
@object-ui/site:build: ├ ○ /_not-found
…
@object-ui/site:build: └ ○ /playground

 Tasks:    29 successful, 29 total
Cached:    28 cached, 29 total
  Time:    1m10.29s
### exit: 0 ###

And the newly-aligned turbo task, which now actually executes rather than being skipped:

$ pnpm exec turbo run type-check --filter='...@object-ui/react' --concurrency=2
@object-ui/site:type-check: cache miss, executing 3964711d030aa302
@object-ui/site:type-check: > fumadocs-mdx && next typegen && tsc --noEmit
@object-ui/site:type-check: ✓ Types generated successfully
 Tasks:    68 successful, 68 total
Cached:    34 cached, 68 total
  Time:    3m18.652s
### exit: 0 ###

cache miss, executing is the line that matters: before this PR that package contributed no task to run at all.

Verification

Check Result
check-control-bytes.mjs PASS — 4393 tracked text files scanned
check-phantom-dependencies.mjs PASS — every in-scope import declared
check-changeset-presence.mjs PASS — 6 files changed, 0 under a released package's src/, 0 changesets owed
check-changeset-no-major.mjs PASS
check-changeset-fixed.mjs PASS
check-type-check-coverage.mjs PASS — 45/46 (was 44/46)
check-lint-coverage.mjs PASS — 46/46
check-doc-links.mjs PASS — 13 scan roots
vitest run scripts/ 42 files, 957 tests passed — includes the #3904 site-host guard and this gate's own suite
eslint, 4 touched files 4 problems (0 errors, 4 warnings) on both sides — byte-identical to origin/main in a compare worktree; net zero

Control-byte self-scan (grep -naP over the widened class, not just NUL) clean on all six touched files.

CI on this branch — converged

All 18 check runs completed; 16 success, 2 skipped by their own conditions (dependabot, Test (coverage)). The headline:

Job Conclusion
Build Docs success — the job that has been red on every main push since #4608
Type Check success — and it now reaches apps/site for the first time
Lint success
Changeset Declaration success
Build & E2E · Test shards 1-4/4 success
Control Byte Scan · Internal Docs Link Check · Changeset Fixed Group · Skill Guide Path success

Changesets

None, and the gate agrees rather than being overridden:

Compared the working tree with 8640cec19 (merge-base with origin/main): 6 file(s) changed,
0 of them under the src/ 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.

apps/site is "private": true and publishes nothing; the script rename and the gate-table edit are tooling. Never major.

No skip-changeset label is applied, deliberately — that label does not exist in this repo. changeset-presence.yml runs node scripts/check-changeset-presence.mjs with no label escape hatch, and scripts/__tests__/ci-cd-pipeline-doc.test.ts:184 records that a "changeset gate skippable with a skip-changeset label" was one of the five phantom workflows objectui#3724 deleted — "neither the workflow nor the label was ever real". Applying it here would be re-creating that phantom.

Scope

packages/react untouched — prop types and toRenderableSchema's implementation alike. packages/plugin-dashboard/src/** untouched (#4614). The five sites are fixed as they exist on origin/main; #4615's branch was not pulled. content/docs/releases/** untouched. No git stash at any point.


Generated by Claude Code

…the docs site builds again (#4617)

`Build Docs` (`next build`, which type-checks apps/site) has failed on every
push to `main` since PR #4608 landed, with 5x TS2322 at the five call sites
that hand a `SchemaNode` to `SchemaRenderer`'s `schema` prop.

Route all five through `toRenderableSchema`, the adapter #4548-Q2 kept for
exactly this crossing: `SchemaNode` deliberately includes `number`/`boolean`,
SchemaRenderer's component union deliberately excludes them. Not a prop
widening, not a cast.

Also close the gate gap that let this reach `main` green: apps/site's
type-check script was named `types:check`, so `turbo run type-check` listed it
as `<NONEXISTENT>` and never ran it. Renamed to `type-check`; its
CHECKED_BY_OWN_BUILD exemption retires with the rename, as that gate's own
ratchet requires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 14, 2026 1:41am

Request Review

@yinlianghui
yinlianghui marked this pull request as ready for review August 14, 2026 01:54
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 14, 2026
Merged via the queue into main with commit 3dc9a82 Aug 14, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4617-site-schemanode-bridge branch August 14, 2026 01:55
os-sales pushed a commit that referenced this pull request Aug 21, 2026
…#4622)

The closing paragraph of `toRenderableSchema`'s JSDoc said the two competing
repo-wide `SchemaNode` spellings "have not been reconciled" and that "when it
lands, the call sites using this can go back to forwarding directly". Both
halves went false when PR #4608 merged, and the second half is the harmful one:
it is an instruction whose trigger condition has now fired, sitting directly
above the function a future author is about to call.

The reconciliation (objectui#4580 / PR #4608) resolved the collision in favour
of `@object-ui/types`' union — `packages/core/src/types/index.ts:41` is now
`export type { SchemaNode } from '@object-ui/types';` — while SchemaRenderer's
prop stays deliberately narrow per objectui#4548 ruling Q2
(`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So
a `SchemaNode` became LESS assignable to that prop, not more, and the bridge is
a permanent crossing between two intentionally different types.

Following the old instruction has a measured cost, and the new paragraph names
it: five `apps/site` call sites were forwarding directly when PR #4608 landed,
and `Build Docs` was red on `main` for roughly five hours until PR #4621 routed
all five through this function (objectui#4617).

Comment-only. The emitted JavaScript is byte-identical (both revisions of the
file transpiled with `tsc --removeComments`, sha256
ce9ba5c4ce7590dce63e5ca853cfb7a79d55df338d1fd32403b215b39cefda06 on each side),
and no changed line falls outside the JSDoc block. The paragraph does ship,
which is why a patch changeset is owed: this package builds with plain `tsc`,
so the comment is carried into `dist/schema-input.d.ts`.

Fixes #4622

Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK

Co-authored-by: Claude <noreply@anthropic.com>
os-sales pushed a commit that referenced this pull request Aug 21, 2026
…o stale

PM ruling on the wording of the TS2322 clause: neither "against this function's return
type" alone nor a bare `string | BaseSchema | null | undefined` alone, but both, with the
literal spelling scoped as a historical quotation.

The reasoning is the axis I had not weighed. Describing the type by its ROLE is
self-updating — if `SchemaRendererProps['schema']` gains or loses a member tomorrow, the
sentence stays true with no edit. Hardcoding today's spelling as a present-tense claim is
NOT self-updating, and would have added a fresh way for this paragraph to go false, which
is the exact defect the paragraph exists to fix. Attributing the spelling to what #4617's
logs said in August 2026 is a fixed fact that cannot rot however the union evolves, and it
hands a reader with the log open the exact string to match in one hop.

    - `number` against this function's return type — until PR #4621 routed all five
    - through here (objectui#4617).
    + `number` against this function's return type, spelled
    + `string | BaseSchema | null | undefined` in objectui#4617's logs — until
    + PR #4621 routed all five through here.

The paragraph grew, so both byte figures in the changeset are re-measured rather than
left stale — the same build, at both revisions, with `dist/` cleared between runs:

    dist/schema-input.js   1,486 B -> 2,377 B   (1.45 KB -> 2.32 KB)   +891 B
    gzipped                  850 B -> 1,266 B   (0.83 KB -> 1.24 KB)   +416 B

previously 2,316 B / 1,246 B for the shorter paragraph. All 19 differing lines in the
emitted file are JSDoc continuations (was 18) and the three executable lines remain
byte-identical, so the growth is still the paragraph and nothing else.

Noted for next time, per the PM: quoting exact byte counts in a changeset couples it to
the comment's length, which is what made this edit cost a re-measure. Still the right
call here — the numbers are the honest replacement for a claim that was wrong — but a
cost to choose deliberately rather than by default.

Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK

Co-authored-by: Claude <noreply@anthropic.com>
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.

main is RED: Build Docs fails on SchemaNode vs SchemaRenderer's schema prop — the docs site has not built since #4608 landed

2 participants