Skip to content

fix(plugin-form,types): resolve form.sections[].group through the single field-group assembler, and bound the section loop that blanked the form - #8644

Merged
os-justin merged 4 commits into
mainfrom
claude/issue-7051-form-sections-group-reference
Sep 9, 2026
Merged

fix(plugin-form,types): resolve form.sections[].group through the single field-group assembler, and bound the section loop that blanked the form#8644
os-justin merged 4 commits into
mainfrom
claude/issue-7051-form-sections-group-reference

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #7051

Deliverable 2 of that card — the view-level form.sections half of objectstack#13855. Deliverable 1 (record:details) landed as #8584 and is not touched here. Base pinned at c4326fe0a; every reading below is against that commit, not against the moving origin/main.


The premise held, and the two surfaces are NOT symmetric

Re-measured, with a lit control. Sweeping packages/*/src/* + apps/*/src/* for a section-level group read returns 4 hits, all of them PR #8584's record-details.tsx. The same command shape over the same population for field.group returns 9 hits across 6 files, so the instrument fires and the zero is a real zero: no form-section renderer read section.group. Premise valid.

The card's warning not to assume symmetry was the right one, and the answer is that the six form layouts are not even symmetric with each other. Measured by rendering an authored { group: 'basic' } section on each, on the base commit:

layout what an on-spec { group } section did containment needed
simple threw Cannot read properties of undefined (reading 'map') and blanked the ENTIRE form — sibling sections gone, body empty yes
tabbed / split / drawer / modal silently rendered nothing, no diagnostic; siblings fine no
wizard rendered an empty step ("No fields configured for this step") no

The throw was captured with an error boundary rather than inferred: SimpleObjectForm (ObjectForm.tsx:1262) -> ObjectForm.tsx:1272, i.e. the section loop in the component's own body, above the JSX it returns. That is the same above-the-loop shape record:details had, so a per-section error boundary could not have caught it either — the card's central warning, confirmed on the one layout where it applies. The other five differ because buildSectionFields (sectionFields.ts) already spells the same read section.fields ?? [].

What changed

One resolution site, above the routing fork. ObjectForm's dispatcher rebuilds each variant's sections prop key by key, so a group left in place is dropped before any container can see it, and each container would otherwise need its own copy of the resolution. Resolving in the dispatcher's own schema memo is what makes "one assembler" true for all six layouts at once — and the four container rows in the pin file are what makes that claim checkable rather than asserted.

Resolution goes through the one adapter. deriveFieldGroupSections (fieldGroups.ts) is this package's single adapter onto deriveFieldGroupLayout (ADR-0085 §5), and it is the same one the no-sections field-group fallback already used. Authoring a group by reference and letting the fallback derive it now produce the same section because one of them is the other's code path — the same construction argument #8584 made for deriveFieldGroupDetailSections. No assembly rule is re-implemented here.

The load is gated. The dispatcher fetches the object definition only when a section actually authors group, so a form that does not use the reference form issues no request and takes no new path. resolveSectionGroupReferences returns its input array by reference when nothing uses the form, so the memo cannot perturb an existing form either.

Two containment layers, and only one of them is about group. Layer 1 is the resolution itself. Layer 2 is section.fields ?? [] at ObjectForm.tsx's section loop, which bounds every other way a memberless section can reach that line — the sixth reader joining the five that already spelled it that way.

One divergence from #8584, deliberate and measured

#8584 drops an unresolvable group section. Here it is emptied (fields: []) instead. Both render nothing; the difference is forced by this surface. A form whose sections array empties out stops being a sectioned form at all and falls back to the flat every-field layout, so dropping would make one mistyped key render more than the author asked for — and it would also move the wizard's step count and the container routing's condition. Reasoning is in the module docstring so the next reader does not "fix" it back.

Where the spec's refusals are actually enforced — measured, not assumed

The card asked that group-owned presentation keys beside group, and group on a wizard step, not be silently accepted. #8584 concluded that existence checking belongs to @objectstack/lint. These two are stronger than that: they are refused at parse, by @objectstack/spec 17.3.0 itself.

authored shape FormSectionSchema / FormViewSchema verdict
{ group, fields } REFUSED — "mutually exclusive on this form section"
{} / neither key REFUSED — "must declare its members exactly one way"
group + name / label / description / collapsible / collapsed / visibleWhen REFUSED, one issue per key
group + columns / pane ACCEPTED (the two page-layout keys)
group on a type: 'wizard' section REFUSED; accepted on simple / tabbed / split / drawer / modal
group: 'Not A Key' REFUSED — FIELD_GROUP_KEY_PATTERN

So authored metadata carrying any of them never reaches this renderer. What does reach it is a programmatic SDUI caller, which does not pass the spec door — and for those the renderer reports rather than accepts: a restated group-owned key is ignored (no override semantics) and warned about; a wizard group is refused and warned about; an unresolvable key renders nothing and is reported once, naming form-section-group-unknown. The parse door itself is pinned in the test file, because "enforcement lives upstream" is a claim about upstream, and a claim nothing checks is how a door quietly stops closing.

⚠️ One premise in the dispatch order was falsified here. It carried "aliases fieldGroup / groupKey fold to group". On FormSectionSchema they do not fold: they are unrecognized keys, refused with a did-you-mean hint. A renderer-side fold would therefore have been a second, more permissive contract. Pinned so the correction survives.

The types half — and which instrument can see it

ObjectFormSection had the analogous omission record:details had (objectui#8583), but a narrower one: exactly one missing key (group) plus a fields that was REQUIRED, and nothing declared that the spec refuses. Enumerated against the installed schema, FormSectionSchema accepts name, label, description, collapsible, collapsed, columns, pane, visibleWhen, visibleOn, fields, group; the type declared all of those but group and visibleOn. (visibleOn is the deprecated ADR-0089 alias and its absence from the authoring type looks deliberate; it is left alone and reported, not widened.)

The instrument was established with a lit control before either reading was trusted. With group?: string removed from the interface — mutation proven on disk, 1 -> 0 occurrences, blob hash moved:

instrument verdict on the offending literal
vitest (2 files, 43 tests) GREEN, exit 0 — it strips types
pnpm --filter @object-ui/types type-check RED, exit 2TS2353 x3, naming group on ObjectFormSection and on the keyof ObjectFormSection key census

So the type-level legs are type-check legs, and a green vitest says nothing about them. Restored and proven restored (blob hash equal to HEAD's, git diff HEAD empty).

The same instrument caught a consequence I would otherwise have shipped: making fields optional broke five call sites in ObjectForm.tsx (TS2322, the per-layout config types are fields-required), and vitest was green on all 3736 tests at that moment.

Ablation — run, not predicted, and it changed the tests twice

Every leg: mutation proven on disk in both directions (before/after occurrence counts plus a moved blob hash, with a void-leg abort if the blob did not move), classified per-test from vitest's JSON reporter, restored by git checkout HEAD -- path and proven restored by blob hash and an empty git diff HEAD.

leg reddens stays green
caricature — resolver answers a constant group for every input SECTION_GROUP_RENDERS_ITS_OWN_MEMBERS, SECTION_GROUP_LABEL_IS_THE_GROUPS_OWN, UNKNOWN_GROUP_..., all four container rows, 2 resolver units (9 red / 22) SIBLING_SECTION_SURVIVES, FORM_IS_NOT_BLANKED, SECTION_WITHOUT_MEMBERS_IS_BOUNDED, every spec-door row
resolution removed entirely 13 / 22, every group leg the three containment / non-regression legs
layer 2 (?? []) reverted, resolution kept SECTION_WITHOUT_MEMBERS_IS_BOUNDED only (1 / 22) everything else
wizard refusal removed WIZARD_STEP_GROUP_IS_REFUSED_AND_REPORTED only everything else
description / visibleWhen carry removed the presentation resolver unit only everything else

⚠️ The stated non-regression axis does not discriminate — again. SIBLING_SECTION_SURVIVES and FORM_IS_NOT_BLANKED both pass under the caricature, exactly as the card's correction from objectui#8497 predicted. The file says out loud which legs are which so nobody re-reads the weak one as the strong one. The discriminating leg is SECTION_GROUP_RENDERS_ITS_OWN_MEMBERS: two sections referencing different groups, asserted as an ordered structural outline of headings and concrete field names, so a resolver that answers the same thing for every input cannot satisfy both.

The ablation changed the tests twice.

  1. The four container rows and the label leg originally referenced contact_info, the first declared group — and stayed GREEN under the caricature, because the first group is exactly what a constant resolver returns. They now reference the second group (billing) and go red. A leg a caricature satisfies by accident tests nothing.
  2. Layer 2 was unpinned by anything else: with group resolved first, a { group } section is never memberless by the time the loop sees it, so every group leg stayed green with the tolerant read reverted. SECTION_WITHOUT_MEMBERS_IS_BOUNDED was added specifically for it, and the third row above is the proof that it is the only leg that catches it. This is the same discovery fix(plugin-detail): implement sections[].group, bound a section's blast radius, and resolve enumerated field labels through the declared label #8584 recorded about its own two guards.

Harness-kill leg. Every rendered leg navigates through requireLiveForm, whose anchor is a section no leg asserts anything about, and whose failure text (HARNESS DEAD: expected exactly 1 ...) is textually unlike every content assertion. It fires in both directions — zero anchors and duplicated anchors — so a caricature cannot satisfy the harness by adding a section. No positional navigation anywhere.

Verification

check result
packages/plugin-form + packages/types (final HEAD 3c57b9c2f) 236 files / 3737 pass, 1 skipped, exit 0
@object-ui/types type-check (3 programs) pass, exit 0
@object-ui/plugin-form type-check (2 programs) pass, exit 0
apps/console touched test file 16 pass, exit 0
node scripts/check-changeset-presence.mjs verdict line: "8 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s): .changeset/7051-form-section-group-reference.md."
node scripts/check-changeset-fixed.mjs "All workspace packages are in the changeset fixed group."
node scripts/check-control-bytes.mjs "OK (scanned 6839 tracked text file(s); skipped 85 binary)."
eslint over the 8 changed files, --format json 0 errors, 114 warnings, all pre-existing no-explicit-any / react-hooks rules on untouched lines

On the eslint narrowing, so it reads as a measurement and not as a skipped run. Population is the 8 files this PR changes, counted from the JSON reporter's own array length. The repo's eslint.config.js sets no parserOptions.project and no projectService, so type-aware linting is not enabled and this diff cannot move the verdict on any file it does not touch. The repo-wide run is CI's.

Everything else in the affected set is CI's: @object-ui/types is a root dependency, so turbo ls --affected names essentially the whole monorepo, and running that locally would be re-running CI.

Fixture triage

One pre-existing file needed a prose correction rather than an assertion change: apps/console/src/__tests__/public-block-binding-reach.test.tsx documented its sections sample by asserting "this repo's own type says the same: ObjectFormSection.fields is REQUIRED" and "ObjectForm.tsx:1166 reads section.fields.map(...) off each entry unguarded". Both halves moved with this PR. The docblock is rewritten, not deleted: the fixture's reasoning is unchanged (a bare string is still not a section, and {} is still refused — the quoted spec message is re-measured against 17.3.0), only its grounds, and the two moved halves are called out explicitly so the paragraph cannot be read as still-true. That file's 16 tests pass unchanged.

Out of scope, filed

objectui#8641apps/console's own FormPage.buildSections is a third form-section consumer with the same gap: sec.fields ?? [] on a { group } section yields a section with no label and no fields, silently. It does not go through ObjectForm's dispatcher and the resolver here is package-internal, so fixing it is a real design choice (export the resolver / call the assembler directly / record the divergence), not a copy-paste. Filed with the discriminating axis attached, and with the honest note that it was established by reading the source rather than by a DOM measurement.

Risk and rollback

The one behaviour change reaching a path that does not author group: deriveFieldGroupSections now carries the group's description and visibleWhen onto the section it derives, which the no-sections field-group fallback also uses. description is additive. visibleWhen is not purely additive — an object that declares a fieldGroups[].visibleWhen and relies on the fallback would now have that predicate honoured, and a predicate resolving false hides the section. That is the ADR-0089 contract and the key the spec names as group-owned, so carrying it is the correct direction; it is called out because it is the only line in this PR that can change an existing form. Rollback is the two added lines in fieldGroups.ts, and the resolver unit leg pins them.

Everything else is gated on an authored group: no such section, no fetch, same array reference out of the resolver, no new path.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…single field-group assembler

The view-level half of objectstack#13855 (objectui#7051); the `record:details`
half shipped as objectui#8497.

`@objectstack/spec` 17.3.0 declares two ways for a `form.sections[]` entry to
give itself members: enumerate `fields`, or point `group` at one of the object's
declared `fieldGroups`. No form-section renderer read `group`, and the omission
was not a no-op — measured on `origin/main`:

  simple                       threw `Cannot read properties of undefined
                               (reading 'map')` out of SimpleObjectForm's own
                               body, ABOVE the JSX it returns, and blanked the
                               whole form (siblings included)
  tabbed/split/drawer/modal    silently rendered nothing, no diagnostic
  wizard                       an empty step

`ObjectForm` now resolves the reference once, above its routing fork, so all six
layouts inherit it. Resolution runs through `deriveFieldGroupLayout` (ADR-0085
§5) via this package's existing single adapter — the same code path the
no-sections field-group fallback uses — so a group authored by reference and one
derived by the fallback are the same section by construction. No assembly rule
is re-implemented on this side.

The object definition is fetched only when a section actually authors `group`.

Unresolvable references render nothing and are reported once; a group-owned key
restated beside `group` is ignored (no override semantics) and reported; `group`
on a wizard step is refused and reported. All three are shapes the spec door
refuses at parse, so they reach the renderer only from programmatic SDUI
callers — the door itself is pinned rather than assumed.

`ObjectFormSection` gains `group` and makes `fields` optional, so the spec-legal
shape compiles for a TypeScript author; the shared adapter now carries the
group's `description` and `visibleWhen` onto the derived section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…ately (objectui#7051)

With `group` resolved above the section loop, every group leg stays green with
the tolerant `?? []` read reverted — the guard was unpinned by anything else.
This leg is the only one that fails then.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…rows (objectui#7051)

Measured, not predicted: with the first group these rows stayed GREEN under the
constant-resolution caricature, because the first group is exactly what that
caricature returns. A leg a caricature satisfies by accident tests nothing.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

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

Size Limits

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

`UNGATED_EXAMPLES` in `scripts/check-doc-example-types.mjs` is keyed by
`path:line symbol`, where the line is the position of the `@example` JSDoc
tag. Two rows on this branch named lines that no longer hold their tag, so
`scripts/__tests__/check-doc-example-types.test.ts` reddened on
"every row names a block that is actually in the compiled tier".

Both blocks still exist and are still in the compiled tier; only their
position moved, and both example bodies are byte-identical to the
merge-base, so each row's recorded diagnostic codes and written reason
stay accurate:

  ObjectForm.tsx  122 -> 123   (one added import above the JSDoc)
  objectql.ts    1572 -> 1604  (schema additions above the JSDoc)

`origin/main` carries both tags at their original lines, so this branch is
the sole cause; no merge from `main` is required. The line-number keying
that makes an unrelated PR edit a CI gate script is objectui#8614.

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

Copy link
Copy Markdown
Collaborator Author

CI repair: Test (shard 3/4) — two doc-example ledger rows re-keyed

scripts/__tests__/check-doc-example-types.test.ts:264 (the real ledger > every row names a block that is actually in the compiled tier) was the only red on 3c57b9c2f; the other 34 checks were green.

What the key is

ledgerKey in scripts/check-doc-example-types.mjs:1051 is:

export function ledgerKey(block) {
  return `${block.file}:${block.line} ${block.symbol}`;
}

block.line is the 1-based line of the @example JSDoc tag. So a row is invalidated by any edit that moves that tag, whether or not the example itself changed.

What actually moved

CI printed one key, but the loop throws on the first failure — a census probe on this head found two stale rows, so fixing only the printed one would have traded this red for the next:

ledger row was now why
packages/plugin-form/src/ObjectForm.tsx ObjectForm 122 123 the added ./sectionGroups import sits above the JSDoc
packages/types/src/objectql.ts ObjectFormSchema 1572 1604 the schema additions sit above the JSDoc

Both blocks still exist and are still in the compiled tier — this is a move, not a deletion:

present: "packages/plugin-form/src/ObjectForm.tsx:123 ObjectForm" symbol=ObjectForm pkg=@object-ui/plugin-form
present: "packages/types/src/objectql.ts:1604 ObjectFormSchema"

Both example bodies are byte-identical to the merge-base (diff of the two regions is empty), so each row's recorded codes and written reason stay accurate — no row was re-derived, only re-addressed.

main is ruled out as the cause

The @example tag sits at its original line on both the merge-base and origin/main, and only moved on this branch:

c4326fe0a (merge-base)  ObjectForm.tsx:122   objectql.ts:1572
da5e4f69e (origin/main) ObjectForm.tsx:122   objectql.ts:1572
3c57b9c2f (this branch) ObjectForm.tsx:123   objectql.ts:1604

main has not touched scripts/check-doc-example-types.mjs since the branch point. Of main's 122 changed files, exactly one carries a ledger row (packages/core/src/data-scope/DataScopeManager.ts), and its tag is unmoved at line 68 on all three refs — so merging main in would neither fix nor break anything here. No merge was needed; the repair is the re-key alone.

Verification

Reproduced the red first, then showed the same check green:

before  Test Files 1 failed (1)   Tests 1 failed | 45 passed (46)
        AssertionError: packages/plugin-form/src/ObjectForm.tsx:122 ObjectForm: expected false to be true
after   Test Files 1 passed (1)   Tests 46 passed (46)

Neighbourhood re-run so one red is not traded for another:

  • pnpm exec vitest run scripts/ — 125 passed | 2 skipped (127 files), 3684 tests
  • pnpm exec vitest run packages/plugin-form/ packages/types/ — 236 passed, 3737 tests
  • node scripts/check-doc-example-shared-reader.mjsOK 80 documented symbol(s), 3989 call site(s), 11 slot(s) ...
  • node scripts/check-changeset-presence.mjs — green on the existing changeset; the gate script is not published source, so no new changeset is owed

On having to edit a CI gate script from a plugin-form PR

This is exactly the tax objectui#8614 describes: UNGATED_EXAMPLES is keyed by line number, so a one-line import added above a JSDoc block forces an unrelated PR to edit a CI gate script to stay green. Paying it here because this PR has to land; objectui#8614 remains the standing card for the keying mechanism and is deliberately not addressed in this diff — changing how the ledger is keyed carries its own design question and would widen a plugin-form PR into gate-infrastructure work.

Diff of this repair is two lines, both ledger keys.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

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

Size Limits

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants