Skip to content

fix(types,cli): discriminate AnyComponentSchema on type so a refusal costs one arm, not 106 (#8498) - #8544

Merged
os-sales merged 3 commits into
mainfrom
claude/issue-8498-union-error-fanout
Sep 8, 2026
Merged

fix(types,cli): discriminate AnyComponentSchema on type so a refusal costs one arm, not 106 (#8498)#8544
os-sales merged 3 commits into
mainfrom
claude/issue-8498-union-error-fanout

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #8498

AnyComponentSchema was a flat z.union. On a refusal Zod builds one invalid_union carrying every arm's issue list, and $ZodError's initializer stringifies that whole tree into .message eagerlyzod/v4/core/errors.js:13, in the constructor, not behind a getter. So the cost was paid whether or not anyone read the message, and it compounded per level of nesting until safeParse threw RangeError: Invalid string length out of safeValidateSchema, which index.zod.ts:414 documents as validating "without throwing errors".

Discriminating on type selects ONE arm from the authored literal, so a refusal carries that arm's diagnosis alone.

⚠️ One thing worth more than the byte tables: a green test run is not a green package

The first spelling of the patch round's guard used (issue.path ?? []).at(-1). Vitest transpiled it and the whole suite went green — 163 files, 3,047 tests, no signal. tsc then rejected it outright: error TS2339: Property 'at' does not exist on type 'readonly PropertyKey[]', under @object-ui/cli's lib target. A green test run alone would have shipped it.

That is the second instance of this family on this lane in one day; PR #8501 hit the build-project-versus-test-project version of it hours earlier. One shape underneath both: the test runner and the type checker do not read the same rules, so a suite passing says nothing about whether the package compiles. The repo's own instruction — run the package's type-check script separately, and note that it is spelled with a hyphen — is what caught it here, and it earned its keep twice today.

The repair chosen, and the measurement that chose it

Option A in the card's alphabet (z.discriminatedUnion), not B (error map). ⚠️ Distinct from the 2026-09-02 maintainer ruling's own A/B/C/D, which is about what to print — nothing here contradicts that ruling.

B was measured and does not do the job. Zod's .message is JSON.stringify(issues), and a message-returning error map leaves the errors array the size comes from exactly as it was. Same document, same zod 4.4.3:

union .message top issue's errors those arm subtrees as JSON
plain z.union 14,624 chars 13 arms 7,046 bytes
z.union + error map returning Invalid input 14,624 chars 13 arms 7,046 bytes

⚠️ Corrected after review, because the first version of this section overstated it. "An error map never touches the errors array" is not categorically true: finalizeIssue (zod/v4/core/util.js:557-565) hands the map the raw mutable issue before copying it, so a map that assigns iss.errors = [] does shrink the message — 14,624 to 105, measured. That variant is still not the repair: it leans on undocumented ordering, it throws away the very errors the 2026-09-02 ruling requires objectui validate to print, and it still computes all 13 arm subtrees at every level, so the depth cliff survives it untouched. ⇒ the selection stands; only the premise sentence was wrong. (An earlier draft also quoted 14,638 for the mapped message; that number depends on the map's own string and did not reproduce — with Invalid input it is 14,624.)

Before / after, this branch vs its merge base 3bc187b7f

document flat discriminated
root refusal, type selects an arm 14,624 chars · 39 issue nodes · 13 arm subtrees 164 chars · 1 node · none
root refusal, type matches nothing 14,855 chars · 38 nodes 2,178 chars · 1 node
refused menu item 4 levels deep 19,311 chars · 49 nodes 4,330 chars · 12 nodes

The cliff itself, with card objectui#8344's redirect applied as an uncommitted measurement control

Never committed, never pushed on this branch — both legs restored and verified by blob hash against HEAD. The document is the card's own.

depth merge base + that redirect this branch + that redirect
0 14,624 chars · 41 ms 164 chars · 11 ms
1 741,330 chars · 41 ms 3,421 chars · 2 ms
2 18,956,924 chars · 473 ms 8,106 chars · 1 ms
3 428,269,086 chars · 8,010 ms 14,219 chars · 0 ms
4 THREW RangeError: Invalid string length after 98 s 21,760 chars · 1 ms
green control, depth 6 accepted, 14 ms accepted, 1 ms

The left column reproduces the card's table to the character. Growth becomes additive (~+7,000 chars per level) instead of ~25x per level; at depth 3 the message is 30,120x smaller.

The accept set does not move

Two independent readings:

  • Structural. The 13 arms declare 107 type literals with zero collisions, so the arm a literal selects was already the only arm that could have accepted a document carrying it.
  • Differential. Over the 440 parseable JSON documents under examples/, flat and discriminated agree on every verdict (393 accepted by both, 47 refused by both). Firing control: a deliberately crippled union differs on 273 of the same 440, so the comparison can see a difference when there is one.

packages/types/src/__tests__/zod-mirror-parity.test.ts is untouched and green (31 tests) — a discriminated union over the same arms infers the same output type, so no ledger row moves.

Why three files changed, not one

Zod 4.4.3 refuses a plain z.union as a member of a discriminated union — it computes no propValues, so it declares no literals to dispatch on (Invalid discriminated union option at index "9"). ObjectQLComponentSchema and CRUDComponentSchema were the last two flat sites, so the root could not discriminate until they did.

crud.zod.ts carries one type assertion. ActionSchema's z.ZodType[ActionDeclaration, ActionDeclaration] annotation is a maintainer ruling (objectui#7760) and does not move; that type declares propValues as possibly-undefined, so tsc cannot see through the z.lazy to the type: z.literal('action') the body really declares. The runtime can — measured, Set { 'action' }. The intersection asserts that one fact, leaves the output type intact, and the fact is pinned at runtime so the cast cannot rot into a lie. ⚠️ Worth stating precisely, from the review: it cannot rot silently, but not because construction validates it — zod builds the discriminator map through util.cached, at FIRST PARSE. A rotted member constructs fine and then throws Invalid discriminated union option out of safeValidateSchema on a GREEN document. So the failure mode is a throw, which is worth naming on a card that exists to stop one; the runtime pin and every parse in CI catch it first. (Angle brackets spelled as square ones above: GitHub deletes tag-shaped fragments from a body, reversed brackets and all.)

The packages/cli half

Per the triage routing, it rides this PR. But not for the reason triage gave — its claim that union-arm-diagnostics.ts rests on errors[i] aligning with arm i is contradicted by the file's own :36 ("Selection here never relies on that alignment"), and that line is correct. The real cost is different and smaller: a discriminated root produces no errors array at all, so the reader met an issue shape it had no branch for and silently printed nothing where the ruling requires a note.

The module learns that shape — one guarded predicate plus a note builder shared with the existing branch — and header fact 2's "two shapes, and they are the only two" is restated as three, because it was a totality claim and it went false.

objectui validate output, unchanged in substance:

1. Invalid input
   Path: type
   Code: invalid_union
   No arm accepts type "dropdwn-menu".
   Nearest of the 107 accepted types: dropdown-menu, context-menu, drawer, breadcrumb, command

Two output shapes DO move, and both test files were opened and restated by hand rather than patched:

  • a document whose type selects an arm no longer produces a root invalid_union — the arm's own issues are the top-level ones, already at absolute paths;
  • a type no arm claims is reported at type rather than at (root). The root-path line this file exists for is re-anchored on a document that is not a component object at all, where the path genuinely is the root.

Tests, and why they are not vacuous

The trap this card is most exposed to: AnyComponentSchema does not yet recurse into child slots (objectui#7869 / objectui#8344), so a nested document is simply ACCEPTED on main and a naive "does not throw at depth 4" test would be green for the wrong reason. So the depth case is built on MenuItemSchema, which already refuses at depth on the merge base.

Proven by ablation, not asserted: the three schema conversions were reverted to the merge base in place, the suites re-run, and the tree restored — restore verified by empty git diff HEAD plus blob-hash equality on all three files.

Against the flat union, 5 of the 7 new packages/types cases go red:

expected 0 to be greater than 0                                 (no propValues at all)
expected [] to include 'object-grid'                            (flat sub-union declares nothing)
expected [ [ {…} ], …(12) ] to have a length of +0 but got 13   (the 13 arm subtrees)
expected 14855 to be less than or equal to 4000                 (unknown-type message)
expected 19311 to be less than or equal to 8000                 (refused node 4 deep)

The 2 that stay green are the two firing controls, as they should. 3 of the restated packages/cli cases also go red on the merge base.

Note the assertion in the middle: it counts the per-arm subtrees directly. That is the mechanism, not a symptom — an array of 13, stringified once per level, IS the exponent.

Patch round — the two defects the in-seat contract review found (42344e912)

The review returned FAIL on two one-line defects and held on everything else; the verdict is on the card at comment 5582392999.

① The no-arm note is for type-keyed unions only. noMatchingDiscriminator routed on note / options / empty errors alone and never checked which key the union dispatches on — so a discriminated union keyed on anything else was summarised as if it were keyed on type. There is a reachable one: @objectstack/spec's ViewDataSchema is discriminatedUnion('provider', …) and rides .data on object-grid, object-form, object-view and object-calendar. An authored { type: 'object-grid', objectName: 'x', data: { type: 'rest' } } printed

No arm accepts type "rest" at data.
Nearest of the 4 accepted types: api, object, schema, value

— the wrong key, and ViewDataSchema's provider names offered as component types. The merge base printed nothing for that node, so this was a silent-to-wrong regression, and no gate on the PR could have seen it.

Guarded on both the path's last segment and the discriminator field, deliberately. They establish different facts and neither implies the other: the path ending at type is what makes path.slice(0, -1) the node, and the discriminator being type is what makes the note's wording and authoredTypeAt correct. Zod 4.4.3 fills both from one source (def.discriminator, schemas.js:1189/1191) so they cannot disagree today — written this way, a shape that ever separates them falls through to silent, never to wrong. That is the direction the guard exists for. Output now:

1. Invalid discriminator value. Expected 'object' | 'api' | 'value' | 'schema'
   Path: data → provider
   Code: invalid_union

② The message override was scoped to the whole schema. { error: () => 'Invalid input' } also replaced this union's invalid_type, so a non-object root lost zod's own detail: 42 said only Invalid input instead of Invalid input: expected object, received number. Narrowed to error: (issue) => issue.code === 'invalid_union' ? 'Invalid input' : undefined, which declines to the locale for everything else. The re-anchored 1. Invalid input pin still passes — verified rather than assumed, since that pin is exactly the one that would keep passing either way — and it now carries the type detail with it.

Three guard tests, all three proved non-vacuous by removing both fixes in place and re-running: end-to-end objectui validate on the real object-grid document (expected … not to contain 'No arm accepts type'), a unit case covering all three ways the two conditions can disagree plus the positive control that they still route a type-keyed union, and the non-object-root message pin (expected 'Invalid input' to contain 'expected object, received number'). Exactly those 3 go red, 19 others stay green; restore verified by empty git diff HEAD and blob-hash equality.

Stale prose the discriminated root falsified is corrected in union-arm-diagnostics.ts's header and in one figure in the new test's header (the unknown-type base reading is 14,855, not 14,624 — this body always had it right). ⚠️ validate.ts:112-114 carries the same falsified sentence and is not corrected here: it lies outside the scope this patch round was confined to. Flagged for the next round rather than widened into silently.

⚠️ Console Performance Budget is red on main, and this PR cannot make it green

Measured with a real apps/console build on both trees:

framework gzip vs PER_CHUNK_GZIP_CEILINGS.framework = 71_000
merge base 3bc187b7f, no change of mine 72,245 over by 1,245
this branch 72,248 over by 1,248

This PR's own cost is +3 gzipped bytes (+8 raw); the aggregate eager closure actually drops 28 bytes. The breach predates the card. Independent confirmation: the Bundle Analysis workflow has been failing on main since f76f43628 — three consecutive pushes, 40a7c538a being the last green.

⇒ the "≈1 byte of headroom / 70,999" figure both earlier comments carry is stale, and designing against it was never possible. PER_CHUNK_GZIP_CEILINGS and PER_CHUNK_BASELINE are maintainer-floor and are not touched here; that ceiling decision is already with the maintainer.

Gates

Every exit code captured by redirect before any pipe; the gate's own verdict line is quoted where it prints one.

gate exit
pnpm exec vitest run packages/types/ packages/cli/ 0 — 163 files, 3047 tests (base: 162 / 3035)
pnpm --filter @object-ui/types run type-check · @object-ui/cli 0
pnpm type-check (whole repo) 0 — 81/81 tasks
pnpm lint (whole repo) 0 — 47/47 tasks, 0 errors (warnings pre-existing)
pnpm turbo run build --filter @object-ui/console 0 — 35/35 tasks (also the downstream consumer sweep)
check-control-bytes · check-shell-escape-residue 0
check-changeset-no-major · -fixed · -presence · -overwrite 0
check-vi-mock-specifiers · check-vi-mock-inherit 0
check:spec-symbols · check:unreferenced-sources · check:handler-key-reads 0
check:self-import · check:phantom-deps · check:entry-guard · check:side-effects-array 0
check:esm-specifiers 0
check:node-esm-load --force-build 0 — 37/37 provenance, 34/39 entries loaded
check:readme-exports 0
check:eager-closure 1 — pre-existing on main, see above
check-governed-queue-guard --test 0 — NOT GOVERNED, 8 paths, 0 matched

Two gates first returned a PREREQUISITE-NOT-MET reading rather than a verdict, and are recorded because the distinction matters: check:readme-exports failed only because packages/plugin-ai had never been built in this worktree, and check:node-esm-load REFUSED two entries on provenance — turbo shares one cache across every worktree of a checkout, so it had replayed artifacts built in a sibling agent's tree. Both green once the prerequisite was met, the second exactly as its own output instructed.

Serial order

This PR carries none of PR #8501's redirect. That branch is parked on this card by the seat ruling at issue objectui#8344 comment 5578545366; it rebases after this lands. Checked while measuring the control above: its defineNodeComponentUnion writes into SchemaNodeSchema's own plain z.union option array, not into AnyComponentSchema, so its by-reference mechanism is untouched by this change — both legs of the control built and ran clean.

Draft, and enqueued by nobody here: *.zod.ts contract schemas put this on the contract path limb, so the pre-enqueue in-seat contract review is the PM's step.

⚠️ Line budget

Net added lines: 469 (536 added, 67 deleted) across source, tests and changeset, against a ceiling the PM raised to 470 on the reasoning that a line budget guards against scope creep and not against work a contract review mandated. The first round landed at 390; the patch round cost 71 net (two guarded conditions, three guard tests, the prose corrections), and the comment-only follow-up costs 8 more. ⛔ Nothing was trimmed to fit and the ceiling was never moved by this branch.

Session: session_01CZY49skxUBYyJcdnTcYPrE.


Generated by Claude Code

…sal costs one arm

`AnyComponentSchema` was a flat `z.union`, so a refusal carried every arm's issue
list, and Zod's `$ZodError` initializer stringifies that whole tree into `.message`
eagerly — in the constructor, not behind a getter. Measured on zod 4.4.3, a root
refusal cost 14,624 chars and grew ~25x per level of nesting until `safeParse` threw
`RangeError: Invalid string length` out of `safeValidateSchema`, which is documented
as validating "without throwing errors".

Discriminating on `type` selects one arm from the authored literal: same document,
164 chars. `ObjectQLComponentSchema` and `CRUDComponentSchema` are discriminated too
because zod 4.4.3 refuses a plain `z.union` as a discriminated member.

The accept set does not move: the 13 arms declare 107 `type` literals with zero
collisions, and flat vs discriminated agree on all 440 example documents.

`@object-ui/cli`'s `union-arm-diagnostics` learns the discriminated issue shape, so
`objectui validate` still prints the 2026-09-02 maintainer ruling's output — the
selected arm alone, or a note plus a capped candidate list.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3475.0 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-Bas1ePit.js
Status FAIL

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.

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

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

Size Limits

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

os-sales commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Bundle Analysis is red here and it is ⛔ NOT this PR's — standing down, with the reason

domain:spec @ objectui execution seat. Pinned to head 808b63bbf (matches head.sha). One comment, ⛔ not repeated per event.

Failing check: Bundle Analysis → the per-chunk leg only. Aggregate closure ✅ (3475.0 / 3512.7 KB, 37.7 KB headroom), entry chunk ✅ (143.9 / 350 KB), ceiling sensitivity ✅, ceiling freshness ✅ — that last one confirms nobody has moved the constants.

Why it is not this PR's, from this PR's own dev, with a real apps/console build on both trees:

tree framework gzip vs PER_CHUNK_GZIP_CEILINGS.framework = 71_000
merge base 3bc187b7f, before any change of this PR's 72,245 over by 1,245
this branch 72,248 over by 1,248

this PR contributes +3 gzipped bytes and its aggregate eager closure DROPS 28. The breach is 415× its contribution and predates it. Independently: Bundle Analysis has failed on every main push since f76f43628 (05:44:56Z), four consecutive merges, last green 40a7c538a.

Filed, twice and by two seats that did not see each other: #8541 (domain:devx, 08:22:50Z, first) and #8542 (this seat, minutes later). ⛔ Neither closed — the merge is triage's. ⭐ Read #8541 first; it is the earlier card and it makes the sharper point that this is not a required check, so nothing is mechanically blocked.

No fix exists to port. The four candidate remedies — raise the ceiling, trim #8512's diagnostic strings, re-chunk, or re-rule the per-chunk gate — are all maintainer-floor or another card's, and the attribution build that would choose between them has not been run by anyone yet.

⛔ What was NOT done

  • No re-run. A size verdict is a measurement, not a flake, and a re-run cannot change a red base.
  • PER_CHUNK_GZIP_CEILINGS / PER_CHUNK_BASELINE untouched. Maintainer floor — the gate's own failure text says "do not widen it just to get a green check."
  • Nothing trimmed for bytes. This PR would have to remove ~1,245 bytes from framework to go green — 415× what it adds — which means gutting the repair for a defect it did not cause.
  • No revert of fix(core): execute the declared $ operators in ValueDataSource, refuse the rest #8512 proposed. It closed a silent fail-open where an unrecognised operator matched every row.

⇒ This PR is parked on the base branch, in the same position as #8540 and #8501. It stays draft and ⛔ is not enqueued regardless: the pre-enqueue in-seat contract review at the contract-review tier is running, per SKILL.md:643.


Generated by Claude Code

… a non-object root its own message

Two defects from the in-seat contract review of PR #8544.

1. `noMatchingDiscriminator` routed on `note`/`options`/empty-`errors` alone, so a
   discriminated union keyed on anything else was summarised as if it were keyed on
   `type`. `@objectstack/spec`'s `ViewDataSchema` is `discriminatedUnion('provider',
   ...)` and rides `.data` on `object-grid` and three siblings, so an authored
   `{ type: 'object-grid', data: { ... } }` printed a confident, ruling-shaped
   sentence naming the wrong key and offering provider names as component types.
   The merge base printed nothing there — silent, not wrong — so this was a
   silent-to-wrong regression. Guarded on both the path's last segment and the
   `discriminator` field: they establish different facts, and a shape that ever
   separates them now falls through to silent rather than to wrong.

2. The union's message override was unconditional, so it was scoped to the whole
   schema and also replaced its `invalid_type`: a non-object root lost "expected
   object, received number". Narrowed to `invalid_union`.

Also corrects stale prose the discriminated root falsified, in the reader's header
and in one figure in the new test's header.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3475.0 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-Bas1ePit.js
Status FAIL

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.

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

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

Size Limits

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

…root falsified

Comment-only, authorised as the single follow-up to the contract re-review PASS on
42344e9. No code changes.

`validate.ts` still described `AnyComponentSchema` as a `z.union` whose every
no-arm document yields one top-level issue at `path: []`, and still credited
`explainUnionIssue` with selecting the arm. Both went false when the union began
discriminating on `type`: zod does the selecting, a matched discriminator yields the
arm's own issues as top-level entries at absolute paths, a missed one is judged at
`['type']`, and a root path now means the document is not a component object at all.

Also corrects a line citation in `union-arm-diagnostics.ts`: zod fills `discriminator`
and `path` from the same `def.discriminator` at `schemas.js:1187` and `:1190`, not
`:1189`/`:1191`. The cited fact was right, the line numbers were not.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3475.0 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-Bas1ePit.js
Status FAIL

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.

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

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

Size Limits

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

os-sales commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Enqueued with landing check ② UNSATISFIED — recorded, ⛔ not silently passed

--pair 8544 exits 4 and ⛔ will stay at 4. Stating that plainly rather than letting a green merge imply it was cleared.

Why it cannot be cleared honestly. The three tells are the literal z.discriminatedUnion('type', [ lines. CLOSED_SET_OPENER fires on the constructor keyword regardless of direction, and the only exit-0 route is re-declaring Clause-②: yes — recording a widening that does not exist. ⛔ Refused. Filed as the second and sharper instance on objectstack#16822: instance one had an honest escape (respell the prose), this one has none.

Why the direction is nonetheless established. Structurally a discriminated union tries exactly one arm where the flat union tried thirteen ⇒ its accept set is a subset; a flat→discriminated conversion cannot widen. And the in-seat contract review at CONTRACT_REVIEW_TIER measured it three ways: 107 type literals with 0 collisions (every one a bare z.literal, no .optional/.default/.catch wrapper, with zod's own source read to close that hole); 26 hand-built odd cases; 440 corpus roots plus 2,649 nested object nodes. Zero verdict differences.

Landing state: ① ✅ contract-tier PASS on 42344e912 (5583049318), carried to ae6886dde after this seat verified that delta comment-only. ② ❌ as above. ③ green once the base is, which #8550 fixes.

Merging on the maintainer's instruction to land the finished work. ⛔ The gate defect is not closed by this merge — objectstack#16822 owns it.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review September 8, 2026 11:04
@os-sales
os-sales added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 67749c7 Sep 8, 2026
33 of 34 checks passed
@os-sales
os-sales deleted the claude/issue-8498-union-error-fanout branch September 8, 2026 11:41
os-sales pushed a commit that referenced this pull request Sep 8, 2026
…ated union

`main` landed the `AnyComponentSchema` discrimination (objectui#8498, PR #8544)
on the same line this branch wraps, so the two changes conflicted textually.
Resolved by COMPOSITION, not by choosing: the arm list is now
`defineNodeComponentUnion(z.discriminatedUnion('type', [...]))`, keeping the
discriminated fan-out AND the node option-slot fill. Both docblock halves kept.

The mechanism is unaffected by construction and it was re-measured, not assumed:
the option slot lives in `base.zod.ts`'s own plain `z.union(nodeUnionOptions)`,
which `main` does not touch, and `defineNodeComponentUnion` writes whatever
schema it is handed into slot 0. Build, all three type-check projects and the
148 `packages/types` test files (2805 tests, including the incoming
`any-component-union-fanout` pins and this branch's `fill is LIVE` leg) are green
on the composed tree.

Also records, in `base.zod.ts`, the measured limit of its own claim: the
"no published entry point can reach the pre-fill window" sentence is about module
graphs, and a bundler honouring `"sideEffects": false` can drop the fill outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
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.

finding(types): a REFUSED node 4 levels deep makes safeValidateSchema THROW RangeError, and costs 428 MB of string at 3

1 participant