test(test-support): one enum-options walk for the top-level reader family (#6924) - #7024

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader
Aug 31, 2026
Merged

test(test-support): one enum-options walk for the top-level reader family (#6924)#7024
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6924

17 hand-written (Schema as { options?: readonly string[] }).options casts across
16 test files in 11 workspace packages read .options straight off a node that
IS the enum. The cast is unchecked: an enum that acquires a wrapper, or a Zod build
that moves .options, yields undefined, the derived vocabulary silently becomes
the empty set, and every assertion built on it passes over nothing.

The shape, and why it is not a fifth approach

The triage ruling required reading #5872's current conclusion first and not
inventing a fifth way. #5872 class (1) landed a shared reader,
packages/test-support/src/spec-enum-options.ts. Reading it on disk: it already
walks the wrapper chain, already accepts unwrap() / def.innerType /
_def.innerType, and its loop checks node.optionsbefore unwrapping — so it
answers a bare enum correctly the moment it is handed one.

The one thing it could not do for this family is reach the node: it opens with
resolvePropsShape(schema) and then indexes shape[key], so given a node that is
already the enum there is no shape to resolve and no key to index, and it
returns [].

That is a missing entry point, not a missing reader. So:

export function enumOptions(node: unknown): string[] // the walk
export function shapeEnumOptions(schema, key): string[] // resolvePropsShape + enumOptions(shape[key])

shapeEnumOptions now delegates — its body is resolvePropsShape plus one
call. There is exactly one wrapper walk in the repository, and a new pin
(the two entry points are one walk) fails if that stops being true; the wrapper
fixtures alone would keep passing against a second copy.

The enumeration, re-derived

Instrument: grep -rn 'options?: readonly string\[\]' --include='*.ts' --include='*.tsx'
over the whole tree, not just packages/, on origin/main@501041f97.
16 call-site files / 17 occurrences, plus the reader module itself (17 paths
matched in total).

The card said 15 files under packages/. That count is right for packages/;
the 16th, examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
is outside it and was missed by a packages/-scoped grep. plugin-report
carries two occurrences in one file, which is where the card's "2 sites" for that
package comes from.

Every one of the 16 is a test file. No production module reads the vocabulary
this way. packages/fields' site is FieldEditWidget.test.ts:111, a different
file from FieldEditWidget.tsx.

Non-vacuity: measured, and the card's expectation was wrong

The card says "nothing in this family is known to carry such an assertion — that is
the first thing a round on this card should measure". Measured: 16 of 17
occurrences already carried one
, in three shapes.

sitenon-vacuity beforeshape
examples/schema-catalog/.../component-fixture-declared-keys.test.tsYESreader throws not an enum-bearing field
app-shell/.../color-variant-field.test.tsxYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/block-config.test.tsYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/palette-discussion-alias.test.tsxNOnone named; empty list would red only via toContain
components/__tests__/toaster-position-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
components/notifications/__tests__/notification-surfaces.test.tsxYES.not.toEqual([]) inside the coverage test
fields/FieldEditWidget.test.tsYESit('reads a non-empty enum from the spec')
plugin-charts/__tests__/chart-type-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/DatasetWidget.colorVariant.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/pivot-aggregation-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/widget-dispatch-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-detail/__tests__/inlineEditTypeCoverage.test.tsxYESit('reads a non-empty enum from the spec')
plugin-grid/__tests__/summary-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (chart)YESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (report)YESit('reads a non-empty enum from the spec')
react/hooks/__tests__/animation-notification-spec-parity.test.tsxYESfirst line of assertParity
types/__tests__/spec-derived-unions.test.tsYESoptionsOf throws on empty

The one gap, palette-discussion-alias.test.tsx, gains the named assertion here.
It was not silent before — its toContain probes would red on an empty list —
but they say "the spec dropped this member", which sends the next reader to the
contract instead of to the reader.

This is worth recording because it changes the card's risk framing without
changing its premise: the duplication was real, the silent-emptying was already
guarded almost everywhere.

Verdict preservation: measured, not assumed

A throwaway suite reproduced the exact hand-written reading each site carried on
501041f97 and compared it to enumOptions(node) for the same node —
23 readings, all IDENTICAL, same members, same order, all non-empty (so the
equality cannot pass by both sides being []). Examples:

IDENTICAL n=49 FieldType [text, textarea, email, ... , tags, vector]
IDENTICAL n=34 PageComponentType [page:header, ... , element:text_input]
IDENTICAL n=20 ChartTypeSchema [bar, horizontal-bar, ... , table, pivot]
IDENTICAL n=11 ColumnSummarySchema [none, count, ... , min, max]
IDENTICAL n= 8 WidgetColorVariantSchema [default, blue, ... , warning, danger]
IDENTICAL n= 6 NotificationPositionSchema / ActionType
IDENTICAL n= 5 NotificationTypeSchema / ChartAggregateFunctionSchema / PageTypeSchema
IDENTICAL n= 5 ToastSchema.shape.variant [default, success, warning, error, info]
IDENTICAL n= 4 ReportType [tabular, summary, matrix, joined]
IDENTICAL n= 2 RadioGroupSchema.shape.orientation

The measurement suite is deleted, not shipped — the same disposition #5872 class (1)
took. What ships instead is the permanent pin that the two entry points agree.

Ablation: an empty reader reds every converted site

Forced enumOptions to return [] on top of the landed commit, then ran all 16
converted files. Mutation proved on disk before the run (injected-marker count 1,
worktree blob hash moved off the HEAD blob); restore proved after (hash back to
the HEAD blob, git diff HEAD empty, marker count 0); the script carries a
trap ... EXIT INT TERM restore with an absolute repo path. No build leg is owed:
@object-ui/test-support has no dist, and both its exports map and the root
vitest alias point at src/, so the mutated source IS what every suite loads.

Result: 16 of 16 files fail loudly. 15 fail on assertions (41 failing tests);
component-fixture-declared-keys.test.ts fails at collection, because its reader
throws at module scope. Zero silently-green suites. This is the assertion the
card asked for: the family cannot go quietly permissive.

What changed, per site

Converted, all 16: examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
app-shell color-variant-field / block-config / palette-discussion-alias,
components toaster-position / notification-surfaces, fields FieldEditWidget,
plugin-charts chart-type, plugin-dashboard DatasetWidget.colorVariant /
pivot-aggregation / widget-dispatch, plugin-detail inlineEditTypeCoverage,
plugin-grid summary, plugin-report report-spec-parity (both), react
animation-notification, types spec-derived-unions.

Exclusions: none. Every site the instrument found is converted.

Two sites keep a local wrapper around the shared reader rather than replacing
their behaviour: types/spec-derived-unions.test.ts:optionsOf and
schema-catalog:enumOptionsOf both throw on an unreadable enum. The walk moves
to the shared reader; the throw stays, because the reader deliberately answers []
and the throw is those files' non-vacuity duty.

Six packages gain the "@object-ui/test-support": "workspace:*" devDependency edge
the README's convention requires: plugin-charts, plugin-dashboard, plugin-grid,
plugin-report, react, types. No dependencies and no published type moved;
packages/types' change is confined to its test file and a devDependency.

Checks run, all on 7663a4892

checkcommandresult
affected testspnpm exec vitest run over test-support, types, schema-catalog, plugin-list, plugin-timeline, components, fields, react, plugin-charts, plugin-report, plugin-grid, plugin-dashboard, plugin-detail, app-shell (2 shards)1515 files / 17672 tests passed, 1 skipped, 0 failed
gate-script suitespnpm exec vitest run scripts/Test Files 90 passed (90) / Tests 2572 passed (2572)
type-checkturbo run type-check over the 11 affected packagesTasks: 41 successful, 41 total
lintturbo run lint (whole farm, plain form)Tasks: 47 successful, 47 total, 0 errors
changeset presencenode scripts/check-changeset-presence.mjs15 source file(s) of 10 released package(s) changed, and this change declares 1 changeset(s)
control bytesnode scripts/check-control-bytes.mjsOK (scanned 5818 tracked text file(s))
phantom depspnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.
self importpnpm check:self-importNo package names itself inside its own src/.
readme exportspnpm check:readme-exports (after turbo run build)OK (386 self-imports judged, 386 real, 0 fabricated)
spec symbolspnpm check:spec-symbols1325 files scanned, 0 untriaged collisions
type-check coveragenode scripts/check-type-check-coverage.mjs45/46 via type-check, 41/41 packages compile their tests
lint coveragenode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
vi-mock specifiers / inheritpnpm check:vi-mock-specifiers, check:vi-mock-inheritOK
pre-install import graphnode scripts/check-pre-install-import-graph.mjsOK
side-effects / entry guard / esm specifierspnpm check:side-effects-array, check:entry-guard, check:esm-specifiersOK

packages/types' type-check was confirmed to actually read the edited file:
tsc -p tsconfig.test.json --listFiles lists both spec-derived-unions.test.ts
and test-support/src/spec-enum-options.ts. A "typecheck is clean" that excluded
them would have said nothing.

One transient red is worth recording so nobody re-finds it:
scripts/__tests__/check-sdui-registration-pins.test.ts fails only while a local
turbo run build output is on disk
— it then derives
packages/app-shell/dist/console/connect/ConnectAgentWidget.js where it expects the
src/ path. With packages/*/dist removed it passes 11/11, and this branch touches
zero files under app-shell/src/console/. Pre-existing environment sensitivity, not
this diff.

Out of scope, filed separately

A sibling family turned up that this card's instrument does not select: 9 sites
spelling the cast non-optionally (as { options: readonly string[] }), so a
failed cast spreads undefined and throws rather than going quietly empty. Different
failure class, same hand-copied reader; recorded rather than folded in, and the
follow-up card names them. Not addressed here; #6924 is the quiet-permissive half.


Generated by Claude Code

…mily (#6924)
17 hand-written `(Schema as { options?: readonly string[] }).options` casts
across 16 spec-parity test files in 11 workspace packages read `.options`
straight off a node that IS the enum. The cast is unchecked: an enum that
acquires a wrapper, or a Zod build that moves `.options`, yields `undefined`
and the derived vocabulary silently becomes the empty set.
`@object-ui/test-support` already carried the wrapper walk, behind a signature
this family could not use: `shapeEnumOptions(schema, key)` opens with
`resolvePropsShape` and then indexes `shape[key]`, so a bare enum has no shape
to resolve and no key to index. That is a missing ENTRY POINT, not a missing
reader. So the walk is exported as `enumOptions(node)` and `shapeEnumOptions`
delegates to it -- one walk, two entry points, no second copy.
Verdict preservation measured, not assumed: against the installed pin, all 23
readings return the identical array in the identical order before and after.
`palette-discussion-alias.test.tsx` gains the non-vacuity assertion the reader's
docblock makes every caller owe; the other 15 files already carried one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

os-dev-report

Conflict against main resolved by merge commit6560e0f60 (no rebase, no amend, no force-push). git merge-tree --write-tree origin/main 6560e0f60 now exits 0, against both d3bf4fa6f and the newer 7fc5c3c127; the GitHub API reports mergeable: true.

Two files, both append-collisions with PR #7022, both sides kept.

packages/test-support/src/index.ts — the barrel now carries the union:

export{enumOptions,shapeEnumOptions}from'./spec-enum-options';/* ...#7022's docblock... */export{ZOD_WRAPPER_KEYS}from'./zod-wrapper-keys';

packages/test-support/README.md — this branch's rewritten spec-enum-options.ts entry AND #7022's two zod-wrapper-keys entries, in main's ordering.

One sentence was corrected rather than kept verbatim, and it is worth naming because it is the one place a "keep both sides" resolution would have produced a false statement. This branch's text said the other censused reader classes, "array-element unwrapping, the wrapper-key walk — are NOT confined here yet and are still hand-copied". #7022 made the wrapper-key half false. The merged sentence carries #7022's fact and this branch's surviving half:

Of the other Zod-internals reader classes #5872 censused, the wrapper-key list is now shared as DATA (below, objectui#6923); array-element unwrapping is NOT confined here yet and is still hand-copied.

pnpm-lock.yaml auto-merged with no markers, and pnpm install left it byte-unchanged, so it needed no regeneration and none was hand-written.

Re-run on the merge commit (root-relative vitest only):

checkresult
pnpm exec vitest run packages/test-support/ + all 16 converted sites + the 4 class-(1) consumers + #7022's 3 touched suitesTest Files 27 passed (27) / Tests 399 passed, 1 skipped (400)
pnpm exec vitest run scripts/Test Files 91 passed (91) / Tests 2582 passed (2582) — 91, up from 90, because #7022's zod-wrapper-keys.shared.test.ts is now in the tree and green here
turbo run type-check over test-support, types, core, app-shell, components, fields, react, plugin-charts, plugin-dashboard, plugin-detail, plugin-grid, plugin-reportTasks: 41 successful, 41 total
pnpm exec eslint packages/test-support/exit 0, no findings
node scripts/check-changeset-presence.mjsCompared the working tree with d3bf4fa6f (merge-base with origin/main) — 1 changeset, empty frontmatter
node scripts/check-control-bytes.mjsOK (scanned 5834 tracked text file(s))

Diffs above were computed from the merge-base, never two-dot: git diff $(git merge-base origin/main HEAD) HEAD is 28 files, exactly this branch's own set plus the changeset — no foreign file rode in.

Still draft; not marked ready, no auto-merge, not merged. No scope was added while in there — resolution only.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:14
@os-sam
os-sam enabled auto-merge August 31, 2026 14:14
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-tZ_USG_l.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.27KB8.44KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 78e98bfAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6924-enum-options-reader branch August 31, 2026 14:39
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…the shared reader (#7025)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR #7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. #6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into #6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR #7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- #5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a #5872 class
(2)/(4) shape rather than this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…the shared reader (objectstack-ai#7025) (objectstack-ai#7057)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR objectstack-ai#7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. objectstack-ai#6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into objectstack-ai#6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR objectstack-ai#7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- objectstack-ai#5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a objectstack-ai#5872 class
(2)/(4) shape rather than this one.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(test-support): one enum-options walk for the top-level reader family (#6924) - #7024

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader
Aug 31, 2026
Merged

test(test-support): one enum-options walk for the top-level reader family (#6924)#7024
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6924

17 hand-written (Schema as { options?: readonly string[] }).options casts across
16 test files in 11 workspace packages read .options straight off a node that
IS the enum. The cast is unchecked: an enum that acquires a wrapper, or a Zod build
that moves .options, yields undefined, the derived vocabulary silently becomes
the empty set, and every assertion built on it passes over nothing.

The shape, and why it is not a fifth approach

The triage ruling required reading #5872's current conclusion first and not
inventing a fifth way. #5872 class (1) landed a shared reader,
packages/test-support/src/spec-enum-options.ts. Reading it on disk: it already
walks the wrapper chain, already accepts unwrap() / def.innerType /
_def.innerType, and its loop checks node.optionsbefore unwrapping — so it
answers a bare enum correctly the moment it is handed one.

The one thing it could not do for this family is reach the node: it opens with
resolvePropsShape(schema) and then indexes shape[key], so given a node that is
already the enum there is no shape to resolve and no key to index, and it
returns [].

That is a missing entry point, not a missing reader. So:

export function enumOptions(node: unknown): string[] // the walk
export function shapeEnumOptions(schema, key): string[] // resolvePropsShape + enumOptions(shape[key])

shapeEnumOptions now delegates — its body is resolvePropsShape plus one
call. There is exactly one wrapper walk in the repository, and a new pin
(the two entry points are one walk) fails if that stops being true; the wrapper
fixtures alone would keep passing against a second copy.

The enumeration, re-derived

Instrument: grep -rn 'options?: readonly string\[\]' --include='*.ts' --include='*.tsx'
over the whole tree, not just packages/, on origin/main@501041f97.
16 call-site files / 17 occurrences, plus the reader module itself (17 paths
matched in total).

The card said 15 files under packages/. That count is right for packages/;
the 16th, examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
is outside it and was missed by a packages/-scoped grep. plugin-report
carries two occurrences in one file, which is where the card's "2 sites" for that
package comes from.

Every one of the 16 is a test file. No production module reads the vocabulary
this way. packages/fields' site is FieldEditWidget.test.ts:111, a different
file from FieldEditWidget.tsx.

Non-vacuity: measured, and the card's expectation was wrong

The card says "nothing in this family is known to carry such an assertion — that is
the first thing a round on this card should measure". Measured: 16 of 17
occurrences already carried one
, in three shapes.

sitenon-vacuity beforeshape
examples/schema-catalog/.../component-fixture-declared-keys.test.tsYESreader throws not an enum-bearing field
app-shell/.../color-variant-field.test.tsxYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/block-config.test.tsYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/palette-discussion-alias.test.tsxNOnone named; empty list would red only via toContain
components/__tests__/toaster-position-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
components/notifications/__tests__/notification-surfaces.test.tsxYES.not.toEqual([]) inside the coverage test
fields/FieldEditWidget.test.tsYESit('reads a non-empty enum from the spec')
plugin-charts/__tests__/chart-type-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/DatasetWidget.colorVariant.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/pivot-aggregation-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/widget-dispatch-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-detail/__tests__/inlineEditTypeCoverage.test.tsxYESit('reads a non-empty enum from the spec')
plugin-grid/__tests__/summary-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (chart)YESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (report)YESit('reads a non-empty enum from the spec')
react/hooks/__tests__/animation-notification-spec-parity.test.tsxYESfirst line of assertParity
types/__tests__/spec-derived-unions.test.tsYESoptionsOf throws on empty

The one gap, palette-discussion-alias.test.tsx, gains the named assertion here.
It was not silent before — its toContain probes would red on an empty list —
but they say "the spec dropped this member", which sends the next reader to the
contract instead of to the reader.

This is worth recording because it changes the card's risk framing without
changing its premise: the duplication was real, the silent-emptying was already
guarded almost everywhere.

Verdict preservation: measured, not assumed

A throwaway suite reproduced the exact hand-written reading each site carried on
501041f97 and compared it to enumOptions(node) for the same node —
23 readings, all IDENTICAL, same members, same order, all non-empty (so the
equality cannot pass by both sides being []). Examples:

IDENTICAL n=49 FieldType [text, textarea, email, ... , tags, vector]
IDENTICAL n=34 PageComponentType [page:header, ... , element:text_input]
IDENTICAL n=20 ChartTypeSchema [bar, horizontal-bar, ... , table, pivot]
IDENTICAL n=11 ColumnSummarySchema [none, count, ... , min, max]
IDENTICAL n= 8 WidgetColorVariantSchema [default, blue, ... , warning, danger]
IDENTICAL n= 6 NotificationPositionSchema / ActionType
IDENTICAL n= 5 NotificationTypeSchema / ChartAggregateFunctionSchema / PageTypeSchema
IDENTICAL n= 5 ToastSchema.shape.variant [default, success, warning, error, info]
IDENTICAL n= 4 ReportType [tabular, summary, matrix, joined]
IDENTICAL n= 2 RadioGroupSchema.shape.orientation

The measurement suite is deleted, not shipped — the same disposition #5872 class (1)
took. What ships instead is the permanent pin that the two entry points agree.

Ablation: an empty reader reds every converted site

Forced enumOptions to return [] on top of the landed commit, then ran all 16
converted files. Mutation proved on disk before the run (injected-marker count 1,
worktree blob hash moved off the HEAD blob); restore proved after (hash back to
the HEAD blob, git diff HEAD empty, marker count 0); the script carries a
trap ... EXIT INT TERM restore with an absolute repo path. No build leg is owed:
@object-ui/test-support has no dist, and both its exports map and the root
vitest alias point at src/, so the mutated source IS what every suite loads.

Result: 16 of 16 files fail loudly. 15 fail on assertions (41 failing tests);
component-fixture-declared-keys.test.ts fails at collection, because its reader
throws at module scope. Zero silently-green suites. This is the assertion the
card asked for: the family cannot go quietly permissive.

What changed, per site

Converted, all 16: examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
app-shell color-variant-field / block-config / palette-discussion-alias,
components toaster-position / notification-surfaces, fields FieldEditWidget,
plugin-charts chart-type, plugin-dashboard DatasetWidget.colorVariant /
pivot-aggregation / widget-dispatch, plugin-detail inlineEditTypeCoverage,
plugin-grid summary, plugin-report report-spec-parity (both), react
animation-notification, types spec-derived-unions.

Exclusions: none. Every site the instrument found is converted.

Two sites keep a local wrapper around the shared reader rather than replacing
their behaviour: types/spec-derived-unions.test.ts:optionsOf and
schema-catalog:enumOptionsOf both throw on an unreadable enum. The walk moves
to the shared reader; the throw stays, because the reader deliberately answers []
and the throw is those files' non-vacuity duty.

Six packages gain the "@object-ui/test-support": "workspace:*" devDependency edge
the README's convention requires: plugin-charts, plugin-dashboard, plugin-grid,
plugin-report, react, types. No dependencies and no published type moved;
packages/types' change is confined to its test file and a devDependency.

Checks run, all on 7663a4892

checkcommandresult
affected testspnpm exec vitest run over test-support, types, schema-catalog, plugin-list, plugin-timeline, components, fields, react, plugin-charts, plugin-report, plugin-grid, plugin-dashboard, plugin-detail, app-shell (2 shards)1515 files / 17672 tests passed, 1 skipped, 0 failed
gate-script suitespnpm exec vitest run scripts/Test Files 90 passed (90) / Tests 2572 passed (2572)
type-checkturbo run type-check over the 11 affected packagesTasks: 41 successful, 41 total
lintturbo run lint (whole farm, plain form)Tasks: 47 successful, 47 total, 0 errors
changeset presencenode scripts/check-changeset-presence.mjs15 source file(s) of 10 released package(s) changed, and this change declares 1 changeset(s)
control bytesnode scripts/check-control-bytes.mjsOK (scanned 5818 tracked text file(s))
phantom depspnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.
self importpnpm check:self-importNo package names itself inside its own src/.
readme exportspnpm check:readme-exports (after turbo run build)OK (386 self-imports judged, 386 real, 0 fabricated)
spec symbolspnpm check:spec-symbols1325 files scanned, 0 untriaged collisions
type-check coveragenode scripts/check-type-check-coverage.mjs45/46 via type-check, 41/41 packages compile their tests
lint coveragenode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
vi-mock specifiers / inheritpnpm check:vi-mock-specifiers, check:vi-mock-inheritOK
pre-install import graphnode scripts/check-pre-install-import-graph.mjsOK
side-effects / entry guard / esm specifierspnpm check:side-effects-array, check:entry-guard, check:esm-specifiersOK

packages/types' type-check was confirmed to actually read the edited file:
tsc -p tsconfig.test.json --listFiles lists both spec-derived-unions.test.ts
and test-support/src/spec-enum-options.ts. A "typecheck is clean" that excluded
them would have said nothing.

One transient red is worth recording so nobody re-finds it:
scripts/__tests__/check-sdui-registration-pins.test.ts fails only while a local
turbo run build output is on disk
— it then derives
packages/app-shell/dist/console/connect/ConnectAgentWidget.js where it expects the
src/ path. With packages/*/dist removed it passes 11/11, and this branch touches
zero files under app-shell/src/console/. Pre-existing environment sensitivity, not
this diff.

Out of scope, filed separately

A sibling family turned up that this card's instrument does not select: 9 sites
spelling the cast non-optionally (as { options: readonly string[] }), so a
failed cast spreads undefined and throws rather than going quietly empty. Different
failure class, same hand-copied reader; recorded rather than folded in, and the
follow-up card names them. Not addressed here; #6924 is the quiet-permissive half.


Generated by Claude Code

…mily (#6924)
17 hand-written `(Schema as { options?: readonly string[] }).options` casts
across 16 spec-parity test files in 11 workspace packages read `.options`
straight off a node that IS the enum. The cast is unchecked: an enum that
acquires a wrapper, or a Zod build that moves `.options`, yields `undefined`
and the derived vocabulary silently becomes the empty set.
`@object-ui/test-support` already carried the wrapper walk, behind a signature
this family could not use: `shapeEnumOptions(schema, key)` opens with
`resolvePropsShape` and then indexes `shape[key]`, so a bare enum has no shape
to resolve and no key to index. That is a missing ENTRY POINT, not a missing
reader. So the walk is exported as `enumOptions(node)` and `shapeEnumOptions`
delegates to it -- one walk, two entry points, no second copy.
Verdict preservation measured, not assumed: against the installed pin, all 23
readings return the identical array in the identical order before and after.
`palette-discussion-alias.test.tsx` gains the non-vacuity assertion the reader's
docblock makes every caller owe; the other 15 files already carried one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

os-dev-report

Conflict against main resolved by merge commit6560e0f60 (no rebase, no amend, no force-push). git merge-tree --write-tree origin/main 6560e0f60 now exits 0, against both d3bf4fa6f and the newer 7fc5c3c127; the GitHub API reports mergeable: true.

Two files, both append-collisions with PR #7022, both sides kept.

packages/test-support/src/index.ts — the barrel now carries the union:

export{enumOptions,shapeEnumOptions}from'./spec-enum-options';/* ...#7022's docblock... */export{ZOD_WRAPPER_KEYS}from'./zod-wrapper-keys';

packages/test-support/README.md — this branch's rewritten spec-enum-options.ts entry AND #7022's two zod-wrapper-keys entries, in main's ordering.

One sentence was corrected rather than kept verbatim, and it is worth naming because it is the one place a "keep both sides" resolution would have produced a false statement. This branch's text said the other censused reader classes, "array-element unwrapping, the wrapper-key walk — are NOT confined here yet and are still hand-copied". #7022 made the wrapper-key half false. The merged sentence carries #7022's fact and this branch's surviving half:

Of the other Zod-internals reader classes #5872 censused, the wrapper-key list is now shared as DATA (below, objectui#6923); array-element unwrapping is NOT confined here yet and is still hand-copied.

pnpm-lock.yaml auto-merged with no markers, and pnpm install left it byte-unchanged, so it needed no regeneration and none was hand-written.

Re-run on the merge commit (root-relative vitest only):

checkresult
pnpm exec vitest run packages/test-support/ + all 16 converted sites + the 4 class-(1) consumers + #7022's 3 touched suitesTest Files 27 passed (27) / Tests 399 passed, 1 skipped (400)
pnpm exec vitest run scripts/Test Files 91 passed (91) / Tests 2582 passed (2582) — 91, up from 90, because #7022's zod-wrapper-keys.shared.test.ts is now in the tree and green here
turbo run type-check over test-support, types, core, app-shell, components, fields, react, plugin-charts, plugin-dashboard, plugin-detail, plugin-grid, plugin-reportTasks: 41 successful, 41 total
pnpm exec eslint packages/test-support/exit 0, no findings
node scripts/check-changeset-presence.mjsCompared the working tree with d3bf4fa6f (merge-base with origin/main) — 1 changeset, empty frontmatter
node scripts/check-control-bytes.mjsOK (scanned 5834 tracked text file(s))

Diffs above were computed from the merge-base, never two-dot: git diff $(git merge-base origin/main HEAD) HEAD is 28 files, exactly this branch's own set plus the changeset — no foreign file rode in.

Still draft; not marked ready, no auto-merge, not merged. No scope was added while in there — resolution only.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:14
@os-sam
os-sam enabled auto-merge August 31, 2026 14:14
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-tZ_USG_l.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.27KB8.44KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 78e98bfAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6924-enum-options-reader branch August 31, 2026 14:39
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…the shared reader (#7025)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR #7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. #6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into #6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR #7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- #5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a #5872 class
(2)/(4) shape rather than this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…the shared reader (objectstack-ai#7025) (objectstack-ai#7057)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR objectstack-ai#7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. objectstack-ai#6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into objectstack-ai#6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR objectstack-ai#7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- objectstack-ai#5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a objectstack-ai#5872 class
(2)/(4) shape rather than this one.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(test-support): one enum-options walk for the top-level reader family (#6924) - #7024

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader
Aug 31, 2026
Merged

test(test-support): one enum-options walk for the top-level reader family (#6924)#7024
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6924

17 hand-written (Schema as { options?: readonly string[] }).options casts across
16 test files in 11 workspace packages read .options straight off a node that
IS the enum. The cast is unchecked: an enum that acquires a wrapper, or a Zod build
that moves .options, yields undefined, the derived vocabulary silently becomes
the empty set, and every assertion built on it passes over nothing.

The shape, and why it is not a fifth approach

The triage ruling required reading #5872's current conclusion first and not
inventing a fifth way. #5872 class (1) landed a shared reader,
packages/test-support/src/spec-enum-options.ts. Reading it on disk: it already
walks the wrapper chain, already accepts unwrap() / def.innerType /
_def.innerType, and its loop checks node.optionsbefore unwrapping — so it
answers a bare enum correctly the moment it is handed one.

The one thing it could not do for this family is reach the node: it opens with
resolvePropsShape(schema) and then indexes shape[key], so given a node that is
already the enum there is no shape to resolve and no key to index, and it
returns [].

That is a missing entry point, not a missing reader. So:

export function enumOptions(node: unknown): string[] // the walk
export function shapeEnumOptions(schema, key): string[] // resolvePropsShape + enumOptions(shape[key])

shapeEnumOptions now delegates — its body is resolvePropsShape plus one
call. There is exactly one wrapper walk in the repository, and a new pin
(the two entry points are one walk) fails if that stops being true; the wrapper
fixtures alone would keep passing against a second copy.

The enumeration, re-derived

Instrument: grep -rn 'options?: readonly string\[\]' --include='*.ts' --include='*.tsx'
over the whole tree, not just packages/, on origin/main@501041f97.
16 call-site files / 17 occurrences, plus the reader module itself (17 paths
matched in total).

The card said 15 files under packages/. That count is right for packages/;
the 16th, examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
is outside it and was missed by a packages/-scoped grep. plugin-report
carries two occurrences in one file, which is where the card's "2 sites" for that
package comes from.

Every one of the 16 is a test file. No production module reads the vocabulary
this way. packages/fields' site is FieldEditWidget.test.ts:111, a different
file from FieldEditWidget.tsx.

Non-vacuity: measured, and the card's expectation was wrong

The card says "nothing in this family is known to carry such an assertion — that is
the first thing a round on this card should measure". Measured: 16 of 17
occurrences already carried one
, in three shapes.

sitenon-vacuity beforeshape
examples/schema-catalog/.../component-fixture-declared-keys.test.tsYESreader throws not an enum-bearing field
app-shell/.../color-variant-field.test.tsxYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/block-config.test.tsYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/palette-discussion-alias.test.tsxNOnone named; empty list would red only via toContain
components/__tests__/toaster-position-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
components/notifications/__tests__/notification-surfaces.test.tsxYES.not.toEqual([]) inside the coverage test
fields/FieldEditWidget.test.tsYESit('reads a non-empty enum from the spec')
plugin-charts/__tests__/chart-type-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/DatasetWidget.colorVariant.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/pivot-aggregation-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/widget-dispatch-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-detail/__tests__/inlineEditTypeCoverage.test.tsxYESit('reads a non-empty enum from the spec')
plugin-grid/__tests__/summary-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (chart)YESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (report)YESit('reads a non-empty enum from the spec')
react/hooks/__tests__/animation-notification-spec-parity.test.tsxYESfirst line of assertParity
types/__tests__/spec-derived-unions.test.tsYESoptionsOf throws on empty

The one gap, palette-discussion-alias.test.tsx, gains the named assertion here.
It was not silent before — its toContain probes would red on an empty list —
but they say "the spec dropped this member", which sends the next reader to the
contract instead of to the reader.

This is worth recording because it changes the card's risk framing without
changing its premise: the duplication was real, the silent-emptying was already
guarded almost everywhere.

Verdict preservation: measured, not assumed

A throwaway suite reproduced the exact hand-written reading each site carried on
501041f97 and compared it to enumOptions(node) for the same node —
23 readings, all IDENTICAL, same members, same order, all non-empty (so the
equality cannot pass by both sides being []). Examples:

IDENTICAL n=49 FieldType [text, textarea, email, ... , tags, vector]
IDENTICAL n=34 PageComponentType [page:header, ... , element:text_input]
IDENTICAL n=20 ChartTypeSchema [bar, horizontal-bar, ... , table, pivot]
IDENTICAL n=11 ColumnSummarySchema [none, count, ... , min, max]
IDENTICAL n= 8 WidgetColorVariantSchema [default, blue, ... , warning, danger]
IDENTICAL n= 6 NotificationPositionSchema / ActionType
IDENTICAL n= 5 NotificationTypeSchema / ChartAggregateFunctionSchema / PageTypeSchema
IDENTICAL n= 5 ToastSchema.shape.variant [default, success, warning, error, info]
IDENTICAL n= 4 ReportType [tabular, summary, matrix, joined]
IDENTICAL n= 2 RadioGroupSchema.shape.orientation

The measurement suite is deleted, not shipped — the same disposition #5872 class (1)
took. What ships instead is the permanent pin that the two entry points agree.

Ablation: an empty reader reds every converted site

Forced enumOptions to return [] on top of the landed commit, then ran all 16
converted files. Mutation proved on disk before the run (injected-marker count 1,
worktree blob hash moved off the HEAD blob); restore proved after (hash back to
the HEAD blob, git diff HEAD empty, marker count 0); the script carries a
trap ... EXIT INT TERM restore with an absolute repo path. No build leg is owed:
@object-ui/test-support has no dist, and both its exports map and the root
vitest alias point at src/, so the mutated source IS what every suite loads.

Result: 16 of 16 files fail loudly. 15 fail on assertions (41 failing tests);
component-fixture-declared-keys.test.ts fails at collection, because its reader
throws at module scope. Zero silently-green suites. This is the assertion the
card asked for: the family cannot go quietly permissive.

What changed, per site

Converted, all 16: examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
app-shell color-variant-field / block-config / palette-discussion-alias,
components toaster-position / notification-surfaces, fields FieldEditWidget,
plugin-charts chart-type, plugin-dashboard DatasetWidget.colorVariant /
pivot-aggregation / widget-dispatch, plugin-detail inlineEditTypeCoverage,
plugin-grid summary, plugin-report report-spec-parity (both), react
animation-notification, types spec-derived-unions.

Exclusions: none. Every site the instrument found is converted.

Two sites keep a local wrapper around the shared reader rather than replacing
their behaviour: types/spec-derived-unions.test.ts:optionsOf and
schema-catalog:enumOptionsOf both throw on an unreadable enum. The walk moves
to the shared reader; the throw stays, because the reader deliberately answers []
and the throw is those files' non-vacuity duty.

Six packages gain the "@object-ui/test-support": "workspace:*" devDependency edge
the README's convention requires: plugin-charts, plugin-dashboard, plugin-grid,
plugin-report, react, types. No dependencies and no published type moved;
packages/types' change is confined to its test file and a devDependency.

Checks run, all on 7663a4892

checkcommandresult
affected testspnpm exec vitest run over test-support, types, schema-catalog, plugin-list, plugin-timeline, components, fields, react, plugin-charts, plugin-report, plugin-grid, plugin-dashboard, plugin-detail, app-shell (2 shards)1515 files / 17672 tests passed, 1 skipped, 0 failed
gate-script suitespnpm exec vitest run scripts/Test Files 90 passed (90) / Tests 2572 passed (2572)
type-checkturbo run type-check over the 11 affected packagesTasks: 41 successful, 41 total
lintturbo run lint (whole farm, plain form)Tasks: 47 successful, 47 total, 0 errors
changeset presencenode scripts/check-changeset-presence.mjs15 source file(s) of 10 released package(s) changed, and this change declares 1 changeset(s)
control bytesnode scripts/check-control-bytes.mjsOK (scanned 5818 tracked text file(s))
phantom depspnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.
self importpnpm check:self-importNo package names itself inside its own src/.
readme exportspnpm check:readme-exports (after turbo run build)OK (386 self-imports judged, 386 real, 0 fabricated)
spec symbolspnpm check:spec-symbols1325 files scanned, 0 untriaged collisions
type-check coveragenode scripts/check-type-check-coverage.mjs45/46 via type-check, 41/41 packages compile their tests
lint coveragenode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
vi-mock specifiers / inheritpnpm check:vi-mock-specifiers, check:vi-mock-inheritOK
pre-install import graphnode scripts/check-pre-install-import-graph.mjsOK
side-effects / entry guard / esm specifierspnpm check:side-effects-array, check:entry-guard, check:esm-specifiersOK

packages/types' type-check was confirmed to actually read the edited file:
tsc -p tsconfig.test.json --listFiles lists both spec-derived-unions.test.ts
and test-support/src/spec-enum-options.ts. A "typecheck is clean" that excluded
them would have said nothing.

One transient red is worth recording so nobody re-finds it:
scripts/__tests__/check-sdui-registration-pins.test.ts fails only while a local
turbo run build output is on disk
— it then derives
packages/app-shell/dist/console/connect/ConnectAgentWidget.js where it expects the
src/ path. With packages/*/dist removed it passes 11/11, and this branch touches
zero files under app-shell/src/console/. Pre-existing environment sensitivity, not
this diff.

Out of scope, filed separately

A sibling family turned up that this card's instrument does not select: 9 sites
spelling the cast non-optionally (as { options: readonly string[] }), so a
failed cast spreads undefined and throws rather than going quietly empty. Different
failure class, same hand-copied reader; recorded rather than folded in, and the
follow-up card names them. Not addressed here; #6924 is the quiet-permissive half.


Generated by Claude Code

…mily (#6924)
17 hand-written `(Schema as { options?: readonly string[] }).options` casts
across 16 spec-parity test files in 11 workspace packages read `.options`
straight off a node that IS the enum. The cast is unchecked: an enum that
acquires a wrapper, or a Zod build that moves `.options`, yields `undefined`
and the derived vocabulary silently becomes the empty set.
`@object-ui/test-support` already carried the wrapper walk, behind a signature
this family could not use: `shapeEnumOptions(schema, key)` opens with
`resolvePropsShape` and then indexes `shape[key]`, so a bare enum has no shape
to resolve and no key to index. That is a missing ENTRY POINT, not a missing
reader. So the walk is exported as `enumOptions(node)` and `shapeEnumOptions`
delegates to it -- one walk, two entry points, no second copy.
Verdict preservation measured, not assumed: against the installed pin, all 23
readings return the identical array in the identical order before and after.
`palette-discussion-alias.test.tsx` gains the non-vacuity assertion the reader's
docblock makes every caller owe; the other 15 files already carried one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

os-dev-report

Conflict against main resolved by merge commit6560e0f60 (no rebase, no amend, no force-push). git merge-tree --write-tree origin/main 6560e0f60 now exits 0, against both d3bf4fa6f and the newer 7fc5c3c127; the GitHub API reports mergeable: true.

Two files, both append-collisions with PR #7022, both sides kept.

packages/test-support/src/index.ts — the barrel now carries the union:

export{enumOptions,shapeEnumOptions}from'./spec-enum-options';/* ...#7022's docblock... */export{ZOD_WRAPPER_KEYS}from'./zod-wrapper-keys';

packages/test-support/README.md — this branch's rewritten spec-enum-options.ts entry AND #7022's two zod-wrapper-keys entries, in main's ordering.

One sentence was corrected rather than kept verbatim, and it is worth naming because it is the one place a "keep both sides" resolution would have produced a false statement. This branch's text said the other censused reader classes, "array-element unwrapping, the wrapper-key walk — are NOT confined here yet and are still hand-copied". #7022 made the wrapper-key half false. The merged sentence carries #7022's fact and this branch's surviving half:

Of the other Zod-internals reader classes #5872 censused, the wrapper-key list is now shared as DATA (below, objectui#6923); array-element unwrapping is NOT confined here yet and is still hand-copied.

pnpm-lock.yaml auto-merged with no markers, and pnpm install left it byte-unchanged, so it needed no regeneration and none was hand-written.

Re-run on the merge commit (root-relative vitest only):

checkresult
pnpm exec vitest run packages/test-support/ + all 16 converted sites + the 4 class-(1) consumers + #7022's 3 touched suitesTest Files 27 passed (27) / Tests 399 passed, 1 skipped (400)
pnpm exec vitest run scripts/Test Files 91 passed (91) / Tests 2582 passed (2582) — 91, up from 90, because #7022's zod-wrapper-keys.shared.test.ts is now in the tree and green here
turbo run type-check over test-support, types, core, app-shell, components, fields, react, plugin-charts, plugin-dashboard, plugin-detail, plugin-grid, plugin-reportTasks: 41 successful, 41 total
pnpm exec eslint packages/test-support/exit 0, no findings
node scripts/check-changeset-presence.mjsCompared the working tree with d3bf4fa6f (merge-base with origin/main) — 1 changeset, empty frontmatter
node scripts/check-control-bytes.mjsOK (scanned 5834 tracked text file(s))

Diffs above were computed from the merge-base, never two-dot: git diff $(git merge-base origin/main HEAD) HEAD is 28 files, exactly this branch's own set plus the changeset — no foreign file rode in.

Still draft; not marked ready, no auto-merge, not merged. No scope was added while in there — resolution only.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:14
@os-sam
os-sam enabled auto-merge August 31, 2026 14:14
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-tZ_USG_l.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.27KB8.44KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 78e98bfAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6924-enum-options-reader branch August 31, 2026 14:39
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…the shared reader (#7025)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR #7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. #6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into #6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR #7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- #5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a #5872 class
(2)/(4) shape rather than this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…the shared reader (objectstack-ai#7025) (objectstack-ai#7057)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR objectstack-ai#7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. objectstack-ai#6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into objectstack-ai#6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR objectstack-ai#7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- objectstack-ai#5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a objectstack-ai#5872 class
(2)/(4) shape rather than this one.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(test-support): one enum-options walk for the top-level reader family (#6924) - #7024

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader
Aug 31, 2026
Merged

test(test-support): one enum-options walk for the top-level reader family (#6924)#7024
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6924

17 hand-written (Schema as { options?: readonly string[] }).options casts across
16 test files in 11 workspace packages read .options straight off a node that
IS the enum. The cast is unchecked: an enum that acquires a wrapper, or a Zod build
that moves .options, yields undefined, the derived vocabulary silently becomes
the empty set, and every assertion built on it passes over nothing.

The shape, and why it is not a fifth approach

The triage ruling required reading #5872's current conclusion first and not
inventing a fifth way. #5872 class (1) landed a shared reader,
packages/test-support/src/spec-enum-options.ts. Reading it on disk: it already
walks the wrapper chain, already accepts unwrap() / def.innerType /
_def.innerType, and its loop checks node.optionsbefore unwrapping — so it
answers a bare enum correctly the moment it is handed one.

The one thing it could not do for this family is reach the node: it opens with
resolvePropsShape(schema) and then indexes shape[key], so given a node that is
already the enum there is no shape to resolve and no key to index, and it
returns [].

That is a missing entry point, not a missing reader. So:

export function enumOptions(node: unknown): string[] // the walk
export function shapeEnumOptions(schema, key): string[] // resolvePropsShape + enumOptions(shape[key])

shapeEnumOptions now delegates — its body is resolvePropsShape plus one
call. There is exactly one wrapper walk in the repository, and a new pin
(the two entry points are one walk) fails if that stops being true; the wrapper
fixtures alone would keep passing against a second copy.

The enumeration, re-derived

Instrument: grep -rn 'options?: readonly string\[\]' --include='*.ts' --include='*.tsx'
over the whole tree, not just packages/, on origin/main@501041f97.
16 call-site files / 17 occurrences, plus the reader module itself (17 paths
matched in total).

The card said 15 files under packages/. That count is right for packages/;
the 16th, examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
is outside it and was missed by a packages/-scoped grep. plugin-report
carries two occurrences in one file, which is where the card's "2 sites" for that
package comes from.

Every one of the 16 is a test file. No production module reads the vocabulary
this way. packages/fields' site is FieldEditWidget.test.ts:111, a different
file from FieldEditWidget.tsx.

Non-vacuity: measured, and the card's expectation was wrong

The card says "nothing in this family is known to carry such an assertion — that is
the first thing a round on this card should measure". Measured: 16 of 17
occurrences already carried one
, in three shapes.

sitenon-vacuity beforeshape
examples/schema-catalog/.../component-fixture-declared-keys.test.tsYESreader throws not an enum-bearing field
app-shell/.../color-variant-field.test.tsxYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/block-config.test.tsYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/palette-discussion-alias.test.tsxNOnone named; empty list would red only via toContain
components/__tests__/toaster-position-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
components/notifications/__tests__/notification-surfaces.test.tsxYES.not.toEqual([]) inside the coverage test
fields/FieldEditWidget.test.tsYESit('reads a non-empty enum from the spec')
plugin-charts/__tests__/chart-type-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/DatasetWidget.colorVariant.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/pivot-aggregation-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/widget-dispatch-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-detail/__tests__/inlineEditTypeCoverage.test.tsxYESit('reads a non-empty enum from the spec')
plugin-grid/__tests__/summary-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (chart)YESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (report)YESit('reads a non-empty enum from the spec')
react/hooks/__tests__/animation-notification-spec-parity.test.tsxYESfirst line of assertParity
types/__tests__/spec-derived-unions.test.tsYESoptionsOf throws on empty

The one gap, palette-discussion-alias.test.tsx, gains the named assertion here.
It was not silent before — its toContain probes would red on an empty list —
but they say "the spec dropped this member", which sends the next reader to the
contract instead of to the reader.

This is worth recording because it changes the card's risk framing without
changing its premise: the duplication was real, the silent-emptying was already
guarded almost everywhere.

Verdict preservation: measured, not assumed

A throwaway suite reproduced the exact hand-written reading each site carried on
501041f97 and compared it to enumOptions(node) for the same node —
23 readings, all IDENTICAL, same members, same order, all non-empty (so the
equality cannot pass by both sides being []). Examples:

IDENTICAL n=49 FieldType [text, textarea, email, ... , tags, vector]
IDENTICAL n=34 PageComponentType [page:header, ... , element:text_input]
IDENTICAL n=20 ChartTypeSchema [bar, horizontal-bar, ... , table, pivot]
IDENTICAL n=11 ColumnSummarySchema [none, count, ... , min, max]
IDENTICAL n= 8 WidgetColorVariantSchema [default, blue, ... , warning, danger]
IDENTICAL n= 6 NotificationPositionSchema / ActionType
IDENTICAL n= 5 NotificationTypeSchema / ChartAggregateFunctionSchema / PageTypeSchema
IDENTICAL n= 5 ToastSchema.shape.variant [default, success, warning, error, info]
IDENTICAL n= 4 ReportType [tabular, summary, matrix, joined]
IDENTICAL n= 2 RadioGroupSchema.shape.orientation

The measurement suite is deleted, not shipped — the same disposition #5872 class (1)
took. What ships instead is the permanent pin that the two entry points agree.

Ablation: an empty reader reds every converted site

Forced enumOptions to return [] on top of the landed commit, then ran all 16
converted files. Mutation proved on disk before the run (injected-marker count 1,
worktree blob hash moved off the HEAD blob); restore proved after (hash back to
the HEAD blob, git diff HEAD empty, marker count 0); the script carries a
trap ... EXIT INT TERM restore with an absolute repo path. No build leg is owed:
@object-ui/test-support has no dist, and both its exports map and the root
vitest alias point at src/, so the mutated source IS what every suite loads.

Result: 16 of 16 files fail loudly. 15 fail on assertions (41 failing tests);
component-fixture-declared-keys.test.ts fails at collection, because its reader
throws at module scope. Zero silently-green suites. This is the assertion the
card asked for: the family cannot go quietly permissive.

What changed, per site

Converted, all 16: examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
app-shell color-variant-field / block-config / palette-discussion-alias,
components toaster-position / notification-surfaces, fields FieldEditWidget,
plugin-charts chart-type, plugin-dashboard DatasetWidget.colorVariant /
pivot-aggregation / widget-dispatch, plugin-detail inlineEditTypeCoverage,
plugin-grid summary, plugin-report report-spec-parity (both), react
animation-notification, types spec-derived-unions.

Exclusions: none. Every site the instrument found is converted.

Two sites keep a local wrapper around the shared reader rather than replacing
their behaviour: types/spec-derived-unions.test.ts:optionsOf and
schema-catalog:enumOptionsOf both throw on an unreadable enum. The walk moves
to the shared reader; the throw stays, because the reader deliberately answers []
and the throw is those files' non-vacuity duty.

Six packages gain the "@object-ui/test-support": "workspace:*" devDependency edge
the README's convention requires: plugin-charts, plugin-dashboard, plugin-grid,
plugin-report, react, types. No dependencies and no published type moved;
packages/types' change is confined to its test file and a devDependency.

Checks run, all on 7663a4892

checkcommandresult
affected testspnpm exec vitest run over test-support, types, schema-catalog, plugin-list, plugin-timeline, components, fields, react, plugin-charts, plugin-report, plugin-grid, plugin-dashboard, plugin-detail, app-shell (2 shards)1515 files / 17672 tests passed, 1 skipped, 0 failed
gate-script suitespnpm exec vitest run scripts/Test Files 90 passed (90) / Tests 2572 passed (2572)
type-checkturbo run type-check over the 11 affected packagesTasks: 41 successful, 41 total
lintturbo run lint (whole farm, plain form)Tasks: 47 successful, 47 total, 0 errors
changeset presencenode scripts/check-changeset-presence.mjs15 source file(s) of 10 released package(s) changed, and this change declares 1 changeset(s)
control bytesnode scripts/check-control-bytes.mjsOK (scanned 5818 tracked text file(s))
phantom depspnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.
self importpnpm check:self-importNo package names itself inside its own src/.
readme exportspnpm check:readme-exports (after turbo run build)OK (386 self-imports judged, 386 real, 0 fabricated)
spec symbolspnpm check:spec-symbols1325 files scanned, 0 untriaged collisions
type-check coveragenode scripts/check-type-check-coverage.mjs45/46 via type-check, 41/41 packages compile their tests
lint coveragenode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
vi-mock specifiers / inheritpnpm check:vi-mock-specifiers, check:vi-mock-inheritOK
pre-install import graphnode scripts/check-pre-install-import-graph.mjsOK
side-effects / entry guard / esm specifierspnpm check:side-effects-array, check:entry-guard, check:esm-specifiersOK

packages/types' type-check was confirmed to actually read the edited file:
tsc -p tsconfig.test.json --listFiles lists both spec-derived-unions.test.ts
and test-support/src/spec-enum-options.ts. A "typecheck is clean" that excluded
them would have said nothing.

One transient red is worth recording so nobody re-finds it:
scripts/__tests__/check-sdui-registration-pins.test.ts fails only while a local
turbo run build output is on disk
— it then derives
packages/app-shell/dist/console/connect/ConnectAgentWidget.js where it expects the
src/ path. With packages/*/dist removed it passes 11/11, and this branch touches
zero files under app-shell/src/console/. Pre-existing environment sensitivity, not
this diff.

Out of scope, filed separately

A sibling family turned up that this card's instrument does not select: 9 sites
spelling the cast non-optionally (as { options: readonly string[] }), so a
failed cast spreads undefined and throws rather than going quietly empty. Different
failure class, same hand-copied reader; recorded rather than folded in, and the
follow-up card names them. Not addressed here; #6924 is the quiet-permissive half.


Generated by Claude Code

…mily (#6924)
17 hand-written `(Schema as { options?: readonly string[] }).options` casts
across 16 spec-parity test files in 11 workspace packages read `.options`
straight off a node that IS the enum. The cast is unchecked: an enum that
acquires a wrapper, or a Zod build that moves `.options`, yields `undefined`
and the derived vocabulary silently becomes the empty set.
`@object-ui/test-support` already carried the wrapper walk, behind a signature
this family could not use: `shapeEnumOptions(schema, key)` opens with
`resolvePropsShape` and then indexes `shape[key]`, so a bare enum has no shape
to resolve and no key to index. That is a missing ENTRY POINT, not a missing
reader. So the walk is exported as `enumOptions(node)` and `shapeEnumOptions`
delegates to it -- one walk, two entry points, no second copy.
Verdict preservation measured, not assumed: against the installed pin, all 23
readings return the identical array in the identical order before and after.
`palette-discussion-alias.test.tsx` gains the non-vacuity assertion the reader's
docblock makes every caller owe; the other 15 files already carried one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

os-dev-report

Conflict against main resolved by merge commit6560e0f60 (no rebase, no amend, no force-push). git merge-tree --write-tree origin/main 6560e0f60 now exits 0, against both d3bf4fa6f and the newer 7fc5c3c127; the GitHub API reports mergeable: true.

Two files, both append-collisions with PR #7022, both sides kept.

packages/test-support/src/index.ts — the barrel now carries the union:

export{enumOptions,shapeEnumOptions}from'./spec-enum-options';/* ...#7022's docblock... */export{ZOD_WRAPPER_KEYS}from'./zod-wrapper-keys';

packages/test-support/README.md — this branch's rewritten spec-enum-options.ts entry AND #7022's two zod-wrapper-keys entries, in main's ordering.

One sentence was corrected rather than kept verbatim, and it is worth naming because it is the one place a "keep both sides" resolution would have produced a false statement. This branch's text said the other censused reader classes, "array-element unwrapping, the wrapper-key walk — are NOT confined here yet and are still hand-copied". #7022 made the wrapper-key half false. The merged sentence carries #7022's fact and this branch's surviving half:

Of the other Zod-internals reader classes #5872 censused, the wrapper-key list is now shared as DATA (below, objectui#6923); array-element unwrapping is NOT confined here yet and is still hand-copied.

pnpm-lock.yaml auto-merged with no markers, and pnpm install left it byte-unchanged, so it needed no regeneration and none was hand-written.

Re-run on the merge commit (root-relative vitest only):

checkresult
pnpm exec vitest run packages/test-support/ + all 16 converted sites + the 4 class-(1) consumers + #7022's 3 touched suitesTest Files 27 passed (27) / Tests 399 passed, 1 skipped (400)
pnpm exec vitest run scripts/Test Files 91 passed (91) / Tests 2582 passed (2582) — 91, up from 90, because #7022's zod-wrapper-keys.shared.test.ts is now in the tree and green here
turbo run type-check over test-support, types, core, app-shell, components, fields, react, plugin-charts, plugin-dashboard, plugin-detail, plugin-grid, plugin-reportTasks: 41 successful, 41 total
pnpm exec eslint packages/test-support/exit 0, no findings
node scripts/check-changeset-presence.mjsCompared the working tree with d3bf4fa6f (merge-base with origin/main) — 1 changeset, empty frontmatter
node scripts/check-control-bytes.mjsOK (scanned 5834 tracked text file(s))

Diffs above were computed from the merge-base, never two-dot: git diff $(git merge-base origin/main HEAD) HEAD is 28 files, exactly this branch's own set plus the changeset — no foreign file rode in.

Still draft; not marked ready, no auto-merge, not merged. No scope was added while in there — resolution only.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:14
@os-sam
os-sam enabled auto-merge August 31, 2026 14:14
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-tZ_USG_l.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.27KB8.44KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 78e98bfAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6924-enum-options-reader branch August 31, 2026 14:39
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…the shared reader (#7025)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR #7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. #6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into #6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR #7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- #5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a #5872 class
(2)/(4) shape rather than this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…the shared reader (objectstack-ai#7025) (objectstack-ai#7057)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR objectstack-ai#7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. objectstack-ai#6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into objectstack-ai#6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR objectstack-ai#7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- objectstack-ai#5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a objectstack-ai#5872 class
(2)/(4) shape rather than this one.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(test-support): one enum-options walk for the top-level reader family (#6924) - #7024

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader
Aug 31, 2026
Merged

test(test-support): one enum-options walk for the top-level reader family (#6924)#7024
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6924

17 hand-written (Schema as { options?: readonly string[] }).options casts across
16 test files in 11 workspace packages read .options straight off a node that
IS the enum. The cast is unchecked: an enum that acquires a wrapper, or a Zod build
that moves .options, yields undefined, the derived vocabulary silently becomes
the empty set, and every assertion built on it passes over nothing.

The shape, and why it is not a fifth approach

The triage ruling required reading #5872's current conclusion first and not
inventing a fifth way. #5872 class (1) landed a shared reader,
packages/test-support/src/spec-enum-options.ts. Reading it on disk: it already
walks the wrapper chain, already accepts unwrap() / def.innerType /
_def.innerType, and its loop checks node.optionsbefore unwrapping — so it
answers a bare enum correctly the moment it is handed one.

The one thing it could not do for this family is reach the node: it opens with
resolvePropsShape(schema) and then indexes shape[key], so given a node that is
already the enum there is no shape to resolve and no key to index, and it
returns [].

That is a missing entry point, not a missing reader. So:

export function enumOptions(node: unknown): string[] // the walk
export function shapeEnumOptions(schema, key): string[] // resolvePropsShape + enumOptions(shape[key])

shapeEnumOptions now delegates — its body is resolvePropsShape plus one
call. There is exactly one wrapper walk in the repository, and a new pin
(the two entry points are one walk) fails if that stops being true; the wrapper
fixtures alone would keep passing against a second copy.

The enumeration, re-derived

Instrument: grep -rn 'options?: readonly string\[\]' --include='*.ts' --include='*.tsx'
over the whole tree, not just packages/, on origin/main@501041f97.
16 call-site files / 17 occurrences, plus the reader module itself (17 paths
matched in total).

The card said 15 files under packages/. That count is right for packages/;
the 16th, examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
is outside it and was missed by a packages/-scoped grep. plugin-report
carries two occurrences in one file, which is where the card's "2 sites" for that
package comes from.

Every one of the 16 is a test file. No production module reads the vocabulary
this way. packages/fields' site is FieldEditWidget.test.ts:111, a different
file from FieldEditWidget.tsx.

Non-vacuity: measured, and the card's expectation was wrong

The card says "nothing in this family is known to carry such an assertion — that is
the first thing a round on this card should measure". Measured: 16 of 17
occurrences already carried one
, in three shapes.

sitenon-vacuity beforeshape
examples/schema-catalog/.../component-fixture-declared-keys.test.tsYESreader throws not an enum-bearing field
app-shell/.../color-variant-field.test.tsxYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/block-config.test.tsYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/palette-discussion-alias.test.tsxNOnone named; empty list would red only via toContain
components/__tests__/toaster-position-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
components/notifications/__tests__/notification-surfaces.test.tsxYES.not.toEqual([]) inside the coverage test
fields/FieldEditWidget.test.tsYESit('reads a non-empty enum from the spec')
plugin-charts/__tests__/chart-type-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/DatasetWidget.colorVariant.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/pivot-aggregation-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/widget-dispatch-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-detail/__tests__/inlineEditTypeCoverage.test.tsxYESit('reads a non-empty enum from the spec')
plugin-grid/__tests__/summary-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (chart)YESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (report)YESit('reads a non-empty enum from the spec')
react/hooks/__tests__/animation-notification-spec-parity.test.tsxYESfirst line of assertParity
types/__tests__/spec-derived-unions.test.tsYESoptionsOf throws on empty

The one gap, palette-discussion-alias.test.tsx, gains the named assertion here.
It was not silent before — its toContain probes would red on an empty list —
but they say "the spec dropped this member", which sends the next reader to the
contract instead of to the reader.

This is worth recording because it changes the card's risk framing without
changing its premise: the duplication was real, the silent-emptying was already
guarded almost everywhere.

Verdict preservation: measured, not assumed

A throwaway suite reproduced the exact hand-written reading each site carried on
501041f97 and compared it to enumOptions(node) for the same node —
23 readings, all IDENTICAL, same members, same order, all non-empty (so the
equality cannot pass by both sides being []). Examples:

IDENTICAL n=49 FieldType [text, textarea, email, ... , tags, vector]
IDENTICAL n=34 PageComponentType [page:header, ... , element:text_input]
IDENTICAL n=20 ChartTypeSchema [bar, horizontal-bar, ... , table, pivot]
IDENTICAL n=11 ColumnSummarySchema [none, count, ... , min, max]
IDENTICAL n= 8 WidgetColorVariantSchema [default, blue, ... , warning, danger]
IDENTICAL n= 6 NotificationPositionSchema / ActionType
IDENTICAL n= 5 NotificationTypeSchema / ChartAggregateFunctionSchema / PageTypeSchema
IDENTICAL n= 5 ToastSchema.shape.variant [default, success, warning, error, info]
IDENTICAL n= 4 ReportType [tabular, summary, matrix, joined]
IDENTICAL n= 2 RadioGroupSchema.shape.orientation

The measurement suite is deleted, not shipped — the same disposition #5872 class (1)
took. What ships instead is the permanent pin that the two entry points agree.

Ablation: an empty reader reds every converted site

Forced enumOptions to return [] on top of the landed commit, then ran all 16
converted files. Mutation proved on disk before the run (injected-marker count 1,
worktree blob hash moved off the HEAD blob); restore proved after (hash back to
the HEAD blob, git diff HEAD empty, marker count 0); the script carries a
trap ... EXIT INT TERM restore with an absolute repo path. No build leg is owed:
@object-ui/test-support has no dist, and both its exports map and the root
vitest alias point at src/, so the mutated source IS what every suite loads.

Result: 16 of 16 files fail loudly. 15 fail on assertions (41 failing tests);
component-fixture-declared-keys.test.ts fails at collection, because its reader
throws at module scope. Zero silently-green suites. This is the assertion the
card asked for: the family cannot go quietly permissive.

What changed, per site

Converted, all 16: examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
app-shell color-variant-field / block-config / palette-discussion-alias,
components toaster-position / notification-surfaces, fields FieldEditWidget,
plugin-charts chart-type, plugin-dashboard DatasetWidget.colorVariant /
pivot-aggregation / widget-dispatch, plugin-detail inlineEditTypeCoverage,
plugin-grid summary, plugin-report report-spec-parity (both), react
animation-notification, types spec-derived-unions.

Exclusions: none. Every site the instrument found is converted.

Two sites keep a local wrapper around the shared reader rather than replacing
their behaviour: types/spec-derived-unions.test.ts:optionsOf and
schema-catalog:enumOptionsOf both throw on an unreadable enum. The walk moves
to the shared reader; the throw stays, because the reader deliberately answers []
and the throw is those files' non-vacuity duty.

Six packages gain the "@object-ui/test-support": "workspace:*" devDependency edge
the README's convention requires: plugin-charts, plugin-dashboard, plugin-grid,
plugin-report, react, types. No dependencies and no published type moved;
packages/types' change is confined to its test file and a devDependency.

Checks run, all on 7663a4892

checkcommandresult
affected testspnpm exec vitest run over test-support, types, schema-catalog, plugin-list, plugin-timeline, components, fields, react, plugin-charts, plugin-report, plugin-grid, plugin-dashboard, plugin-detail, app-shell (2 shards)1515 files / 17672 tests passed, 1 skipped, 0 failed
gate-script suitespnpm exec vitest run scripts/Test Files 90 passed (90) / Tests 2572 passed (2572)
type-checkturbo run type-check over the 11 affected packagesTasks: 41 successful, 41 total
lintturbo run lint (whole farm, plain form)Tasks: 47 successful, 47 total, 0 errors
changeset presencenode scripts/check-changeset-presence.mjs15 source file(s) of 10 released package(s) changed, and this change declares 1 changeset(s)
control bytesnode scripts/check-control-bytes.mjsOK (scanned 5818 tracked text file(s))
phantom depspnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.
self importpnpm check:self-importNo package names itself inside its own src/.
readme exportspnpm check:readme-exports (after turbo run build)OK (386 self-imports judged, 386 real, 0 fabricated)
spec symbolspnpm check:spec-symbols1325 files scanned, 0 untriaged collisions
type-check coveragenode scripts/check-type-check-coverage.mjs45/46 via type-check, 41/41 packages compile their tests
lint coveragenode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
vi-mock specifiers / inheritpnpm check:vi-mock-specifiers, check:vi-mock-inheritOK
pre-install import graphnode scripts/check-pre-install-import-graph.mjsOK
side-effects / entry guard / esm specifierspnpm check:side-effects-array, check:entry-guard, check:esm-specifiersOK

packages/types' type-check was confirmed to actually read the edited file:
tsc -p tsconfig.test.json --listFiles lists both spec-derived-unions.test.ts
and test-support/src/spec-enum-options.ts. A "typecheck is clean" that excluded
them would have said nothing.

One transient red is worth recording so nobody re-finds it:
scripts/__tests__/check-sdui-registration-pins.test.ts fails only while a local
turbo run build output is on disk
— it then derives
packages/app-shell/dist/console/connect/ConnectAgentWidget.js where it expects the
src/ path. With packages/*/dist removed it passes 11/11, and this branch touches
zero files under app-shell/src/console/. Pre-existing environment sensitivity, not
this diff.

Out of scope, filed separately

A sibling family turned up that this card's instrument does not select: 9 sites
spelling the cast non-optionally (as { options: readonly string[] }), so a
failed cast spreads undefined and throws rather than going quietly empty. Different
failure class, same hand-copied reader; recorded rather than folded in, and the
follow-up card names them. Not addressed here; #6924 is the quiet-permissive half.


Generated by Claude Code

…mily (#6924)
17 hand-written `(Schema as { options?: readonly string[] }).options` casts
across 16 spec-parity test files in 11 workspace packages read `.options`
straight off a node that IS the enum. The cast is unchecked: an enum that
acquires a wrapper, or a Zod build that moves `.options`, yields `undefined`
and the derived vocabulary silently becomes the empty set.
`@object-ui/test-support` already carried the wrapper walk, behind a signature
this family could not use: `shapeEnumOptions(schema, key)` opens with
`resolvePropsShape` and then indexes `shape[key]`, so a bare enum has no shape
to resolve and no key to index. That is a missing ENTRY POINT, not a missing
reader. So the walk is exported as `enumOptions(node)` and `shapeEnumOptions`
delegates to it -- one walk, two entry points, no second copy.
Verdict preservation measured, not assumed: against the installed pin, all 23
readings return the identical array in the identical order before and after.
`palette-discussion-alias.test.tsx` gains the non-vacuity assertion the reader's
docblock makes every caller owe; the other 15 files already carried one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

os-dev-report

Conflict against main resolved by merge commit6560e0f60 (no rebase, no amend, no force-push). git merge-tree --write-tree origin/main 6560e0f60 now exits 0, against both d3bf4fa6f and the newer 7fc5c3c127; the GitHub API reports mergeable: true.

Two files, both append-collisions with PR #7022, both sides kept.

packages/test-support/src/index.ts — the barrel now carries the union:

export{enumOptions,shapeEnumOptions}from'./spec-enum-options';/* ...#7022's docblock... */export{ZOD_WRAPPER_KEYS}from'./zod-wrapper-keys';

packages/test-support/README.md — this branch's rewritten spec-enum-options.ts entry AND #7022's two zod-wrapper-keys entries, in main's ordering.

One sentence was corrected rather than kept verbatim, and it is worth naming because it is the one place a "keep both sides" resolution would have produced a false statement. This branch's text said the other censused reader classes, "array-element unwrapping, the wrapper-key walk — are NOT confined here yet and are still hand-copied". #7022 made the wrapper-key half false. The merged sentence carries #7022's fact and this branch's surviving half:

Of the other Zod-internals reader classes #5872 censused, the wrapper-key list is now shared as DATA (below, objectui#6923); array-element unwrapping is NOT confined here yet and is still hand-copied.

pnpm-lock.yaml auto-merged with no markers, and pnpm install left it byte-unchanged, so it needed no regeneration and none was hand-written.

Re-run on the merge commit (root-relative vitest only):

checkresult
pnpm exec vitest run packages/test-support/ + all 16 converted sites + the 4 class-(1) consumers + #7022's 3 touched suitesTest Files 27 passed (27) / Tests 399 passed, 1 skipped (400)
pnpm exec vitest run scripts/Test Files 91 passed (91) / Tests 2582 passed (2582) — 91, up from 90, because #7022's zod-wrapper-keys.shared.test.ts is now in the tree and green here
turbo run type-check over test-support, types, core, app-shell, components, fields, react, plugin-charts, plugin-dashboard, plugin-detail, plugin-grid, plugin-reportTasks: 41 successful, 41 total
pnpm exec eslint packages/test-support/exit 0, no findings
node scripts/check-changeset-presence.mjsCompared the working tree with d3bf4fa6f (merge-base with origin/main) — 1 changeset, empty frontmatter
node scripts/check-control-bytes.mjsOK (scanned 5834 tracked text file(s))

Diffs above were computed from the merge-base, never two-dot: git diff $(git merge-base origin/main HEAD) HEAD is 28 files, exactly this branch's own set plus the changeset — no foreign file rode in.

Still draft; not marked ready, no auto-merge, not merged. No scope was added while in there — resolution only.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:14
@os-sam
os-sam enabled auto-merge August 31, 2026 14:14
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-tZ_USG_l.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.27KB8.44KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 78e98bfAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6924-enum-options-reader branch August 31, 2026 14:39
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…the shared reader (#7025)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR #7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. #6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into #6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR #7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- #5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a #5872 class
(2)/(4) shape rather than this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…the shared reader (objectstack-ai#7025) (objectstack-ai#7057)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR objectstack-ai#7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. objectstack-ai#6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into objectstack-ai#6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR objectstack-ai#7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- objectstack-ai#5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a objectstack-ai#5872 class
(2)/(4) shape rather than this one.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(test-support): one enum-options walk for the top-level reader family (#6924) - #7024

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader
Aug 31, 2026
Merged

test(test-support): one enum-options walk for the top-level reader family (#6924)#7024
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6924

17 hand-written (Schema as { options?: readonly string[] }).options casts across
16 test files in 11 workspace packages read .options straight off a node that
IS the enum. The cast is unchecked: an enum that acquires a wrapper, or a Zod build
that moves .options, yields undefined, the derived vocabulary silently becomes
the empty set, and every assertion built on it passes over nothing.

The shape, and why it is not a fifth approach

The triage ruling required reading #5872's current conclusion first and not
inventing a fifth way. #5872 class (1) landed a shared reader,
packages/test-support/src/spec-enum-options.ts. Reading it on disk: it already
walks the wrapper chain, already accepts unwrap() / def.innerType /
_def.innerType, and its loop checks node.optionsbefore unwrapping — so it
answers a bare enum correctly the moment it is handed one.

The one thing it could not do for this family is reach the node: it opens with
resolvePropsShape(schema) and then indexes shape[key], so given a node that is
already the enum there is no shape to resolve and no key to index, and it
returns [].

That is a missing entry point, not a missing reader. So:

export function enumOptions(node: unknown): string[] // the walk
export function shapeEnumOptions(schema, key): string[] // resolvePropsShape + enumOptions(shape[key])

shapeEnumOptions now delegates — its body is resolvePropsShape plus one
call. There is exactly one wrapper walk in the repository, and a new pin
(the two entry points are one walk) fails if that stops being true; the wrapper
fixtures alone would keep passing against a second copy.

The enumeration, re-derived

Instrument: grep -rn 'options?: readonly string\[\]' --include='*.ts' --include='*.tsx'
over the whole tree, not just packages/, on origin/main@501041f97.
16 call-site files / 17 occurrences, plus the reader module itself (17 paths
matched in total).

The card said 15 files under packages/. That count is right for packages/;
the 16th, examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
is outside it and was missed by a packages/-scoped grep. plugin-report
carries two occurrences in one file, which is where the card's "2 sites" for that
package comes from.

Every one of the 16 is a test file. No production module reads the vocabulary
this way. packages/fields' site is FieldEditWidget.test.ts:111, a different
file from FieldEditWidget.tsx.

Non-vacuity: measured, and the card's expectation was wrong

The card says "nothing in this family is known to carry such an assertion — that is
the first thing a round on this card should measure". Measured: 16 of 17
occurrences already carried one
, in three shapes.

sitenon-vacuity beforeshape
examples/schema-catalog/.../component-fixture-declared-keys.test.tsYESreader throws not an enum-bearing field
app-shell/.../color-variant-field.test.tsxYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/block-config.test.tsYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/palette-discussion-alias.test.tsxNOnone named; empty list would red only via toContain
components/__tests__/toaster-position-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
components/notifications/__tests__/notification-surfaces.test.tsxYES.not.toEqual([]) inside the coverage test
fields/FieldEditWidget.test.tsYESit('reads a non-empty enum from the spec')
plugin-charts/__tests__/chart-type-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/DatasetWidget.colorVariant.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/pivot-aggregation-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/widget-dispatch-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-detail/__tests__/inlineEditTypeCoverage.test.tsxYESit('reads a non-empty enum from the spec')
plugin-grid/__tests__/summary-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (chart)YESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (report)YESit('reads a non-empty enum from the spec')
react/hooks/__tests__/animation-notification-spec-parity.test.tsxYESfirst line of assertParity
types/__tests__/spec-derived-unions.test.tsYESoptionsOf throws on empty

The one gap, palette-discussion-alias.test.tsx, gains the named assertion here.
It was not silent before — its toContain probes would red on an empty list —
but they say "the spec dropped this member", which sends the next reader to the
contract instead of to the reader.

This is worth recording because it changes the card's risk framing without
changing its premise: the duplication was real, the silent-emptying was already
guarded almost everywhere.

Verdict preservation: measured, not assumed

A throwaway suite reproduced the exact hand-written reading each site carried on
501041f97 and compared it to enumOptions(node) for the same node —
23 readings, all IDENTICAL, same members, same order, all non-empty (so the
equality cannot pass by both sides being []). Examples:

IDENTICAL n=49 FieldType [text, textarea, email, ... , tags, vector]
IDENTICAL n=34 PageComponentType [page:header, ... , element:text_input]
IDENTICAL n=20 ChartTypeSchema [bar, horizontal-bar, ... , table, pivot]
IDENTICAL n=11 ColumnSummarySchema [none, count, ... , min, max]
IDENTICAL n= 8 WidgetColorVariantSchema [default, blue, ... , warning, danger]
IDENTICAL n= 6 NotificationPositionSchema / ActionType
IDENTICAL n= 5 NotificationTypeSchema / ChartAggregateFunctionSchema / PageTypeSchema
IDENTICAL n= 5 ToastSchema.shape.variant [default, success, warning, error, info]
IDENTICAL n= 4 ReportType [tabular, summary, matrix, joined]
IDENTICAL n= 2 RadioGroupSchema.shape.orientation

The measurement suite is deleted, not shipped — the same disposition #5872 class (1)
took. What ships instead is the permanent pin that the two entry points agree.

Ablation: an empty reader reds every converted site

Forced enumOptions to return [] on top of the landed commit, then ran all 16
converted files. Mutation proved on disk before the run (injected-marker count 1,
worktree blob hash moved off the HEAD blob); restore proved after (hash back to
the HEAD blob, git diff HEAD empty, marker count 0); the script carries a
trap ... EXIT INT TERM restore with an absolute repo path. No build leg is owed:
@object-ui/test-support has no dist, and both its exports map and the root
vitest alias point at src/, so the mutated source IS what every suite loads.

Result: 16 of 16 files fail loudly. 15 fail on assertions (41 failing tests);
component-fixture-declared-keys.test.ts fails at collection, because its reader
throws at module scope. Zero silently-green suites. This is the assertion the
card asked for: the family cannot go quietly permissive.

What changed, per site

Converted, all 16: examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
app-shell color-variant-field / block-config / palette-discussion-alias,
components toaster-position / notification-surfaces, fields FieldEditWidget,
plugin-charts chart-type, plugin-dashboard DatasetWidget.colorVariant /
pivot-aggregation / widget-dispatch, plugin-detail inlineEditTypeCoverage,
plugin-grid summary, plugin-report report-spec-parity (both), react
animation-notification, types spec-derived-unions.

Exclusions: none. Every site the instrument found is converted.

Two sites keep a local wrapper around the shared reader rather than replacing
their behaviour: types/spec-derived-unions.test.ts:optionsOf and
schema-catalog:enumOptionsOf both throw on an unreadable enum. The walk moves
to the shared reader; the throw stays, because the reader deliberately answers []
and the throw is those files' non-vacuity duty.

Six packages gain the "@object-ui/test-support": "workspace:*" devDependency edge
the README's convention requires: plugin-charts, plugin-dashboard, plugin-grid,
plugin-report, react, types. No dependencies and no published type moved;
packages/types' change is confined to its test file and a devDependency.

Checks run, all on 7663a4892

checkcommandresult
affected testspnpm exec vitest run over test-support, types, schema-catalog, plugin-list, plugin-timeline, components, fields, react, plugin-charts, plugin-report, plugin-grid, plugin-dashboard, plugin-detail, app-shell (2 shards)1515 files / 17672 tests passed, 1 skipped, 0 failed
gate-script suitespnpm exec vitest run scripts/Test Files 90 passed (90) / Tests 2572 passed (2572)
type-checkturbo run type-check over the 11 affected packagesTasks: 41 successful, 41 total
lintturbo run lint (whole farm, plain form)Tasks: 47 successful, 47 total, 0 errors
changeset presencenode scripts/check-changeset-presence.mjs15 source file(s) of 10 released package(s) changed, and this change declares 1 changeset(s)
control bytesnode scripts/check-control-bytes.mjsOK (scanned 5818 tracked text file(s))
phantom depspnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.
self importpnpm check:self-importNo package names itself inside its own src/.
readme exportspnpm check:readme-exports (after turbo run build)OK (386 self-imports judged, 386 real, 0 fabricated)
spec symbolspnpm check:spec-symbols1325 files scanned, 0 untriaged collisions
type-check coveragenode scripts/check-type-check-coverage.mjs45/46 via type-check, 41/41 packages compile their tests
lint coveragenode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
vi-mock specifiers / inheritpnpm check:vi-mock-specifiers, check:vi-mock-inheritOK
pre-install import graphnode scripts/check-pre-install-import-graph.mjsOK
side-effects / entry guard / esm specifierspnpm check:side-effects-array, check:entry-guard, check:esm-specifiersOK

packages/types' type-check was confirmed to actually read the edited file:
tsc -p tsconfig.test.json --listFiles lists both spec-derived-unions.test.ts
and test-support/src/spec-enum-options.ts. A "typecheck is clean" that excluded
them would have said nothing.

One transient red is worth recording so nobody re-finds it:
scripts/__tests__/check-sdui-registration-pins.test.ts fails only while a local
turbo run build output is on disk
— it then derives
packages/app-shell/dist/console/connect/ConnectAgentWidget.js where it expects the
src/ path. With packages/*/dist removed it passes 11/11, and this branch touches
zero files under app-shell/src/console/. Pre-existing environment sensitivity, not
this diff.

Out of scope, filed separately

A sibling family turned up that this card's instrument does not select: 9 sites
spelling the cast non-optionally (as { options: readonly string[] }), so a
failed cast spreads undefined and throws rather than going quietly empty. Different
failure class, same hand-copied reader; recorded rather than folded in, and the
follow-up card names them. Not addressed here; #6924 is the quiet-permissive half.


Generated by Claude Code

…mily (#6924)
17 hand-written `(Schema as { options?: readonly string[] }).options` casts
across 16 spec-parity test files in 11 workspace packages read `.options`
straight off a node that IS the enum. The cast is unchecked: an enum that
acquires a wrapper, or a Zod build that moves `.options`, yields `undefined`
and the derived vocabulary silently becomes the empty set.
`@object-ui/test-support` already carried the wrapper walk, behind a signature
this family could not use: `shapeEnumOptions(schema, key)` opens with
`resolvePropsShape` and then indexes `shape[key]`, so a bare enum has no shape
to resolve and no key to index. That is a missing ENTRY POINT, not a missing
reader. So the walk is exported as `enumOptions(node)` and `shapeEnumOptions`
delegates to it -- one walk, two entry points, no second copy.
Verdict preservation measured, not assumed: against the installed pin, all 23
readings return the identical array in the identical order before and after.
`palette-discussion-alias.test.tsx` gains the non-vacuity assertion the reader's
docblock makes every caller owe; the other 15 files already carried one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

os-dev-report

Conflict against main resolved by merge commit6560e0f60 (no rebase, no amend, no force-push). git merge-tree --write-tree origin/main 6560e0f60 now exits 0, against both d3bf4fa6f and the newer 7fc5c3c127; the GitHub API reports mergeable: true.

Two files, both append-collisions with PR #7022, both sides kept.

packages/test-support/src/index.ts — the barrel now carries the union:

export{enumOptions,shapeEnumOptions}from'./spec-enum-options';/* ...#7022's docblock... */export{ZOD_WRAPPER_KEYS}from'./zod-wrapper-keys';

packages/test-support/README.md — this branch's rewritten spec-enum-options.ts entry AND #7022's two zod-wrapper-keys entries, in main's ordering.

One sentence was corrected rather than kept verbatim, and it is worth naming because it is the one place a "keep both sides" resolution would have produced a false statement. This branch's text said the other censused reader classes, "array-element unwrapping, the wrapper-key walk — are NOT confined here yet and are still hand-copied". #7022 made the wrapper-key half false. The merged sentence carries #7022's fact and this branch's surviving half:

Of the other Zod-internals reader classes #5872 censused, the wrapper-key list is now shared as DATA (below, objectui#6923); array-element unwrapping is NOT confined here yet and is still hand-copied.

pnpm-lock.yaml auto-merged with no markers, and pnpm install left it byte-unchanged, so it needed no regeneration and none was hand-written.

Re-run on the merge commit (root-relative vitest only):

checkresult
pnpm exec vitest run packages/test-support/ + all 16 converted sites + the 4 class-(1) consumers + #7022's 3 touched suitesTest Files 27 passed (27) / Tests 399 passed, 1 skipped (400)
pnpm exec vitest run scripts/Test Files 91 passed (91) / Tests 2582 passed (2582) — 91, up from 90, because #7022's zod-wrapper-keys.shared.test.ts is now in the tree and green here
turbo run type-check over test-support, types, core, app-shell, components, fields, react, plugin-charts, plugin-dashboard, plugin-detail, plugin-grid, plugin-reportTasks: 41 successful, 41 total
pnpm exec eslint packages/test-support/exit 0, no findings
node scripts/check-changeset-presence.mjsCompared the working tree with d3bf4fa6f (merge-base with origin/main) — 1 changeset, empty frontmatter
node scripts/check-control-bytes.mjsOK (scanned 5834 tracked text file(s))

Diffs above were computed from the merge-base, never two-dot: git diff $(git merge-base origin/main HEAD) HEAD is 28 files, exactly this branch's own set plus the changeset — no foreign file rode in.

Still draft; not marked ready, no auto-merge, not merged. No scope was added while in there — resolution only.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:14
@os-sam
os-sam enabled auto-merge August 31, 2026 14:14
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-tZ_USG_l.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.27KB8.44KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 78e98bfAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6924-enum-options-reader branch August 31, 2026 14:39
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…the shared reader (#7025)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR #7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. #6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into #6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR #7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- #5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a #5872 class
(2)/(4) shape rather than this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…the shared reader (objectstack-ai#7025) (objectstack-ai#7057)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR objectstack-ai#7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. objectstack-ai#6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into objectstack-ai#6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR objectstack-ai#7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- objectstack-ai#5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a objectstack-ai#5872 class
(2)/(4) shape rather than this one.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(test-support): one enum-options walk for the top-level reader family (#6924) - #7024

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader
Aug 31, 2026
Merged

test(test-support): one enum-options walk for the top-level reader family (#6924)#7024
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6924

17 hand-written (Schema as { options?: readonly string[] }).options casts across
16 test files in 11 workspace packages read .options straight off a node that
IS the enum. The cast is unchecked: an enum that acquires a wrapper, or a Zod build
that moves .options, yields undefined, the derived vocabulary silently becomes
the empty set, and every assertion built on it passes over nothing.

The shape, and why it is not a fifth approach

The triage ruling required reading #5872's current conclusion first and not
inventing a fifth way. #5872 class (1) landed a shared reader,
packages/test-support/src/spec-enum-options.ts. Reading it on disk: it already
walks the wrapper chain, already accepts unwrap() / def.innerType /
_def.innerType, and its loop checks node.optionsbefore unwrapping — so it
answers a bare enum correctly the moment it is handed one.

The one thing it could not do for this family is reach the node: it opens with
resolvePropsShape(schema) and then indexes shape[key], so given a node that is
already the enum there is no shape to resolve and no key to index, and it
returns [].

That is a missing entry point, not a missing reader. So:

export function enumOptions(node: unknown): string[] // the walk
export function shapeEnumOptions(schema, key): string[] // resolvePropsShape + enumOptions(shape[key])

shapeEnumOptions now delegates — its body is resolvePropsShape plus one
call. There is exactly one wrapper walk in the repository, and a new pin
(the two entry points are one walk) fails if that stops being true; the wrapper
fixtures alone would keep passing against a second copy.

The enumeration, re-derived

Instrument: grep -rn 'options?: readonly string\[\]' --include='*.ts' --include='*.tsx'
over the whole tree, not just packages/, on origin/main@501041f97.
16 call-site files / 17 occurrences, plus the reader module itself (17 paths
matched in total).

The card said 15 files under packages/. That count is right for packages/;
the 16th, examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
is outside it and was missed by a packages/-scoped grep. plugin-report
carries two occurrences in one file, which is where the card's "2 sites" for that
package comes from.

Every one of the 16 is a test file. No production module reads the vocabulary
this way. packages/fields' site is FieldEditWidget.test.ts:111, a different
file from FieldEditWidget.tsx.

Non-vacuity: measured, and the card's expectation was wrong

The card says "nothing in this family is known to carry such an assertion — that is
the first thing a round on this card should measure". Measured: 16 of 17
occurrences already carried one
, in three shapes.

sitenon-vacuity beforeshape
examples/schema-catalog/.../component-fixture-declared-keys.test.tsYESreader throws not an enum-bearing field
app-shell/.../color-variant-field.test.tsxYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/block-config.test.tsYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/palette-discussion-alias.test.tsxNOnone named; empty list would red only via toContain
components/__tests__/toaster-position-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
components/notifications/__tests__/notification-surfaces.test.tsxYES.not.toEqual([]) inside the coverage test
fields/FieldEditWidget.test.tsYESit('reads a non-empty enum from the spec')
plugin-charts/__tests__/chart-type-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/DatasetWidget.colorVariant.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/pivot-aggregation-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/widget-dispatch-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-detail/__tests__/inlineEditTypeCoverage.test.tsxYESit('reads a non-empty enum from the spec')
plugin-grid/__tests__/summary-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (chart)YESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (report)YESit('reads a non-empty enum from the spec')
react/hooks/__tests__/animation-notification-spec-parity.test.tsxYESfirst line of assertParity
types/__tests__/spec-derived-unions.test.tsYESoptionsOf throws on empty

The one gap, palette-discussion-alias.test.tsx, gains the named assertion here.
It was not silent before — its toContain probes would red on an empty list —
but they say "the spec dropped this member", which sends the next reader to the
contract instead of to the reader.

This is worth recording because it changes the card's risk framing without
changing its premise: the duplication was real, the silent-emptying was already
guarded almost everywhere.

Verdict preservation: measured, not assumed

A throwaway suite reproduced the exact hand-written reading each site carried on
501041f97 and compared it to enumOptions(node) for the same node —
23 readings, all IDENTICAL, same members, same order, all non-empty (so the
equality cannot pass by both sides being []). Examples:

IDENTICAL n=49 FieldType [text, textarea, email, ... , tags, vector]
IDENTICAL n=34 PageComponentType [page:header, ... , element:text_input]
IDENTICAL n=20 ChartTypeSchema [bar, horizontal-bar, ... , table, pivot]
IDENTICAL n=11 ColumnSummarySchema [none, count, ... , min, max]
IDENTICAL n= 8 WidgetColorVariantSchema [default, blue, ... , warning, danger]
IDENTICAL n= 6 NotificationPositionSchema / ActionType
IDENTICAL n= 5 NotificationTypeSchema / ChartAggregateFunctionSchema / PageTypeSchema
IDENTICAL n= 5 ToastSchema.shape.variant [default, success, warning, error, info]
IDENTICAL n= 4 ReportType [tabular, summary, matrix, joined]
IDENTICAL n= 2 RadioGroupSchema.shape.orientation

The measurement suite is deleted, not shipped — the same disposition #5872 class (1)
took. What ships instead is the permanent pin that the two entry points agree.

Ablation: an empty reader reds every converted site

Forced enumOptions to return [] on top of the landed commit, then ran all 16
converted files. Mutation proved on disk before the run (injected-marker count 1,
worktree blob hash moved off the HEAD blob); restore proved after (hash back to
the HEAD blob, git diff HEAD empty, marker count 0); the script carries a
trap ... EXIT INT TERM restore with an absolute repo path. No build leg is owed:
@object-ui/test-support has no dist, and both its exports map and the root
vitest alias point at src/, so the mutated source IS what every suite loads.

Result: 16 of 16 files fail loudly. 15 fail on assertions (41 failing tests);
component-fixture-declared-keys.test.ts fails at collection, because its reader
throws at module scope. Zero silently-green suites. This is the assertion the
card asked for: the family cannot go quietly permissive.

What changed, per site

Converted, all 16: examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
app-shell color-variant-field / block-config / palette-discussion-alias,
components toaster-position / notification-surfaces, fields FieldEditWidget,
plugin-charts chart-type, plugin-dashboard DatasetWidget.colorVariant /
pivot-aggregation / widget-dispatch, plugin-detail inlineEditTypeCoverage,
plugin-grid summary, plugin-report report-spec-parity (both), react
animation-notification, types spec-derived-unions.

Exclusions: none. Every site the instrument found is converted.

Two sites keep a local wrapper around the shared reader rather than replacing
their behaviour: types/spec-derived-unions.test.ts:optionsOf and
schema-catalog:enumOptionsOf both throw on an unreadable enum. The walk moves
to the shared reader; the throw stays, because the reader deliberately answers []
and the throw is those files' non-vacuity duty.

Six packages gain the "@object-ui/test-support": "workspace:*" devDependency edge
the README's convention requires: plugin-charts, plugin-dashboard, plugin-grid,
plugin-report, react, types. No dependencies and no published type moved;
packages/types' change is confined to its test file and a devDependency.

Checks run, all on 7663a4892

checkcommandresult
affected testspnpm exec vitest run over test-support, types, schema-catalog, plugin-list, plugin-timeline, components, fields, react, plugin-charts, plugin-report, plugin-grid, plugin-dashboard, plugin-detail, app-shell (2 shards)1515 files / 17672 tests passed, 1 skipped, 0 failed
gate-script suitespnpm exec vitest run scripts/Test Files 90 passed (90) / Tests 2572 passed (2572)
type-checkturbo run type-check over the 11 affected packagesTasks: 41 successful, 41 total
lintturbo run lint (whole farm, plain form)Tasks: 47 successful, 47 total, 0 errors
changeset presencenode scripts/check-changeset-presence.mjs15 source file(s) of 10 released package(s) changed, and this change declares 1 changeset(s)
control bytesnode scripts/check-control-bytes.mjsOK (scanned 5818 tracked text file(s))
phantom depspnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.
self importpnpm check:self-importNo package names itself inside its own src/.
readme exportspnpm check:readme-exports (after turbo run build)OK (386 self-imports judged, 386 real, 0 fabricated)
spec symbolspnpm check:spec-symbols1325 files scanned, 0 untriaged collisions
type-check coveragenode scripts/check-type-check-coverage.mjs45/46 via type-check, 41/41 packages compile their tests
lint coveragenode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
vi-mock specifiers / inheritpnpm check:vi-mock-specifiers, check:vi-mock-inheritOK
pre-install import graphnode scripts/check-pre-install-import-graph.mjsOK
side-effects / entry guard / esm specifierspnpm check:side-effects-array, check:entry-guard, check:esm-specifiersOK

packages/types' type-check was confirmed to actually read the edited file:
tsc -p tsconfig.test.json --listFiles lists both spec-derived-unions.test.ts
and test-support/src/spec-enum-options.ts. A "typecheck is clean" that excluded
them would have said nothing.

One transient red is worth recording so nobody re-finds it:
scripts/__tests__/check-sdui-registration-pins.test.ts fails only while a local
turbo run build output is on disk
— it then derives
packages/app-shell/dist/console/connect/ConnectAgentWidget.js where it expects the
src/ path. With packages/*/dist removed it passes 11/11, and this branch touches
zero files under app-shell/src/console/. Pre-existing environment sensitivity, not
this diff.

Out of scope, filed separately

A sibling family turned up that this card's instrument does not select: 9 sites
spelling the cast non-optionally (as { options: readonly string[] }), so a
failed cast spreads undefined and throws rather than going quietly empty. Different
failure class, same hand-copied reader; recorded rather than folded in, and the
follow-up card names them. Not addressed here; #6924 is the quiet-permissive half.


Generated by Claude Code

…mily (#6924)
17 hand-written `(Schema as { options?: readonly string[] }).options` casts
across 16 spec-parity test files in 11 workspace packages read `.options`
straight off a node that IS the enum. The cast is unchecked: an enum that
acquires a wrapper, or a Zod build that moves `.options`, yields `undefined`
and the derived vocabulary silently becomes the empty set.
`@object-ui/test-support` already carried the wrapper walk, behind a signature
this family could not use: `shapeEnumOptions(schema, key)` opens with
`resolvePropsShape` and then indexes `shape[key]`, so a bare enum has no shape
to resolve and no key to index. That is a missing ENTRY POINT, not a missing
reader. So the walk is exported as `enumOptions(node)` and `shapeEnumOptions`
delegates to it -- one walk, two entry points, no second copy.
Verdict preservation measured, not assumed: against the installed pin, all 23
readings return the identical array in the identical order before and after.
`palette-discussion-alias.test.tsx` gains the non-vacuity assertion the reader's
docblock makes every caller owe; the other 15 files already carried one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

os-dev-report

Conflict against main resolved by merge commit6560e0f60 (no rebase, no amend, no force-push). git merge-tree --write-tree origin/main 6560e0f60 now exits 0, against both d3bf4fa6f and the newer 7fc5c3c127; the GitHub API reports mergeable: true.

Two files, both append-collisions with PR #7022, both sides kept.

packages/test-support/src/index.ts — the barrel now carries the union:

export{enumOptions,shapeEnumOptions}from'./spec-enum-options';/* ...#7022's docblock... */export{ZOD_WRAPPER_KEYS}from'./zod-wrapper-keys';

packages/test-support/README.md — this branch's rewritten spec-enum-options.ts entry AND #7022's two zod-wrapper-keys entries, in main's ordering.

One sentence was corrected rather than kept verbatim, and it is worth naming because it is the one place a "keep both sides" resolution would have produced a false statement. This branch's text said the other censused reader classes, "array-element unwrapping, the wrapper-key walk — are NOT confined here yet and are still hand-copied". #7022 made the wrapper-key half false. The merged sentence carries #7022's fact and this branch's surviving half:

Of the other Zod-internals reader classes #5872 censused, the wrapper-key list is now shared as DATA (below, objectui#6923); array-element unwrapping is NOT confined here yet and is still hand-copied.

pnpm-lock.yaml auto-merged with no markers, and pnpm install left it byte-unchanged, so it needed no regeneration and none was hand-written.

Re-run on the merge commit (root-relative vitest only):

checkresult
pnpm exec vitest run packages/test-support/ + all 16 converted sites + the 4 class-(1) consumers + #7022's 3 touched suitesTest Files 27 passed (27) / Tests 399 passed, 1 skipped (400)
pnpm exec vitest run scripts/Test Files 91 passed (91) / Tests 2582 passed (2582) — 91, up from 90, because #7022's zod-wrapper-keys.shared.test.ts is now in the tree and green here
turbo run type-check over test-support, types, core, app-shell, components, fields, react, plugin-charts, plugin-dashboard, plugin-detail, plugin-grid, plugin-reportTasks: 41 successful, 41 total
pnpm exec eslint packages/test-support/exit 0, no findings
node scripts/check-changeset-presence.mjsCompared the working tree with d3bf4fa6f (merge-base with origin/main) — 1 changeset, empty frontmatter
node scripts/check-control-bytes.mjsOK (scanned 5834 tracked text file(s))

Diffs above were computed from the merge-base, never two-dot: git diff $(git merge-base origin/main HEAD) HEAD is 28 files, exactly this branch's own set plus the changeset — no foreign file rode in.

Still draft; not marked ready, no auto-merge, not merged. No scope was added while in there — resolution only.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:14
@os-sam
os-sam enabled auto-merge August 31, 2026 14:14
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-tZ_USG_l.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.27KB8.44KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 78e98bfAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6924-enum-options-reader branch August 31, 2026 14:39
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…the shared reader (#7025)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR #7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. #6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into #6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR #7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- #5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a #5872 class
(2)/(4) shape rather than this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…the shared reader (objectstack-ai#7025) (objectstack-ai#7057)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR objectstack-ai#7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. objectstack-ai#6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into objectstack-ai#6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR objectstack-ai#7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- objectstack-ai#5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a objectstack-ai#5872 class
(2)/(4) shape rather than this one.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(test-support): one enum-options walk for the top-level reader family (#6924) - #7024

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader
Aug 31, 2026
Merged

test(test-support): one enum-options walk for the top-level reader family (#6924)#7024
os-sam merged 2 commits into
mainfrom
claude/issue-6924-enum-options-reader

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6924

17 hand-written (Schema as { options?: readonly string[] }).options casts across
16 test files in 11 workspace packages read .options straight off a node that
IS the enum. The cast is unchecked: an enum that acquires a wrapper, or a Zod build
that moves .options, yields undefined, the derived vocabulary silently becomes
the empty set, and every assertion built on it passes over nothing.

The shape, and why it is not a fifth approach

The triage ruling required reading #5872's current conclusion first and not
inventing a fifth way. #5872 class (1) landed a shared reader,
packages/test-support/src/spec-enum-options.ts. Reading it on disk: it already
walks the wrapper chain, already accepts unwrap() / def.innerType /
_def.innerType, and its loop checks node.optionsbefore unwrapping — so it
answers a bare enum correctly the moment it is handed one.

The one thing it could not do for this family is reach the node: it opens with
resolvePropsShape(schema) and then indexes shape[key], so given a node that is
already the enum there is no shape to resolve and no key to index, and it
returns [].

That is a missing entry point, not a missing reader. So:

export function enumOptions(node: unknown): string[] // the walk
export function shapeEnumOptions(schema, key): string[] // resolvePropsShape + enumOptions(shape[key])

shapeEnumOptions now delegates — its body is resolvePropsShape plus one
call. There is exactly one wrapper walk in the repository, and a new pin
(the two entry points are one walk) fails if that stops being true; the wrapper
fixtures alone would keep passing against a second copy.

The enumeration, re-derived

Instrument: grep -rn 'options?: readonly string\[\]' --include='*.ts' --include='*.tsx'
over the whole tree, not just packages/, on origin/main@501041f97.
16 call-site files / 17 occurrences, plus the reader module itself (17 paths
matched in total).

The card said 15 files under packages/. That count is right for packages/;
the 16th, examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
is outside it and was missed by a packages/-scoped grep. plugin-report
carries two occurrences in one file, which is where the card's "2 sites" for that
package comes from.

Every one of the 16 is a test file. No production module reads the vocabulary
this way. packages/fields' site is FieldEditWidget.test.ts:111, a different
file from FieldEditWidget.tsx.

Non-vacuity: measured, and the card's expectation was wrong

The card says "nothing in this family is known to carry such an assertion — that is
the first thing a round on this card should measure". Measured: 16 of 17
occurrences already carried one
, in three shapes.

sitenon-vacuity beforeshape
examples/schema-catalog/.../component-fixture-declared-keys.test.tsYESreader throws not an enum-bearing field
app-shell/.../color-variant-field.test.tsxYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/block-config.test.tsYESit('reads a non-empty enum from the spec')
app-shell/.../previews/__tests__/palette-discussion-alias.test.tsxNOnone named; empty list would red only via toContain
components/__tests__/toaster-position-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
components/notifications/__tests__/notification-surfaces.test.tsxYES.not.toEqual([]) inside the coverage test
fields/FieldEditWidget.test.tsYESit('reads a non-empty enum from the spec')
plugin-charts/__tests__/chart-type-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/DatasetWidget.colorVariant.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/pivot-aggregation-spec-parity.test.tsxYESit('reads a non-empty enum from the spec')
plugin-dashboard/__tests__/widget-dispatch-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-detail/__tests__/inlineEditTypeCoverage.test.tsxYESit('reads a non-empty enum from the spec')
plugin-grid/__tests__/summary-spec-parity.test.tsYESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (chart)YESit('reads a non-empty enum from the spec')
plugin-report/__tests__/report-spec-parity.test.tsx (report)YESit('reads a non-empty enum from the spec')
react/hooks/__tests__/animation-notification-spec-parity.test.tsxYESfirst line of assertParity
types/__tests__/spec-derived-unions.test.tsYESoptionsOf throws on empty

The one gap, palette-discussion-alias.test.tsx, gains the named assertion here.
It was not silent before — its toContain probes would red on an empty list —
but they say "the spec dropped this member", which sends the next reader to the
contract instead of to the reader.

This is worth recording because it changes the card's risk framing without
changing its premise: the duplication was real, the silent-emptying was already
guarded almost everywhere.

Verdict preservation: measured, not assumed

A throwaway suite reproduced the exact hand-written reading each site carried on
501041f97 and compared it to enumOptions(node) for the same node —
23 readings, all IDENTICAL, same members, same order, all non-empty (so the
equality cannot pass by both sides being []). Examples:

IDENTICAL n=49 FieldType [text, textarea, email, ... , tags, vector]
IDENTICAL n=34 PageComponentType [page:header, ... , element:text_input]
IDENTICAL n=20 ChartTypeSchema [bar, horizontal-bar, ... , table, pivot]
IDENTICAL n=11 ColumnSummarySchema [none, count, ... , min, max]
IDENTICAL n= 8 WidgetColorVariantSchema [default, blue, ... , warning, danger]
IDENTICAL n= 6 NotificationPositionSchema / ActionType
IDENTICAL n= 5 NotificationTypeSchema / ChartAggregateFunctionSchema / PageTypeSchema
IDENTICAL n= 5 ToastSchema.shape.variant [default, success, warning, error, info]
IDENTICAL n= 4 ReportType [tabular, summary, matrix, joined]
IDENTICAL n= 2 RadioGroupSchema.shape.orientation

The measurement suite is deleted, not shipped — the same disposition #5872 class (1)
took. What ships instead is the permanent pin that the two entry points agree.

Ablation: an empty reader reds every converted site

Forced enumOptions to return [] on top of the landed commit, then ran all 16
converted files. Mutation proved on disk before the run (injected-marker count 1,
worktree blob hash moved off the HEAD blob); restore proved after (hash back to
the HEAD blob, git diff HEAD empty, marker count 0); the script carries a
trap ... EXIT INT TERM restore with an absolute repo path. No build leg is owed:
@object-ui/test-support has no dist, and both its exports map and the root
vitest alias point at src/, so the mutated source IS what every suite loads.

Result: 16 of 16 files fail loudly. 15 fail on assertions (41 failing tests);
component-fixture-declared-keys.test.ts fails at collection, because its reader
throws at module scope. Zero silently-green suites. This is the assertion the
card asked for: the family cannot go quietly permissive.

What changed, per site

Converted, all 16: examples/schema-catalog/test/component-fixture-declared-keys.test.ts,
app-shell color-variant-field / block-config / palette-discussion-alias,
components toaster-position / notification-surfaces, fields FieldEditWidget,
plugin-charts chart-type, plugin-dashboard DatasetWidget.colorVariant /
pivot-aggregation / widget-dispatch, plugin-detail inlineEditTypeCoverage,
plugin-grid summary, plugin-report report-spec-parity (both), react
animation-notification, types spec-derived-unions.

Exclusions: none. Every site the instrument found is converted.

Two sites keep a local wrapper around the shared reader rather than replacing
their behaviour: types/spec-derived-unions.test.ts:optionsOf and
schema-catalog:enumOptionsOf both throw on an unreadable enum. The walk moves
to the shared reader; the throw stays, because the reader deliberately answers []
and the throw is those files' non-vacuity duty.

Six packages gain the "@object-ui/test-support": "workspace:*" devDependency edge
the README's convention requires: plugin-charts, plugin-dashboard, plugin-grid,
plugin-report, react, types. No dependencies and no published type moved;
packages/types' change is confined to its test file and a devDependency.

Checks run, all on 7663a4892

checkcommandresult
affected testspnpm exec vitest run over test-support, types, schema-catalog, plugin-list, plugin-timeline, components, fields, react, plugin-charts, plugin-report, plugin-grid, plugin-dashboard, plugin-detail, app-shell (2 shards)1515 files / 17672 tests passed, 1 skipped, 0 failed
gate-script suitespnpm exec vitest run scripts/Test Files 90 passed (90) / Tests 2572 passed (2572)
type-checkturbo run type-check over the 11 affected packagesTasks: 41 successful, 41 total
lintturbo run lint (whole farm, plain form)Tasks: 47 successful, 47 total, 0 errors
changeset presencenode scripts/check-changeset-presence.mjs15 source file(s) of 10 released package(s) changed, and this change declares 1 changeset(s)
control bytesnode scripts/check-control-bytes.mjsOK (scanned 5818 tracked text file(s))
phantom depspnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.
self importpnpm check:self-importNo package names itself inside its own src/.
readme exportspnpm check:readme-exports (after turbo run build)OK (386 self-imports judged, 386 real, 0 fabricated)
spec symbolspnpm check:spec-symbols1325 files scanned, 0 untriaged collisions
type-check coveragenode scripts/check-type-check-coverage.mjs45/46 via type-check, 41/41 packages compile their tests
lint coveragenode scripts/check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errors
vi-mock specifiers / inheritpnpm check:vi-mock-specifiers, check:vi-mock-inheritOK
pre-install import graphnode scripts/check-pre-install-import-graph.mjsOK
side-effects / entry guard / esm specifierspnpm check:side-effects-array, check:entry-guard, check:esm-specifiersOK

packages/types' type-check was confirmed to actually read the edited file:
tsc -p tsconfig.test.json --listFiles lists both spec-derived-unions.test.ts
and test-support/src/spec-enum-options.ts. A "typecheck is clean" that excluded
them would have said nothing.

One transient red is worth recording so nobody re-finds it:
scripts/__tests__/check-sdui-registration-pins.test.ts fails only while a local
turbo run build output is on disk
— it then derives
packages/app-shell/dist/console/connect/ConnectAgentWidget.js where it expects the
src/ path. With packages/*/dist removed it passes 11/11, and this branch touches
zero files under app-shell/src/console/. Pre-existing environment sensitivity, not
this diff.

Out of scope, filed separately

A sibling family turned up that this card's instrument does not select: 9 sites
spelling the cast non-optionally (as { options: readonly string[] }), so a
failed cast spreads undefined and throws rather than going quietly empty. Different
failure class, same hand-copied reader; recorded rather than folded in, and the
follow-up card names them. Not addressed here; #6924 is the quiet-permissive half.


Generated by Claude Code

…mily (#6924)
17 hand-written `(Schema as { options?: readonly string[] }).options` casts
across 16 spec-parity test files in 11 workspace packages read `.options`
straight off a node that IS the enum. The cast is unchecked: an enum that
acquires a wrapper, or a Zod build that moves `.options`, yields `undefined`
and the derived vocabulary silently becomes the empty set.
`@object-ui/test-support` already carried the wrapper walk, behind a signature
this family could not use: `shapeEnumOptions(schema, key)` opens with
`resolvePropsShape` and then indexes `shape[key]`, so a bare enum has no shape
to resolve and no key to index. That is a missing ENTRY POINT, not a missing
reader. So the walk is exported as `enumOptions(node)` and `shapeEnumOptions`
delegates to it -- one walk, two entry points, no second copy.
Verdict preservation measured, not assumed: against the installed pin, all 23
readings return the identical array in the identical order before and after.
`palette-discussion-alias.test.tsx` gains the non-vacuity assertion the reader's
docblock makes every caller owe; the other 15 files already carried one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

os-dev-report

Conflict against main resolved by merge commit6560e0f60 (no rebase, no amend, no force-push). git merge-tree --write-tree origin/main 6560e0f60 now exits 0, against both d3bf4fa6f and the newer 7fc5c3c127; the GitHub API reports mergeable: true.

Two files, both append-collisions with PR #7022, both sides kept.

packages/test-support/src/index.ts — the barrel now carries the union:

export{enumOptions,shapeEnumOptions}from'./spec-enum-options';/* ...#7022's docblock... */export{ZOD_WRAPPER_KEYS}from'./zod-wrapper-keys';

packages/test-support/README.md — this branch's rewritten spec-enum-options.ts entry AND #7022's two zod-wrapper-keys entries, in main's ordering.

One sentence was corrected rather than kept verbatim, and it is worth naming because it is the one place a "keep both sides" resolution would have produced a false statement. This branch's text said the other censused reader classes, "array-element unwrapping, the wrapper-key walk — are NOT confined here yet and are still hand-copied". #7022 made the wrapper-key half false. The merged sentence carries #7022's fact and this branch's surviving half:

Of the other Zod-internals reader classes #5872 censused, the wrapper-key list is now shared as DATA (below, objectui#6923); array-element unwrapping is NOT confined here yet and is still hand-copied.

pnpm-lock.yaml auto-merged with no markers, and pnpm install left it byte-unchanged, so it needed no regeneration and none was hand-written.

Re-run on the merge commit (root-relative vitest only):

checkresult
pnpm exec vitest run packages/test-support/ + all 16 converted sites + the 4 class-(1) consumers + #7022's 3 touched suitesTest Files 27 passed (27) / Tests 399 passed, 1 skipped (400)
pnpm exec vitest run scripts/Test Files 91 passed (91) / Tests 2582 passed (2582) — 91, up from 90, because #7022's zod-wrapper-keys.shared.test.ts is now in the tree and green here
turbo run type-check over test-support, types, core, app-shell, components, fields, react, plugin-charts, plugin-dashboard, plugin-detail, plugin-grid, plugin-reportTasks: 41 successful, 41 total
pnpm exec eslint packages/test-support/exit 0, no findings
node scripts/check-changeset-presence.mjsCompared the working tree with d3bf4fa6f (merge-base with origin/main) — 1 changeset, empty frontmatter
node scripts/check-control-bytes.mjsOK (scanned 5834 tracked text file(s))

Diffs above were computed from the merge-base, never two-dot: git diff $(git merge-base origin/main HEAD) HEAD is 28 files, exactly this branch's own set plus the changeset — no foreign file rode in.

Still draft; not marked ready, no auto-merge, not merged. No scope was added while in there — resolution only.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:14
@os-sam
os-sam enabled auto-merge August 31, 2026 14:14
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-tZ_USG_l.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.27KB8.44KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 78e98bfAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6924-enum-options-reader branch August 31, 2026 14:39
os-sam pushed a commit that referenced this pull request Aug 31, 2026
…the shared reader (#7025)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR #7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. #6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into #6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR #7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- #5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a #5872 class
(2)/(4) shape rather than this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…the shared reader (objectstack-ai#7025) (objectstack-ai#7057)
9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
options-bearing shape and read `.options` straight off it -- the SIXTH spelling
of the Zod-internals reader hazard objectui#5872 catalogues, and the one
objectui#6924's instrument (`options?:`) structurally cannot see.
They converge onto `@object-ui/test-support`'s `enumOptions(node)`, the walk
PR objectstack-ai#7024 exported for objectui#6924's family.
The decisive difference from that family is the FAILURE MODE, and it decides the
shape of every conversion here. objectstack-ai#6924's sites guarded with
`Array.isArray(raw) ? [...raw] : []`, so a failed cast went quietly empty. These
sites spread the cast result directly, so a failed cast THROWS. `enumOptions`
deliberately answers `[]` rather than raising, so a bare conversion would have
downgraded a loud throw into a silent empty vocabulary -- turning this population
into objectstack-ai#6924's, backwards. Every site therefore keeps its own non-vacuity duty:
- 7 module-scope reads gain a throwing wrapper, modelled on the two sites that
already solved this in PR objectstack-ai#7024 (`types/spec-derived-unions.test.ts` and
`examples/schema-catalog/.../component-fixture-declared-keys.test.ts`);
- 2 in-test reads (`plugin-grid/spec-symbol-batch7`,
`types/spec-subschema-parity`) already carried an explicit non-vacuity
assertion, which is the sanctioned alternative, so they keep theirs.
The four `expandableFamily.identity-*` files are textually identical across four
packages -- objectstack-ai#5872 class (1)'s sharpest shape, where a reviewer diffing one copy
cannot see that the other three did not move -- and were converted as one unit.
Verdict preservation measured, not assumed: all 9 readings return the identical
array in the identical order before and after (FieldType n=49,
ColumnSummarySchema n=11) against the installed pin.
The retired spelling is deliberately not quoted in the new comments: the card's
enumeration instrument is a grep for it, and a comment carrying the literal text
would make every future re-derivation read a false positive.
Out of scope, measured and left alone: `types/spec-subschema-parity.test.ts:161`
casts to an `unwrap().def.options` shape -- a further spelling, and a objectstack-ai#5872 class
(2)/(4) shape rather than this one.
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sam@claude