Skip to content

fix(console): return packages/core to the framework chunk by grouping rule, and pin chunk membership - #9488

Draft
claude[bot] wants to merge 1 commit into
mainfrom
claude/issue-9345-core-chunk-membership
Draft

claude[bot] wants to merge 1 commit into
mainfrom
claude/issue-9345-core-chunk-membership

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Part of objectui#9345 — route B, director seat decision batch #128 item 2

Two ruled deliverables land here. ⚠️ One acceptance criterion is NOT met and no constant was moved to meet it — read the first section before the rest.


⚠️ The number that disagrees with the ruling: framework is over its ceiling

The ruling expected framework to read about 82.6 K with membership restored, under its 100,000 ceiling, so that nothing needed re-pinning. Measured, it reads 104,636 gzipped bytes — 4,636 over. pnpm check:eager-closure exits 1 on this branch and prints:

framework                102.2 KB / 97.7 KB ceiling (OVER by 4.5 KB)

⛔ No ceiling, baseline or exemption constant was moved to absorb that, per the ruling. The row is left red and reported.

Why the two figures differ, measured rather than guessed. The card's ~82.6 K came from an ablation that ALSO reverted objectui#9185's dynamic import. That import is what keeps every export of @object-ui/core live, so reverting it shrinks the retained module set. This repair keeps the import — the ruling requires that — so the full core surface stays, and it now lands on the line that names it.

These bytes are not new. data-adapter is in the eager closure, so the browser was already downloading them. Across the pair, before and after this change on the same tree: 123,388 gzipped before, 123,173 after, and the whole eager closure moves by −461 bytes. ⇒ the overage is PRE-EXISTING and this change makes it visible for the first time; it does not create it.

⇒ this is the "new reading for a new card" the ruling's point 3 anticipates. It needs a decision this seat does not carry, which is why this pull request is filed as Part of — the repair and the pin land here, the ceiling question stays open on the card.


1. The rolldown rule that re-attributed the subgraph, named

CodeSplittingGroup.includeDependenciesRecursively, whose default is true.

The reading that identifies it, from rolldown 1.2.7's own type documentation, two options one paragraph apart:

  • includeDependenciesRecursively — "Whether to include captured modules' dependencies." Default true.
  • priority — "Group with higher priority will be chosen first to match modules and create chunks. When converting the group to a chunk, modules of that group will be removed from other groups."

Together: a group takes the modules its test matches AND everything those modules import, and the lower-priority groups whose regex DOES match them lose them. packages/data-objectstack imports @object-ui/core; data-adapter is priority 84 and framework is 80. So framework's own declared members were written into a group with no ceiling and no baseline.

Why it moved with no edit to the chunking config. The size of the transfer tracks TREE-SHAKING. While @object-ui/core was imported by name only, the retained slice reachable through data-objectstack was a handful of modules — the config's own comment disclosed it as five. objectui#9185 added an import('@object-ui/core') of the BARREL in packages/app-shell, every export became live, and the same recursive walk reached all 92.

Causal proof, not inference. Adding includeDependenciesRecursively: false to that one group — nothing else changed, same tree — returns all 92 modules to framework. The control is below.

Not fixed by priority. @object-ui/react depends on both @object-ui/i18n and @object-ui/data-objectstack, so a recursive framework lifted above tier 84 would swallow the locale catalogues and this group in one move — objectui#7399's defect, re-run. Narrowing the receiving group leaves every priority untouched.

2. Membership after a real build, from the emitted chunks' own module lists

⚠️ Built first: turbo run build --filter=@object-ui/console... over the dependency closure (35 tasks, exit 0), then vite build per reading. Attribution is read from the bundle, never from the config.

package before (ff1d5ea8d1) after
packages/core 92 modules in data-adapter, 0 in framework 92 in framework
packages/types 19 in framework, 3 in data-adapter 22 in framework
packages/react 64 in framework 64 in framework
packages/data-objectstack 7 in data-adapter 7 in data-adapter

data-adapter now holds exactly its declared regex and nothing else. Every other workspace package's attribution is unchanged row for row.

3. The byte table, re-measured

chunk before after
framework 45,278 104,636
data-adapter 78,110 18,537
the two together 123,388 123,173
whole eager closure 3,134,906 3,134,445

data-adapter returns to the neighbourhood of the 19.6 K the gate's exemption rests on, as the ruling predicted. framework does not land where the ruling predicted — see the first section.

4. The membership pin, and its firing control

apps/console/vite.config.ts emits dist/chunk-membership.json: which chunk each workspace package's modules landed in, counted over EVERY emitted chunk, lazy ones included. evaluatePerChunkMembership in scripts/check-eager-closure-budget.mjs is a fifth half of that gate and asserts each budgeted group's declared packages landed wholly in their declared chunk. ⛔ EXACT, not a ratchet: one stray module is a finding, by name. A declaration is limited to chunks that carry a ceiling, and each package name is cross-checked in a unit test against that group's OWN regex, so the table cannot become a second opinion about the config.

Firing control — the mutation is the defect itself. Removing the flag, rebuilding, and reading:

✅ Per-chunk eager budgets (4 chunks weighed):
  ✅ framework                 44.2 KB / 97.7 KB ceiling (headroom 53.4 KB)
❌ 2 budgeted package(s) did not land in the chunk the console config declares for them:
  ❌ `packages/core` is declared in `framework` but 92 in `data-adapter` (0 of its 92 modules landed in `framework`)
  ❌ `packages/types` is declared in `framework` but 3 in `data-adapter` (19 of its 22 modules landed in `framework`)

⭐ That is the incident, reproduced and caught: every byte ceiling green, with 53.4 KB of apparent headroom, while the whole of packages/core sat in a chunk nothing budgets. The new half is the only thing in the tree that sees it.

Restore, rebuild, read again: the membership half is pass and names its population — 5 packages, 462 modules, each wholly in its declared chunk. Mutation proven on disk by blob hash (the file's HEAD blob against the mutated one, both printed), restore proven by that blob hash matching HEAD again AND by git diff HEAD being empty; the script carries a trap ... EXIT INT TERM restore on an absolute path. The static half fires too: the unit pin on the flag reds under the same mutation and is green after.

Every zero in this pull request carries a control that returned non-zero in the same command — the membership pass case is paired with the stray, whole-package-moved, majority-in-place, absent-artifact, unknown-version, empty-attribution, absent-package and unbudgeted-declaration cases; the regex cross-check carries a must-miss control.

5. Verification

  • pnpm exec vitest run scripts/ — 158 files, 4,654 tests, exit 0. The gate's own suite is 162 of them.
  • pnpm exec tsc -p tsconfig.scripts.json --noEmit exit 0; pnpm exec tsc -b apps/console/tsconfig.node.json --force exit 0.
  • check:control-bytes, check:new-line-citations, check:eager-locale-catalogues, check:docs-route-closure, check:comment-mask-corpus, check:shell-escape-residue, check:governed-queue-guard, check:required-check-set, check:changeset-claims — all exit 0.
  • check:eager-closure exits 1 on the framework ceiling row above. Its membership half is pass.
  • Lint, narrowed and declared: eslint --no-inline-config over the 5 linted files in this diff, 0 errors and 0 warnings, file count read from --format json. The narrowing is a measurement, not a skip — eslint.config.js configures no project or projectService, so type-aware linting is off, and no rule under eslint-rules/ reads the filesystem at all (a grep for readFileSync / readdirSync / existsSync over that directory exits 1). ⇒ this diff cannot move the verdict on a file it does not touch. The .yml file is outside eslint's population entirely.
  • Exit codes were captured by redirect-then-capture throughout, never across a pipe.

gates_weakened: NONE. Nothing was relaxed, deleted or given headroom. The only pre-existing assertions edited are the advancedChunks group parse in the gate's test file — widened to read an options tail it previously could not, which had made it stop matching the group ENTIRELY rather than degrade — and the main harnesses, which now co-write the second artifact the way a real build does. Both are additions of subject, not removals of it.

6. Changeset

None owed, measured rather than argued: node scripts/check-changeset-presence.mjs exits 0 and prints its own reading — 6 files changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved.

Scope

The diff is the build config, the gate, the gate's tests, and the two files the gate's own tests REQUIRE a new half to be wired through (the workflow env line and the PR-comment renderer — render-budget-comment.test.ts fails until a published output reaches the comment step). ⛔ Nothing under apps/console/src/ is touched, so the lane fence this card carries is intact. objectui#9185's dynamic import is untouched.


Generated by Claude Code, session session_013VGeMu3p6qEFWR6K6GGLaW — written here as prose and as a code span so the reference survives a body rewrite.

Generated by Claude Code


Generated by Claude Code

… rule, and pin chunk membership

`data-adapter` declares `includeDependenciesRecursively: false`, so the group
holds exactly what its regex names. The rule that re-attributed the subgraph is
rolldown's `includeDependenciesRecursively` (default `true`): a group captures
its matched modules AND everything they import, and the same option's priority
doc states those are then removed from lower-priority groups whose regex does
match them. `packages/data-objectstack` imports `@object-ui/core` and this group
outranks `framework` (84 over 80), so the recursive half handed `framework`'s
declared members to a group with no ceiling and no baseline.

The transfer tracks TREE-SHAKING, which is why it moved with no edit here:
while `@object-ui/core` was imported by name only, the slice reachable through
`data-objectstack` was a handful of modules; objectui#9185 added an
`import('@object-ui/core')` of the BARREL, every export became live, and the
same walk reached all 92.

Measured, console build either side of the one-line repair:

  data-adapter  78,110 -> 18,537 gzipped, holding only packages/data-objectstack
  framework     45,278 -> 104,636 gzipped, holding core|react|types and nothing else
  eager closure -461 bytes across the whole bundle

The bytes were always downloaded — `data-adapter` is in the eager closure — so
this is a re-attribution, not a payload change.

Second half: `scripts/check-eager-closure-budget.mjs` gains a MEMBERSHIP half.
`apps/console/vite.config.ts` emits `dist/chunk-membership.json` (which chunk
each workspace package's modules landed in, counted over every emitted chunk,
lazy ones included) and `evaluatePerChunkMembership` asserts each budgeted
group's declared packages landed wholly in their declared chunk. EXACT, not a
ratchet: one stray module is a finding, by name. Every pass-by-measuring-nothing
route is an error rather than a pass — absent artifact, unknown version, empty
attribution, a declared package absent from the bundle, a declaration naming a
chunk no ceiling governs.

No ceiling, baseline or exemption constant moves.

Refs: objectui#9345

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

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.1 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-D_DdfqN4.js
Status FAIL

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

Which half objected:

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
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.83KB 10.95KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.46KB 65.59KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
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.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
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.04KB 5.36KB
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

Copy link
Copy Markdown
Collaborator

HELD — correct, reviewed, and ⛔ not landable until another lane's one-liner lands

domain:devx @ objectui seat, session_013VGeMu3p6qEFWR6K6GGLaW, R58, 2026-09-14T11:0xZ.

The option-B measurement is in (card objectui#9345, comment 5662857036) and it resolves this PR's only open question. Recording the disposition here so nobody reads the red as a defect in this branch:

  • The red is this PR telling the truth. Bundle Analysis fails because framework is over its ceiling by 4.5 KB — and it was over all along. This repair did not cost 4,636 bytes, it made 4,636 bytes VISIBLE by returning packages/core modules from the unbudgeted data-adapter chunk to the budgeted one.
  • The forbidden repair is raising the ceiling. It is not being proposed and will not be.
  • The permitted repair is measured and over-delivers: narrowing one dynamic import in packages/app-shell frees 14,096 gz — the gap closes whole, with 9,460 gz to spare, and check:eager-closure goes exit 1 → exit 0 with ⛔ no constant touched.
  • That edit is domain:ui surface, not this lane's. Filed as objectui#9492 with the full measurement, the anchor, the fences and the acceptance.
  • This PR's repair survives it: with the narrowing applied, the membership half stays green — packages/core 76 modules in framework, zero core modules in any other chunk. objectui#9492's acceptance says so explicitly, so this branch cannot be quietly undone by the fix that unblocks it.

this PR stays open, stays draft, and is ⛔ NOT enqueued. Card objectui#9345 moves pm:dispatchedpm:blocked:

Blocked-by: #9492
Restart-when: `grep -c 'as unknown as RowCanonModule' packages/app-shell/src/views/metadata-admin/celAuthoring.ts` reads 0 on `main`

⛔ No approval from this seat, ⛔ no merge, ⛔ no ceiling moved, ⛔ no test skipped.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants