Skip to content

docs(react): toRenderableSchema's header says the bridge is permanent (#4622) - #5602

Merged
os-sales merged 3 commits into
mainfrom
claude/issue-4622-torenderableschema-header
Aug 21, 2026
Merged

docs(react): toRenderableSchema's header says the bridge is permanent (#4622)#5602
os-sales merged 3 commits into
mainfrom
claude/issue-4622-torenderableschema-header

Conversation

@os-sales

@os-sales os-sales commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4622.

One paragraph of prose, at the bottom of toRenderableSchema's JSDoc in packages/react/src/schema-input.ts. The first two paragraphs are accurate and are untouched.

Correction, kept in place rather than quietly edited away. The first revision of this PR and its changeset both claimed "the emitted JavaScript is byte-identical". That was false. The measurement behind it is retracted and re-done below, against the build this repo actually performs. The correction is recorded here because a claim that does not survive being checked is the exact defect this card exists to fix, and silently swapping the sentence would leave the reviewer no way to see it happened.

What was wrong

The closing paragraph 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". That is not a stale fact — it is an instruction whose trigger condition has already fired, sitting directly above the function every future author calls. Acting on it reproduces objectui#4617 exactly.

Every claim in the replacement, verified against origin/main rather than taken from the card

The card exists because a header made a confident claim that later went false, so each new claim was read out of the code on 7a28e1e3f before it was written down.

1. The reconciliation landed, and core re-exports the unionpackages/core/src/types/index.ts:41:

export type { SchemaNode } from '@object-ui/types';

and the declaration it now points at, packages/types/src/base.ts:334:

export type SchemaNode = BaseSchema | string | number | boolean | null | undefined;

2. SchemaRenderer's prop is still deliberately narrow, and still excludes number / booleanpackages/react/src/SchemaRenderer.tsx, the interface and the sentence above it:

 * handles: an object schema, a bare string (rendered as text), or nothing at
 * all. `number` / `boolean` are deliberately excluded  the runtime tolerates
 * them defensively (see the primitive guard in the evaluation memo) but no
 * author should be invited to pass them.
export interface SchemaRendererProps {
  schema: BaseSchema | string | null | undefined;
}

So the two types are intentionally different, in the direction that makes the bridge permanent: a value typed SchemaNode is now less assignable to that prop than it was, not more.

3. The ruling and PR #4608 agree with the code — from #4608's 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."

4. The measured cost of the old reading — objectui#4617: five apps/site call sites (InteractiveDemo.tsx twice, LiveSplitDemo.tsx, SchemaThumbnail.tsx, playground/page.tsx) were forwarding directly when #4608 landed, each reported as error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'. Merge timestamps bound the red window: #4608 merged 2026-08-13T20:24:13Z, #4621 merged 2026-08-14T01:55:18Z — 5h31m, i.e. the "roughly five hours" the new paragraph states. All five sites route through this function today (grep for toRenderableSchema in apps/site).

Nothing had moved, so there was nothing to stop for.

The replacement paragraph

 * The bridge is PERMANENT — not scaffolding awaiting a merge. The two competing
 * repo-wide `SchemaNode` spellings ARE reconciled (objectui#4580, PR #4608):
 * `@object-ui/core` stopped hand-declaring its own interface and now re-exports
 * `@object-ui/types`' union, so one declaration is left to disagree with. That
 * reconciliation resolved in favour of the UNION, while `SchemaRenderer`'s prop
 * stays deliberately narrow (objectui#4548 ruling Q2 — it declares no `number`
 * or `boolean`), so a `SchemaNode` is now LESS assignable to that prop than it
 * was, not more. This step therefore bridges two intentionally different types,
 * and it stays. ⛔ Do not "tidy" a call site back into a direct forward: the
 * five `apps/site` sites that were forwarding directly when PR #4608 landed are
 * what kept `Build Docs` red on `main` for ~5 hours — each one a TS2322 naming
 * `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. An earlier revision of this paragraph
 * said the reconciliation was still pending and invited exactly that edit.

Why the TS2322 clause is worded in two halves

A paragraph whose subject is a claim that rotted should not introduce a new way to rot. Naming the type by its role ("this function's return type") is self-updating: if SchemaRendererProps['schema'] gains or loses a member, that clause stays true with no edit. Naming today's spelling as a present-tense fact would not be — it would hardcode a type into prose, which is the defect this card exists to fix.

So the literal spelling is scoped as a historical quotation of what #4617's logs said, which is a fixed fact that cannot go stale however the union evolves, and which hands a reader with the log open the exact string to match in one hop. The role-based half and the quoted half do different jobs; keeping both costs about twenty bytes.

The equivalence behind the role-based half is not folklore either: the function is annotated : SchemaRendererProps['schema'], and SchemaRenderer.propsResolution.test.ts pins Equal<ReturnType<typeof toRenderableSchema>, SchemaRendererProps['schema']>, so a future edit that breaks the correspondence fails a type assertion rather than quietly falsifying this sentence.

The source change is comment-only; the emitted artifact is NOT unchanged

Two different questions, and the first revision of this PR ran the second one wrong.

Retracted: the --removeComments experiment

It transpiled both revisions with tsc --ignoreConfig --removeComments --noResolve --skipLibCheck, got matching sha256s, and concluded the emitted JS was byte-identical. That measured a build this repo does not perform. tsconfig.base.json:22 sets "removeComments": false deliberately, and this package's build script is plain tsc, so comments are emitted into dist/*.js. Passing --removeComments overrode the exact setting that decides the answer — it measured away the thing being measured. What that experiment does still establish is narrow and not what was claimed: the two revisions are identical once comments are stripped, i.e. no executable statement changed.

The real measurement

The package built the way the repo builds it (pnpm --filter @object-ui/react build, dist/ cleared between runs), at both revisions, at the current head 21d9d4726:

packages/react/dist/schema-input.js before (7a28e1e3f) after delta
raw 1,486 B (1.45 KB) 2,377 B (2.32 KB) +891 B
gzipped 850 B (0.83 KB) 1,266 B (1.24 KB) +416 B

Cross-checked against CI's own bot. The bundle-size row is wc -c and gzip -c | wc -c of this very file (performance-budget.yml, "Generate package size report"), so it and this table measure the same artifact. At head 012c57cb8 the bot reported 2.26 KB / 1.22 KB for react (schema-input.js) against 1.45 KB / 0.83 KB on two sibling PRs cut from the same merge-base that do not touch this file — and building that same tree locally reproduced 2.26 KB / 1.22 KB exactly. The current head is slightly larger (2.32 KB / 1.24 KB) because the paragraph gained the quoted spelling above; the before figure is unchanged and re-measured at 1,486 B / 850 B on this round.

(One trap worth naming, since it moved a number: gzip stores the source filename in its header, so gzipping a renamed copy of the same bytes gives a different size — 862 B for dist-before-schema-input.js versus 850 B for schema-input.js. Every gzip figure above is measured at the real dist/ path, which is what CI measures.)

What did not change. All 19 differing lines in the emitted file are JSDoc continuations, and the three executable lines are byte-identical on both sides:

$ diff dist-before/schema-input.js dist-after/schema-input.js \
    | grep -E '^[<>]' | grep -vE '^[<>]  \* '
(no output)

export function toRenderableSchema(node) {
    return typeof node === 'number' || typeof node === 'boolean' ? String(node) : node;
}

Structural check on the source, unaffected by the above and still sound — every added and removed line is a JSDoc continuation:

$ git diff -U0 -- packages/react/src/schema-input.ts \
    | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -vE '^[+-] \* '
(no output)

The size is not being chased. Roughly 0.4 KB gzipped is the deliberate price of not repeating a 5h31m Build Docs outage on main; shrinking the paragraph to win those bytes back would trade the thing of value for the thing of no value.

No test and no ablation is possible here, and none is manufactured. There is no executable behaviour to pin, and mutating a doc comment cannot turn any suite red — a "reverse verification" of this diff would be a ritual that proves nothing. The measurements above stand in its place.

Why a changeset, and why patch

The gate said so first, red before the changeset was written:

Compared the working tree with 7a28e1e3f (merge-base with origin/main): 1 file(s) changed,
1 of them under the src/ of a package the release covers, 0 under a package changesets
ignores, 0 changeset(s) added.
❌  1 source file(s) of 1 released package(s) changed, and this change adds no changeset:
      @object-ui/react
          packages/react/src/schema-input.ts

Patch rather than the empty frontmatter, because the paragraph genuinely ships — in dist/schema-input.d.ts, where it is what an editor shows on hover at each call site, and in dist/schema-input.js as the bytes measured above. The changeset carries the corrected, re-measured numbers, not the retracted claim. Documentation of a published API changed; behaviour did not.

No skip-changeset label, deliberately — that label does not exist in this repo; PR #4621 recorded that it was one of the phantom workflows objectui#3724 deleted. content/docs/releases/** untouched.

Gates

Re-derived from .github/workflows/ against this diff (packages/react/src/** plus .changeset/**), not taken from a list. Exit codes captured before any pipe, and each line below is the gate's own verdict.

Re-run at the current head 21d9d47, working tree clean:

Gate Exit Its own verdict line
pnpm --filter @object-ui/react type-check 0 tsc --noEmit && tsc -p tsconfig.test.json — no diagnostics
pnpm --filter @object-ui/react lint 0 ✖ 347 problems (0 errors, 347 warnings) — all pre-existing; 0 attributable to the touched file (grep -c schema-input.ts on the log = 0)
pnpm exec vitest run packages/react/ --maxWorkers=2 (repo root) 0 Test Files 49 passed (49) · Tests 680 passed (680)
check-control-bytes.mjs 0 ✅ check-control-bytes: OK (scanned 4654 tracked text file(s); skipped 85 binary).
check-changeset-presence.mjs 0 ✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs 0 ✅ No changeset declares a 'major' bump.
check-changeset-fixed.mjs 0 ✅ All workspace packages are in the changeset fixed group.

From head 012c57c, whose tree differs from the current one only in prose inside these same two files — no gate below reads that prose:

Gate Exit Its own verdict line
check-type-check-coverage.mjs 0 ✅ type-check coverage: 45/46 via 'type-check', 0 via their own build, 0 known-broken, 1 not compiled.
check-lint-coverage.mjs 0 ✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-doc-links.mjs 0 Links are valid across 13 scan roots.
check-doc-component-types.mjs 0 ✅ Every documented component type is registered.
check-doc-snippet-types.mjs 0 Semantic phase: 88 of 88 block(s) judged, 0 failed. (needed the workspace built first — the unbuilt run reports a precondition, not a finding)
check-skills-paths.mjs 0 ✅ check-skills-paths: OK (95/96 stated path(s) resolve across 18 guide file(s); 1 baselined).
check-package-self-import.mjs 0 ✅ No package names itself inside its own src/.
check-phantom-dependencies.mjs 0 ✅ Every in-scope import is declared by the package that publishes it.
check-node-esm-load.mjs --specifiers-only 0 Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check-spec-symbol-derivation.mjs 0 ✅ spec symbol derivation: 1289 files scanned against 4912 spec export names
check-action-forward-parity.mjs 0 every action row payload excess-property CHECKED
check-i18n-call-site-keys.mjs 0 Every in-scope call-site key resolves against the en pack (2918 keys)
check-i18n-en-drift.mjs 0 No en value changed in this range.

Build Docs specifically, given this card's history — apps/site is the consumer that went red last time:

$ pnpm --filter @object-ui/site type-check
> fumadocs-mdx && next typegen && tsc --noEmit
✓ Types generated successfully
### exit: 0 ###

The full workspace build the snippet gate needed also passed: Tasks: 43 successful, 43 total.

Declared narrowing

Repo-wide pnpm lint was not run; the package-scoped run above was, and it covers this diff completely. Three pieces of evidence, so this reads as a measurement rather than a gap:

  1. Population from eslint's own config, not from a guesseslint.config.js:28 selects files: ['**/*.{ts,tsx}']. The changeset .md is outside eslint's universe entirely, so packages/react/src/schema-input.ts is the only lintable file in the diff.
  2. Count from --format jsoneslint --format json packages/react/src/schema-input.ts reports files linted: 1, errors 0, warnings 0.
  3. Invariance for untouched files — the config extends tseslint.configs.recommended (not recommendedTypeChecked) and sets no parserOptions.project, so linting is not type-aware and this diff cannot move a verdict on any file it does not contain.

Repo-wide pnpm type-check and the full pnpm test farm were likewise left to CI, which runs both in full on this PR.

Scope

packages/react/src/schema-input.ts (one paragraph) and one changeset. No other package, no apps/site, no content/docs/releases/**, no governed surface (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md). No git stash at any point — the worktree is dedicated to this issue, and every before/after build comparison used a committed revision plus git checkout <rev> -- <path> (or a patch file for an in-flight edit), restored and verified byte-identical by sha256 afterwards.

Draft on purpose: the PM lands it.


Generated by Claude Code

…#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>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3785.7 KB 3867.2 KB
Main entry chunk (gzip) 151.5 KB 350 KB
Entry file index-B-xm0qe1.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 (index.js) 10.04KB 3.72KB
app-shell (runtime-config.js) 8.91KB 2.99KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 29.34KB 7.05KB
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) 6.35KB 2.43KB
auth (index.js) 2.77KB 1.22KB
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.02KB 0.89KB
auth (useIsWorkspaceAdmin.js) 3.04KB 1.45KB
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) 506.99KB 113.73KB
core (index.js) 4.51KB 1.80KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 159.80KB 44.33KB
fields (index.js) 238.85KB 60.13KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.22KB 3.08KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.72KB 18.35KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 128.53KB 32.97KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 242.15KB 60.89KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.07KB 30.43KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.70KB 27.17KB
plugin-map (index.js) 20.06KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.50KB 20.68KB
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) 3.77KB 1.33KB
react (SchemaRenderer.js) 43.66KB 14.77KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.33KB 0.69KB
react (schema-input.js) 2.26KB 1.22KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (index.js) 4.77KB 2.16KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 6.92KB 2.40KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.08KB 1.53KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

…hip as bytes

The changeset on the previous commit said "the emitted JavaScript is byte-identical".
That is FALSE, and the way it became false is worth recording, because it is the same
failure mode this card exists to prevent — a confident claim that does not survive being
checked.

The measurement behind it transpiled both revisions with `tsc --removeComments` and
compared sha256s. That is a correct measurement of a build this repo does not perform:
`tsconfig.base.json:22` sets `"removeComments": false` deliberately, and this package
builds with plain `tsc`, so comments are emitted into `dist/*.js`. The flag measured away
the very thing being measured.

Re-measured by building the package the way the repo builds it, at both revisions, with
`dist/` cleared between runs:

    dist/schema-input.js   1,486 B -> 2,316 B   (1.45 KB -> 2.26 KB)
    gzipped                  850 B -> 1,246 B   (0.83 KB -> 1.22 KB)
                                                +830 B raw, +396 B gzipped

which reproduces CI's bundle-size rows for this PR (2.26 KB / 1.22 KB) and for two
sibling PRs cut from the same merge-base that do not touch this file (1.45 KB / 0.83 KB).

What IS true, and is what the changeset now says: all 18 differing lines in the emitted
file are JSDoc continuations, and the three executable lines are byte-identical, so the
growth is the paragraph and nothing else. The source change remains comment-only — the
`git diff -U0` structural check is unaffected and still holds.

The doc paragraph itself is unchanged: it makes no claim about emitted bytes, and the
size is not being chased. Roughly 0.4 KB gzipped is the deliberate price of not repeating
a five-hour `Build Docs` outage.

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

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3785.7 KB 3867.2 KB
Main entry chunk (gzip) 151.5 KB 350 KB
Entry file index-B-xm0qe1.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 (index.js) 10.04KB 3.72KB
app-shell (runtime-config.js) 8.91KB 2.99KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 29.34KB 7.05KB
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) 6.35KB 2.43KB
auth (index.js) 2.77KB 1.22KB
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.02KB 0.89KB
auth (useIsWorkspaceAdmin.js) 3.04KB 1.45KB
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) 506.99KB 113.73KB
core (index.js) 4.51KB 1.80KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 159.80KB 44.33KB
fields (index.js) 238.85KB 60.13KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.22KB 3.08KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.72KB 18.35KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 128.53KB 32.97KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 242.15KB 60.89KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.07KB 30.43KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.70KB 27.17KB
plugin-map (index.js) 20.06KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.50KB 20.68KB
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) 3.77KB 1.33KB
react (SchemaRenderer.js) 43.66KB 14.77KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.33KB 0.69KB
react (schema-input.js) 2.26KB 1.22KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (index.js) 4.77KB 2.16KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 6.92KB 2.40KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.08KB 1.53KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

…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>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3785.7 KB 3867.2 KB
Main entry chunk (gzip) 151.6 KB 350 KB
Entry file index-Chxftl7O.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 (index.js) 10.04KB 3.72KB
app-shell (runtime-config.js) 8.91KB 2.99KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 29.34KB 7.05KB
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) 6.35KB 2.43KB
auth (index.js) 2.77KB 1.22KB
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.02KB 0.89KB
auth (useIsWorkspaceAdmin.js) 3.04KB 1.45KB
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) 506.99KB 113.73KB
core (index.js) 4.51KB 1.80KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 159.80KB 44.33KB
fields (index.js) 238.85KB 60.13KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.22KB 3.08KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.72KB 18.35KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 128.36KB 32.95KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 242.15KB 60.89KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.07KB 30.43KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.70KB 27.17KB
plugin-map (index.js) 20.06KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.50KB 20.68KB
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) 3.77KB 1.33KB
react (SchemaRenderer.js) 43.66KB 14.77KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.33KB 0.69KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (index.js) 4.77KB 2.16KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 6.92KB 2.40KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.08KB 1.53KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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 Author

PM review — ACCEPT (card #4622)

Gates. 22 named check runs read individually for completed + success at 21d9d4726: 19 success, 3 skippedTest (coverage), the unexpanded Test (coverage shard …/4) matrix placeholder, and dependabot. All four real shards, Type Check, Lint, Build & E2E, Build Docs, Bundle Analysis, Live E2E, both doc type checks, Control Byte Scan, Skill Guide Path Check, Internal Docs Link Check and all three changeset checks green.

This PR took two correction rounds, and both were worth it

Round one — a false claim I caught, and the trap that produced it. The PR body and changeset asserted the emitted JavaScript was byte-identical, measured by transpiling both revisions with --removeComments. CI's own bundle bot said otherwise: react (schema-input.js) read 2.26 KB against a 1.45 KB baseline that two concurrent PRs off the same merge-base reported. The implementer verified the discrepancy rather than taking my word for it, established that the bot's row is literally packages/react/dist/schema-input.js from performance-budget.yml's size step, rebuilt both revisions the way the repo builds them, and reproduced the bot's numbers to the byte.

The root cause is worth recording for the whole repo: tsconfig.base.json:22 sets "removeComments": false deliberately, and this package builds with plain tsc. The --removeComments probe overrode the single setting that decides the question. A second implementer in this same round reached the identical wrong conclusion independently on packages/core, so this is a trap the repo lays, not carelessness — and it means "comment-only" never implies "emitted bytes unchanged" here.

Both artifacts were then corrected with the experiment retracted in place rather than swapped out silently, which is the difference between a fix and a cover-up.

Round two — a phrasing judgement I ruled on. The new paragraph cites #4617's failure as "a TS2322 naming number". Naming the type by its role is self-updating; naming today's spelling as a present-tense fact would have added a fresh way for this very paragraph to go false, which is the defect it exists to fix. Ruled a hybrid: keep the role-based description, and scope the literal spelling as a historical quotation of #4617's logs, which cannot rot however the union evolves while still giving a reader with the log open a one-hop match.

The implementer then did better than the ruling required — it established that the role-based half is machine-enforced: the function is annotated : SchemaRendererProps['schema'] and SchemaRenderer.propsResolution.test.ts holds Equal(ReturnType(toRenderableSchema), SchemaRendererProps['schema']), so an edit breaking that correspondence fails a type assertion instead of quietly falsifying the sentence. It also wrote the reasoning into the PR body under "Why the TS2322 clause is worded in two halves", so the next author inherits the rule rather than just its output.

The measurement, with a control

dist/schema-input.js 1,486 → 2,377 B raw, 850 → 1,266 B gzipped. The before-leg reproduced at exactly 1,486 B / 850 B with a sha256 identical to the previous round, which is what makes the delta attributable to the paragraph rather than build drift. CI's independent table now reports 2.32 KB / 1.24 KB — agreeing with the local figures the changeset quotes.

All 19 differing lines in the emitted file are JSDoc continuations; the three executable lines are byte-identical.

The trade, stated plainly

0.4 KB gzipped of shipped comment, deliberately accepted, to prevent a repeat of a measured 5h31m Build Docs red on main. I told the implementer not to shrink the paragraph to chase bytes, and it did not. The problem was ever only the false claim, never the size.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 17:42
@os-sales
os-sales added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit f90b8fb Aug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-4622-torenderableschema-header branch August 21, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants