diff --git a/.changeset/6776-metadata-admin-lazy-registration.md b/.changeset/6776-metadata-admin-lazy-registration.md
new file mode 100644
index 000000000..17bdd4d2d
--- /dev/null
+++ b/.changeset/6776-metadata-admin-lazy-registration.md
@@ -0,0 +1,40 @@
+---
+'@object-ui/app-shell': patch
+---
+
+Take the metadata-admin engine out of the console's eager closure (objectui#6776).
+
+`AppContent` has declared six `lazy()` imports of `views/metadata-admin/index.ts`
+for a long time, and none of them deferred anything: the module ran five
+registrations at load, so the package's published `sideEffects` array named it,
+an array entry is unshakeable, and the package barrel re-exported 25 runtime
+values from it — an ordinary static edge from an entry every consumer imports.
+Every page, preview and inspector under `views/metadata-admin/` was therefore
+fetched and parsed before first render. Measured from
+`apps/console/dist/eager-closure.json`: **3,254,230 → 3,222,314 gzipped bytes,
+−31,916 B**, and the 172,945-byte `metadata-admin` chunk leaves the eager set
+entirely.
+
+**Published surface — two contract-bearing changes, no signature change:**
+
+- `packages/app-shell/package.json`'s `sideEffects` array now names
+ `views/metadata-admin/register-builtins` (the new leaf that performs the five
+ registrations) instead of `views/metadata-admin/index`. The five
+ registrations still run at package load, bare-imported by the package entry,
+ so nothing a consumer could observe changes — but the array is a contract
+ every consumer's bundler reads, so the swap is stated here rather than left
+ to a diff.
+- The package barrel's 25 metadata-admin runtime re-exports (and 11 type-only
+ ones) now point at their leaf modules. **Same names, same types.** They are
+ unreachable from outside the package by any other path — `exports` is
+ root-only — so no import an out-of-package consumer can write is affected.
+
+`registerAppComponent`'s signature is unchanged. `metadata:directory` and
+`metadata:resource` are now registered as `lazy()` values, each wrapping itself
+in its own `Suspense` boundary, which is the shape the already-lazy
+registrations in `apps/console` use; no render site changes.
+
+Also re-baselined `MAX_EAGER_CLOSURE_GZIP_BYTES` in the same change, from
+3,300,000 to 3,268,000. Taking 31,916 bytes out without moving the ceiling would
+leave 0.85x of the 89 KiB regression the gate exists to catch as headroom —
+near-blind — so the ratchet advances with the win rather than after it.
diff --git a/packages/app-shell/package.json b/packages/app-shell/package.json
index 7c4c3ed9a..c9eae8183 100644
--- a/packages/app-shell/package.json
+++ b/packages/app-shell/package.json
@@ -12,7 +12,7 @@
"./dist/services/builtinComponents.js",
"./dist/views/global-notifications-renderer.js",
"./dist/views/global-search-renderer.js",
- "./dist/views/metadata-admin/index.js",
+ "./dist/views/metadata-admin/register-builtins.js",
"./dist/views/record-approvals-renderer.js",
"./dist/views/record-attachments-renderer.js",
"./dist/views/studio-design/studio-canvas-preview.js",
@@ -25,7 +25,7 @@
"./src/services/builtinComponents.tsx",
"./src/views/global-notifications-renderer.tsx",
"./src/views/global-search-renderer.tsx",
- "./src/views/metadata-admin/index.ts",
+ "./src/views/metadata-admin/register-builtins.ts",
"./src/views/record-approvals-renderer.tsx",
"./src/views/record-attachments-renderer.tsx",
"./src/views/studio-design/studio-canvas-preview.tsx",
diff --git a/packages/app-shell/src/index.ts b/packages/app-shell/src/index.ts
index 6200b1b67..22003e143 100644
--- a/packages/app-shell/src/index.ts
+++ b/packages/app-shell/src/index.ts
@@ -309,57 +309,88 @@ import './views/record-approvals-renderer.js';
// `global:notifications`.
import './views/global-search-renderer.js';
import './views/global-notifications-renderer.js';
+// The metadata-admin engine's five load-time registrations (built-in anchors,
+// default JSONSchemas, the datasource resource, built-in previews, built-in
+// inspectors). objectui#6776 moved them OUT of `views/metadata-admin/index.ts`
+// into this leaf so the page barrel became shakeable; the bare import lives
+// HERE, on the package entry, and must not be moved onto the page barrel —
+// `scripts/vite-declared-lazy-views.ts` reads a bare import as "this module is
+// not pure" and the whole eager closure comes back. See the leaf's own header.
+import './views/metadata-admin/register-builtins.js';
// Phase 3c — generic metadata admin engine. Re-exported so plugins
// can call `registerMetadataResource()` to override the per-type
// list / edit / create components, and host apps can compose the
// page primitives directly when needed.
+//
+// ⚠️ These 25 runtime re-exports name the LEAF modules, never
+// `./views/metadata-admin/index.js` (objectui#6776). The names and their types
+// are unchanged — an out-of-package consumer imports exactly what it imported
+// before — but a named re-export is an ordinary STATIC EDGE, and the console's
+// entry imports this barrel, so pointing them at the page barrel made that
+// barrel (and every page, preview and inspector it reaches) eager on every
+// console page load, past the six `lazy()` declarations `AppContent` writes for
+// it. The 11 TYPE-ONLY re-exports below are erased at build and carry no edge;
+// they are grouped separately for that reason and not because they are less
+// public.
+export { MetadataDirectoryPage } from './views/metadata-admin/DirectoryPage.js';
+export { MetadataResourceRouter } from './views/metadata-admin/ResourceRouter.js';
+export { MetadataResourceListPage } from './views/metadata-admin/ResourceListPage.js';
+export { MetadataResourceEditPage } from './views/metadata-admin/ResourceEditPage.js';
+export { MetadataResourceHistoryPage } from './views/metadata-admin/ResourceHistoryPage.js';
+export { MetadataDiagnosticsPage } from './views/metadata-admin/DiagnosticsPage.js';
+export { MetadataQuickFind } from './views/metadata-admin/QuickFind.js';
+export { PageShell as MetadataPageShell } from './views/metadata-admin/PageShell.js';
+export { SchemaForm } from './views/metadata-admin/SchemaForm.js';
+export { LayeredDiff } from './views/metadata-admin/LayeredDiff.js';
export {
- MetadataDirectoryPage,
- MetadataResourceRouter,
- MetadataResourceListPage,
- MetadataResourceEditPage,
- MetadataResourceHistoryPage,
- MetadataDiagnosticsPage,
- MetadataQuickFind,
- MetadataPageShell,
- SchemaForm,
- LayeredDiff,
registerMetadataResource,
getMetadataResource,
listMetadataResources,
resolveResourceConfig,
+} from './views/metadata-admin/registry.js';
+export {
useMetadataClient,
useMetadataTypes,
useTypesIndex,
useGlobalDiagnostics,
matchesQuery,
+} from './views/metadata-admin/useMetadata.js';
+export {
registerMetadataPreview,
getMetadataPreview,
listMetadataPreviewTypes,
+} from './views/metadata-admin/preview-registry.js';
+export {
registerMetadataInspector,
getMetadataInspector,
listMetadataInspectorTypes,
-} from './views/metadata-admin/index.js';
+} from './views/metadata-admin/inspector-registry.js';
export type {
MetadataResourceConfig,
MetadataDomain,
- RichMetadataTypeEntry,
+} from './views/metadata-admin/registry.js';
+export type { RichMetadataTypeEntry } from './views/metadata-admin/useMetadata.js';
+export type {
MetadataPreview,
MetadataPreviewProps,
MetadataSelection,
+} from './views/metadata-admin/preview-registry.js';
+export type {
MetadataInspector,
MetadataInspectorProps,
- // The form authoring surface, in ONE declaration per layer: the field
- // (objectui#5040 / #5542) and the two containers above it (objectui#5596).
- // `apps/console` renders the same authored `FormView` documents this
- // package's metadata-admin does; before it could import these names it kept
- // its own hand-written copies of all three shapes. See the note on the
- // re-export in `views/metadata-admin/index.ts`.
+} from './views/metadata-admin/inspector-registry.js';
+// The form authoring surface, in ONE declaration per layer: the field
+// (objectui#5040 / #5542) and the two containers above it (objectui#5596).
+// `apps/console` renders the same authored `FormView` documents this package's
+// metadata-admin does; before it could import these names it kept its own
+// hand-written copies of all three shapes. See the note on the re-export in
+// `views/metadata-admin/index.ts`.
+export type {
FormFieldSpec,
FormSectionSpec,
FormViewSpec,
-} from './views/metadata-admin/index.js';
+} from './views/metadata-admin/form-spec.js';
// Studio WYSIWYG design surface (ADR-0080) — the open-source design surface.
// The left AI copilot is an injected `aiSlot`; OSS renders three zones.
diff --git a/packages/app-shell/src/services/builtinComponents.tsx b/packages/app-shell/src/services/builtinComponents.tsx
index 6e706838f..2caa77109 100644
--- a/packages/app-shell/src/services/builtinComponents.tsx
+++ b/packages/app-shell/src/services/builtinComponents.tsx
@@ -15,12 +15,9 @@
* metadata type.
*/
+import { lazy, Suspense } from 'react';
import { registerAppComponent } from './componentRegistry.js';
-import {
- MetadataDirectoryPage,
- MetadataResourceRouter,
- registerMetadataResource,
-} from '../views/metadata-admin/index.js';
+import { registerMetadataResource } from '../views/metadata-admin/registry.js';
import { PermissionMatrixEditPage } from '../views/metadata-admin/PermissionMatrixEditor.js';
import { PackagesPage } from '../views/metadata-admin/PackagesPage.js';
import { PackagedAutomationPage } from '../views/setup/PackagedAutomationPage.js';
@@ -33,18 +30,96 @@ import {
/* 1) Top-level admin pages — bound to `metadata:directory` + `metadata:resource` */
/* -------------------------------------------------------------------------- */
+/**
+ * ⚠️ TRAP — read this before "just making something here lazy" (objectui#6776).
+ *
+ * The two registrations below hold their pages behind `lazy()` + `Suspense`, and
+ * that is ONE HALF of a two-part change. The other half is that
+ * `packages/app-shell/src/index.ts` re-exports the metadata-admin names from
+ * their LEAF modules rather than from `views/metadata-admin/index.ts`, and that
+ * the five load-time registrations moved to
+ * `views/metadata-admin/register-builtins.ts`. Doing the `lazy()` here WITHOUT
+ * the other half is the smallest, most in-fence-looking version of this change,
+ * and it is worth almost nothing:
+ *
+ * Measured on `fab4802e3` — a full console build of that commit with ONLY this
+ * file's two registrations turned into `lazy()` values, against the same
+ * commit unmodified:
+ *
+ * eager closure 3,254,230 -> 3,254,441 B gzipped (+211 B)
+ * `metadata-admin` chunk 172,945 -> 173,341 B gzipped (+396 B)
+ * eager chunk count 45 -> 45 of 513 (UNCHANGED)
+ * the chunk itself still EAGER
+ *
+ * So it does not merely fail to pay — it costs bytes in both places, and it is
+ * the `lazy()`/`Suspense` scaffolding itself that it spends them on. And every
+ * gate stays GREEN while it does: that build exits 0, the
+ * `ineffective-dynamic-import` ledger prints its usual 43 pinned entries with no
+ * 44th, and `declared-lazy-views` prints "2 eager, all pinned". The ledger
+ * cannot see this because the static edge that defeats the `import()` does not
+ * live in this module at all — it lives in the package barrel's re-export. That
+ * is the objectui#5486 shape: code that CLAIMS a code split it does not have,
+ * with a loading fallback no user can ever reach.
+ *
+ * (The ruling that ordered this comment predicted −30 B and +189 B. The
+ * direction of the chunk growth and the green gates reproduced; the closure
+ * figure did not, and it came back POSITIVE. The measured numbers are the ones
+ * above.)
+ *
+ * ⚠️ And a second, INDEPENDENT rebuild of the same variant disagreed with that
+ * closure figure on its SIGN: −7 B where the run above measured +211 B. Both
+ * stand as what their run measured; together they say only that this delta is
+ * small and sensitive to the exact byte-form of the edit, so the sign is not a
+ * finding and neither is the "it costs bytes" reading of it. What both
+ * rebuilds reproduced identically IS the finding: the chunk stays EAGER and the
+ * eager chunk count stays 45 of 513 (build exit 0, every gate green). Cite
+ * those two, never a signed byte delta.
+ *
+ * So: a `lazy()` in this file is only ever true when nothing in the package's
+ * EAGER graph still names the same module statically. Check the barrel first,
+ * and measure from `apps/console/dist/eager-closure.json` and the emitted
+ * chunk's own module list — never from a source-level search, which cannot see
+ * chunk co-tenancy (objectui#6680, objectui#6681).
+ */
+const MetadataDirectoryPage = lazy(() =>
+ import('../views/metadata-admin/index.js').then((m) => ({ default: m.MetadataDirectoryPage })),
+);
+const MetadataResourceRouter = lazy(() =>
+ import('../views/metadata-admin/index.js').then((m) => ({ default: m.MetadataResourceRouter })),
+);
+
+function MetadataAdminFallback({ label }: { label: string }) {
+ return
Loading {label}…
;
+}
+
+/**
+ * The Suspense boundary lives INSIDE the registration value, which is the shape
+ * every other lazy `registerAppComponent` entry already uses
+ * (`apps/console/src/registerAccountComponents.tsx`,
+ * `registerDeveloperComponents.tsx`, `registerApprovalsComponents.tsx`). It
+ * keeps `registerAppComponent`'s published signature unchanged — a component
+ * VALUE, as before — and it means no render site has to learn about pending.
+ */
registerAppComponent({
ref: 'metadata:directory',
label: 'All Metadata Types',
source: '@object-ui/app-shell',
- component: MetadataDirectoryPage,
+ component: (props: any) => (
+ }>
+
+
+ ),
});
registerAppComponent({
ref: 'metadata:resource',
label: 'Metadata Resource',
source: '@object-ui/app-shell',
- component: MetadataResourceRouter,
+ component: (props: any) => (
+ }>
+
+
+ ),
});
registerAppComponent({
diff --git a/packages/app-shell/src/views/metadata-admin/index.ts b/packages/app-shell/src/views/metadata-admin/index.ts
index d0a2697f8..10edaac8c 100644
--- a/packages/app-shell/src/views/metadata-admin/index.ts
+++ b/packages/app-shell/src/views/metadata-admin/index.ts
@@ -66,30 +66,26 @@ export type {
MetadataAnchor,
} from './registry.js';
-// Side-effect: register the built-in anchor relationships so the Related
-// tab works out of the box for objects (hooks, views, pages, …).
-import { registerBuiltinAnchors } from './anchors.js';
-registerBuiltinAnchors();
-
-// Side-effect: register fallback JSONSchemas for the 12 writable types
-// so the generic SchemaForm renders a real form (vs raw-JSON fallback)
-// until the framework wires Zod→JSONSchema generation into /meta/types.
-import { registerDefaultMetadataSchemas } from './default-schemas.js';
-registerDefaultMetadataSchemas();
-import { registerDatasourceResource } from './datasource/register.js';
-registerDatasourceResource();
-
-// Side-effect: register built-in Preview-tab renderers (page, view,
-// dashboard, report, app, object, email_template). Plugins can add or
-// override entries via `registerMetadataPreview()`.
-import { registerBuiltinPreviews } from './previews/index.js';
-registerBuiltinPreviews();
-
-// Side-effect: register built-in scoped inspectors (dashboard widget,
-// …). Plugins can add or override entries via
-// `registerMetadataInspector()`.
-import { registerBuiltinInspectors } from './inspectors/index.js';
-registerBuiltinInspectors();
+/**
+ * ⛔ NO LOAD-TIME REGISTRATION BELONGS IN THIS FILE (objectui#6776).
+ *
+ * The five built-in registrations (`registerBuiltinAnchors`,
+ * `registerDefaultMetadataSchemas`, `registerDatasourceResource`,
+ * `registerBuiltinPreviews`, `registerBuiltinInspectors`) used to run here.
+ * They now live in `./register-builtins.js`, which the PACKAGE ENTRY
+ * (`packages/app-shell/src/index.ts`) bare-imports; that file carries the
+ * reasoning. In one line: a module that registers at load time is named by
+ * `@object-ui/app-shell`'s published `sideEffects` array, an array entry is
+ * unshakeable, and the package barrel re-exports 25 runtime values from HERE —
+ * so a registration in this file drags every page, preview and inspector under
+ * `views/metadata-admin/` into the console's eager closure, past the six
+ * `lazy()` boundaries `AppContent` declares for it.
+ *
+ * That also rules out the tidy-looking version of the same mistake: a bare
+ * `import './register-builtins.js';` on THIS module. `bareSideEffectImport` in
+ * `scripts/vite-declared-lazy-views.ts` reads that line and refuses to declare
+ * this barrel pure, which puts the closure straight back.
+ */
export {
registerMetadataPreview,
diff --git a/packages/app-shell/src/views/metadata-admin/register-builtins.ts b/packages/app-shell/src/views/metadata-admin/register-builtins.ts
new file mode 100644
index 000000000..576060a08
--- /dev/null
+++ b/packages/app-shell/src/views/metadata-admin/register-builtins.ts
@@ -0,0 +1,77 @@
+// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * The metadata-admin engine's five LOAD-TIME registrations, in a leaf module of
+ * their own (objectui#6776).
+ *
+ * ## Why they are not in `./index.ts` any more
+ *
+ * They used to sit at the bottom of the directory barrel, and that placement —
+ * not the registrations themselves — is what held 172,945 gzipped bytes of
+ * metadata-admin in the console's EAGER closure. The mechanism, in two links:
+ *
+ * 1. a module that registers at load time cannot be tree-shaken, so
+ * `@object-ui/app-shell`'s published `sideEffects` array named
+ * `views/metadata-admin/index.ts` (objectui#6683) and every bundler kept
+ * it whole; and
+ * 2. the package barrel (`packages/app-shell/src/index.ts`) re-exported 25
+ * runtime values FROM that barrel, and the console's entry imports the
+ * package barrel statically. A named re-export is an ordinary static edge,
+ * so the unshakeable module — and its whole import closure, every page,
+ * preview and inspector under `views/metadata-admin/` — was reachable
+ * eagerly even though `AppContent` declares six `lazy()` imports of it.
+ *
+ * Neither link can be cut where it is observed. Deleting the registrations is
+ * not on the table: they are load-bearing (drop them and the Related tab, the
+ * generic SchemaForm, the datasource resource, and every built-in Preview and
+ * Inspector go missing), and `scripts/vite-declared-lazy-views.ts` carries a
+ * guard that refuses to declare the module pure for exactly that reason.
+ *
+ * So the two concerns are SPLIT instead. This module is the side-effectful half
+ * and nothing re-exports through it; `./index.ts` is the pure re-export half and
+ * registers nothing. The package entry bare-imports THIS file, which keeps all
+ * five registrations exactly as eager as they were, while `./index.ts` becomes
+ * shakeable and reaches the browser only through the `lazy()` boundaries that
+ * always claimed to defer it.
+ *
+ * ## ⛔ Do not attach this import to the page barrel
+ *
+ * The bare import belongs to the PACKAGE ENTRY (`packages/app-shell/src/index.ts`).
+ * Putting it back on `./index.ts` — even as `import './register-builtins.js';` —
+ * re-creates the defect under a new name: `bareSideEffectImport` in
+ * `scripts/vite-declared-lazy-views.ts` reads that line and refuses to declare
+ * the barrel pure, so the barrel is unshakeable again and the closure returns.
+ *
+ * ## Ordering
+ *
+ * These five are independent of one another and of `./index.ts`: each writes
+ * into a registry module that holds a plain `Map`, and no registrar reads
+ * another's table at module scope. The order below is the order they were
+ * written in the barrel, kept so a `git log -p` of the move reads as a move.
+ */
+
+import { registerBuiltinAnchors } from './anchors.js';
+import { registerDefaultMetadataSchemas } from './default-schemas.js';
+import { registerDatasourceResource } from './datasource/register.js';
+import { registerBuiltinPreviews } from './previews/index.js';
+import { registerBuiltinInspectors } from './inspectors/index.js';
+
+// Register the built-in anchor relationships so the Related tab works out of
+// the box for objects (hooks, views, pages, ...).
+registerBuiltinAnchors();
+
+// Register fallback JSONSchemas for the 12 writable types so the generic
+// SchemaForm renders a real form (vs raw-JSON fallback) until the framework
+// wires Zod->JSONSchema generation into /meta/types.
+registerDefaultMetadataSchemas();
+
+registerDatasourceResource();
+
+// Register built-in Preview-tab renderers (page, view, dashboard, report, app,
+// object, email_template). Plugins can add or override entries via
+// `registerMetadataPreview()`.
+registerBuiltinPreviews();
+
+// Register built-in scoped inspectors (dashboard widget, ...). Plugins can add
+// or override entries via `registerMetadataInspector()`.
+registerBuiltinInspectors();
diff --git a/scripts/__tests__/check-eager-closure-budget.test.ts b/scripts/__tests__/check-eager-closure-budget.test.ts
index b3ac408d6..a602f15ba 100644
--- a/scripts/__tests__/check-eager-closure-budget.test.ts
+++ b/scripts/__tests__/check-eager-closure-budget.test.ts
@@ -470,10 +470,11 @@ describe('ceiling sensitivity, judged live (objectui#5924)', () => {
]);
// A passing run still prints every measurement, so a reader watching a
// ceiling drift upward sees it coming rather than the day it reds. The
- // literal is `BASELINE.gzipBytes` rendered, re-taken when objectui#6683
- // re-baselined it downward — a rendering derived in the test would agree
- // with the renderer by construction and pin nothing.
- expect(result.message).toContain('3177.7');
+ // literal is `BASELINE.gzipBytes` rendered, re-taken each time the baseline
+ // moves (objectui#6683 down to 3177.7, objectui#6776 down to 3146.8) — a
+ // rendering derived in the test would agree with the renderer by
+ // construction and pin nothing.
+ expect(result.message).toContain('3146.8');
});
it('is exactly one regression wide, from either side of the line', () => {
@@ -648,7 +649,7 @@ describe('main', () => {
expect(code).toBe(0);
expect(outputs.closure_status).toBe('pass');
expect(outputs.closure_chunks).toBe('5');
- expect(outputs.closure_gzip_kb).toBe('3177.7');
+ expect(outputs.closure_gzip_kb).toBe('3146.8');
});
it('exits 1 — a verdict about the BUNDLE — when over budget', () => {
diff --git a/scripts/__tests__/vite-declared-lazy-views.test.ts b/scripts/__tests__/vite-declared-lazy-views.test.ts
index d4088c98e..7cb4a66c4 100644
--- a/scripts/__tests__/vite-declared-lazy-views.test.ts
+++ b/scripts/__tests__/vite-declared-lazy-views.test.ts
@@ -188,17 +188,22 @@ describe('declaredSideEffectful', () => {
expect(declaredSideEffectful(['./src/**/*.css'], 'src/views/Alpha.tsx')).toContain('glob');
});
- it('agrees with the real package: metadata-admin/index.ts is declared side-effectful', () => {
+ it('agrees with the real package: register-builtins.ts is declared side-effectful', () => {
// The subject. This is the file whose FIVE top-level registration calls
// `bareSideEffectImport` cannot see, so without this guard the plugin would
// declare it pure the moment someone deleted its ledger line
- // (objectui#6681).
- const owner = nearestPackage('packages/app-shell/src/views/metadata-admin/index.ts', REPO_ROOT);
+ // (objectui#6681). objectui#6776 MOVED those five calls out of the page
+ // barrel and into this leaf; the guard's job and its blind spot are
+ // unchanged, only the file that carries them.
+ const owner = nearestPackage(
+ 'packages/app-shell/src/views/metadata-admin/register-builtins.ts',
+ REPO_ROOT,
+ );
expect(owner?.packageJsonPath).toBe('packages/app-shell/package.json');
- expect(owner?.packageRelative).toBe('src/views/metadata-admin/index.ts');
+ expect(owner?.packageRelative).toBe('src/views/metadata-admin/register-builtins.ts');
const manifest = JSON.parse(read(owner!.packageJsonPath)) as { sideEffects?: unknown };
expect(declaredSideEffectful(manifest.sideEffects, owner!.packageRelative)).toBe(
- './src/views/metadata-admin/index.ts',
+ './src/views/metadata-admin/register-builtins.ts',
);
// The positive control in the same query shape: a declared-lazy module the
// array does NOT name, so a matcher that answered "side-effectful" to
@@ -208,6 +213,21 @@ describe('declaredSideEffectful', () => {
).toBeNull();
});
+ it('the page barrel is NOT declared side-effectful any more (objectui#6776)', () => {
+ // The other half of the move, stated as a pin rather than left to the
+ // ledger: `views/metadata-admin/index.ts` is what the console's six
+ // `lazy()` declarations name, and it is shakeable ONLY while the published
+ // `sideEffects` array does not name it. Re-adding it there — or putting a
+ // bare `import './register-builtins.js';` back on the barrel, which
+ // `bareSideEffectImport` reads as the same claim — puts 172,945 gzipped
+ // bytes back into every console page load, silently.
+ const owner = nearestPackage('packages/app-shell/src/views/metadata-admin/index.ts', REPO_ROOT);
+ expect(owner?.packageJsonPath).toBe('packages/app-shell/package.json');
+ const manifest = JSON.parse(read(owner!.packageJsonPath)) as { sideEffects?: unknown };
+ expect(declaredSideEffectful(manifest.sideEffects, owner!.packageRelative)).toBeNull();
+ expect(bareSideEffectImport(read('packages/app-shell/src/views/metadata-admin/index.ts'))).toBeNull();
+ });
+
it('the source-reading guard is blind to it, which is why this one exists', () => {
// Stated as a test rather than a comment: if `bareSideEffectImport` ever
// learns to see top-level calls, this expectation flips and the reader is
diff --git a/scripts/check-eager-closure-budget.mjs b/scripts/check-eager-closure-budget.mjs
index 186ff4501..314b15e0d 100644
--- a/scripts/check-eager-closure-budget.mjs
+++ b/scripts/check-eager-closure-budget.mjs
@@ -138,6 +138,26 @@
* reopening the same day it was measured shut. The two numbers move in ONE
* commit for the reason the paragraph above gives.
*
+ * objectui#6776 lowered it a third time, to 3,268,000 over 3,222,314, and this
+ * one was earned the same way. `views/metadata-admin/index.ts` stopped being a
+ * registering module — its five load-time registrations moved to a leaf the
+ * PACKAGE ENTRY bare-imports — so the `sideEffects` array stopped naming it, the
+ * package barrel's 25 runtime re-exports were re-pointed at leaf modules, and
+ * the whole `metadata-admin` chunk (172,945 gzipped bytes, 144 modules) left the
+ * eager closure: 3,254,230 -> 3,222,314, −31,916 bytes, measured on two full
+ * console builds. The maintainer ruling of 2026-08-30 made the re-baseline part
+ * of the change rather than a follow-up, in its own words:
+ *
+ * ⛔ ceiling 处置写死(不作实施者临场判断):-31KB 把余量推到 ~0.89x 门禁
+ * 89KB 回归阈值(近盲),同批重设 `MAX_EAGER_CLOSURE_GZIP_BYTES`;抬 ceiling
+ * 是有申报程序的 ratchet,裁决原话引入 PR 正文。
+ *
+ * Measured, the drift was 0.85x rather than 0.89x — the gate printed
+ * `headroom 75.9 KB = 0.85x the 89.0 KB regression` on the post-change build
+ * before this constant moved. Either way it is the blind band reopening, and the
+ * direction of this edit is DOWN: no build that passed before it and measures
+ * under 3,268,000 fails after it.
+ *
* ⚠️ Read the direction correctly: the closure did NOT fall by the 242.6 KB the
* objectui#6683 card projected. That figure was measured for
* `"sideEffects": false`, which is closed by measurement because it also DROPS
@@ -157,12 +177,12 @@
* become an excuse to widen it — a ceiling that rises while the sensitivity
* relaxes is a gate quietly retiring itself.
*
- * This is a truthful CURRENT-STATE ceiling, not a target. 3.15 MB gzipped
+ * This is a truthful CURRENT-STATE ceiling, not a target. 3.07 MB gzipped
* before first render is a bad payload, and the honest long-term line is far
* below it — but lowering the line to a TARGET is a separate decision with its
* own work behind it (objectui#5324 names the candidates), and re-baselining
* onto a fresh measurement is not that. Nothing here should be read as a
- * finding that 3.19 MB is acceptable.
+ * finding that 3.12 MB is acceptable.
*
* ## Per-chunk ceilings (objectui#5490)
*
@@ -210,10 +230,10 @@ import { isEntrypoint } from './invoked-as.mjs';
/**
* Ceiling for the console eager closure, in gzipped bytes. See the header for
- * how this number was chosen; measured 3,254,004 on `bd2a7ec50`.
+ * how this number was chosen; measured 3,222,314 on `3d257c85a`.
*
- * Re-baselined DOWNWARD twice, each time toward a measurement the payload had
- * already fallen to:
+ * Re-baselined DOWNWARD three times, each time toward a measurement the payload
+ * had already fallen to:
*
* - objectui#5924, from 4,086,000 (derived from the 4,005,911 reading on
* `4c1623c0c`) to 3,345,000 over 3,299,898 on `48e53814e`.
@@ -224,10 +244,17 @@ import { isEntrypoint } from './invoked-as.mjs';
* at 1.00x its own sensitivity and, on the next byte of shrink, tripped the
* exit-2 verdict about the gauge. Lowering it in the SAME change is the
* tightening the maintainer ruling of 2026-08-29 asked for.
- *
- * Headroom is 45,996 bytes — 0.50x {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}.
+ * - objectui#6776, to 3,268,000 over 3,222,314. The metadata-admin engine's
+ * five load-time registrations moved out of the page barrel, so the barrel
+ * stopped being named by the `sideEffects` array and the 172,945-byte
+ * `metadata-admin` chunk left the eager closure (−31,916 bytes). The
+ * 3,300,000 ceiling was measured carrying 75.9 KB of headroom afterwards —
+ * 0.85x the regression this gate must catch, the blind band reopening — and
+ * the 2026-08-30 ruling made moving it part of the same change.
+ *
+ * Headroom is 45,686 bytes — 0.50x {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}.
*/
-export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_300_000;
+export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_268_000;
/**
* The measurement the ceiling above was derived from. Exported so the two
@@ -240,17 +267,17 @@ export const BASELINE = Object.freeze({
/**
* `emitEagerClosureReport`'s `eagerGzipBytes` on this commit.
*
- * `bd2a7ec50` is the commit that carries the array and the gates; this
- * constant was written one commit later, and the two trees differ ONLY by
- * this recorded identifier. That is safe to state rather than hope: the
+ * `3d257c85a` is the commit that carries the metadata-admin split
+ * (objectui#6776); this constant was written one commit later, and the two
+ * trees differ ONLY by this file. That is safe to state rather than hope, and
+ * it is the same argument the previous baseline (`bd2a7ec50`) made: the
* console build's turbo `inputs` cover `scripts/vite-*.ts`, not
- * `scripts/check-*.mjs`, so nothing in this file reaches the bundler. The
- * figure was re-measured on the later commit and came back identical.
+ * `scripts/check-*.mjs`, so nothing in this file reaches the bundler.
*/
- gzipBytes: 3_254_004,
+ gzipBytes: 3_222_314,
chunks: 48,
- totalChunks: 513,
- commit: 'bd2a7ec50',
+ totalChunks: 517,
+ commit: '3d257c85a',
});
/**
diff --git a/scripts/vite-declared-lazy-views.ts b/scripts/vite-declared-lazy-views.ts
index e1971ad30..6dc67961a 100644
--- a/scripts/vite-declared-lazy-views.ts
+++ b/scripts/vite-declared-lazy-views.ts
@@ -104,7 +104,7 @@ import type { Plugin, Rollup } from 'vite';
*
* | chunk | gz eager | mechanism |
* |----------------------------|----------|------------------------------------|
- * | `metadata-admin` | 172,651 | real static edges — PINNED below |
+ * | `metadata-admin` | 172,651 | real static edges — FIXED, #6776 |
* | `MarketplacePackagePage` | 7,647 | chunk co-tenancy — FIXED |
* | `MarketplaceInstalledPage` | 1,836 | chunk co-tenancy — FIXED |
* | `MarketplacePage` | 0 | already lazy (the control) |
@@ -178,52 +178,73 @@ export const EAGER_WALK_CONTROL = 'packages/app-shell/src/views/ObjectView.tsx';
* (`scripts/__tests__/vite-declared-lazy-views.test.ts` checks that, and that
* every entry still names a file that exists).
*
- * Two entries stand, and neither stands for the barrel re-export objectui#6535
- * removed:
+ * ONE entry stands, and it does not stand for the barrel re-export
+ * objectui#6535 removed:
*
* - `RecordDetailView` — a real static edge.
* `packages/app-shell/src/views/ObjectView.tsx` imports it by name, and
* `ObjectView` sits in AppContent's own "eagerly loaded — always needed"
* block. Splitting it would mean giving `ObjectView` a lazy boundary.
*
- * - `views/metadata-admin/index.ts` — the LARGEST single entry this ledger has
- * ever carried (172,651 bytes gzipped, 144 modules, 5.3% of the whole eager
- * closure) and the one most likely to be "fixed" by someone who has not read
- * why it stands. It is the target of SIX `lazy()` declarations in AppContent
- * and it is statically imported by two modules that are eager by
- * construction, both measured from the emitted chunk's module list on
- * `b98352a15` (objectui#6681):
- *
- * 1. `packages/app-shell/src/index.ts` — the package barrel, which
- * re-exports eleven runtime values from it (`registerMetadataPreview`,
- * `useMetadataClient`, …). The console's entry imports that barrel.
- * 2. `packages/app-shell/src/services/builtinComponents.tsx` — which the
- * barrel BARE-imports (`import './services/builtinComponents.js';`) for
- * its ComponentRegistry registrations, and which imports
- * `MetadataDirectoryPage` and `MetadataResourceRouter` from this module
- * BY VALUE. A registry entry that names a component must hold the
- * component.
- *
- * Neither edge is removable inside a bundling change, and the module cannot
- * be declared pure: it performs FIVE top-level registrations at module load
- * (`registerBuiltinAnchors`, `registerDefaultMetadataSchemas`,
- * `registerDatasourceResource`, `registerBuiltinPreviews`,
- * `registerBuiltinInspectors`), which is why
- * `@object-ui/app-shell`'s own `sideEffects` array names it (objectui#6683).
- * ⚠️ Those five are CALLS, not bare imports, so {@link bareSideEffectImport}
- * returns `null` for this file — the guard that actually refuses to declare
- * it pure is {@link declaredSideEffectful}, reading the package's own array.
- *
- * The six pages the declarations name (`DirectoryPage`, `StudioHomePage`,
- * `ResourceListPage`, `ResourceEditPage`, `ResourceHistoryPage`,
- * `DiagnosticsPage`) have NO dynamic importer of their own in the emitted
- * graph — they are reached only through this barrel's static re-exports —
- * so no chunking policy separates them from it. Making this lazy means
- * changing what `registerAppComponent` accepts (a component VALUE today) and
- * what the package barrel re-exports; that is a published-contract decision,
- * not a bundling one, and it is recorded as such rather than attempted here.
- *
- * ## Two entries were REMOVED here, and that removal is a recorded win
+ * ## `views/metadata-admin/index.ts` was the third removal — objectui#6776
+ *
+ * It was the LARGEST single entry this ledger ever carried: 172,945 bytes
+ * gzipped over 144 modules, 5.3% of the eager closure, the target of SIX
+ * `lazy()` declarations in AppContent that deferred nothing. Two static edges
+ * held it, both measured from the emitted chunk's module list (objectui#6681):
+ *
+ * 1. `packages/app-shell/src/index.ts` — the package barrel re-exported 25
+ * runtime values from it (`registerMetadataPreview`, `useMetadataClient`,
+ * …; the earlier note here said eleven, which counted only part of the
+ * list and omitted the 11 type-only names that carry no edge at all —
+ * corrected by objectui#6785). The console's entry imports that barrel.
+ * 2. `packages/app-shell/src/services/builtinComponents.tsx` — which the
+ * barrel BARE-imports for its ComponentRegistry registrations, and which
+ * imported `MetadataDirectoryPage` and `MetadataResourceRouter` from this
+ * module BY VALUE. A registry entry that names a component must hold the
+ * component.
+ *
+ * The note that stood here said neither edge was removable inside a bundling
+ * change, and that was right — the repair was NOT a bundling change. Under the
+ * maintainer ruling of 2026-08-30 (objectui#6776), three things moved together:
+ *
+ * - the five top-level registrations (`registerBuiltinAnchors`,
+ * `registerDefaultMetadataSchemas`, `registerDatasourceResource`,
+ * `registerBuiltinPreviews`, `registerBuiltinInspectors`) moved to
+ * `views/metadata-admin/register-builtins.ts`, bare-imported by the PACKAGE
+ * ENTRY, so they stay exactly as eager as they were while the page barrel
+ * stops being side-effectful. `@object-ui/app-shell`'s `sideEffects` array
+ * names the new leaf instead of the barrel;
+ * - the package barrel's 25 runtime re-exports now name the LEAF modules,
+ * same names and same types, which removes edge 1; and
+ * - `builtinComponents.tsx` registers the two pages as `lazy()` values, each
+ * behind its own `Suspense` inside the registration value, which removes
+ * edge 2 without touching `registerAppComponent`'s signature.
+ *
+ * ⚠️ The `lazy()` in `builtinComponents.tsx` is the half that looks sufficient
+ * and is not. Measured on `fab4802e3` with ONLY that half applied: the eager
+ * closure went UP by 211 bytes, the `metadata-admin` chunk went up by 396, the
+ * eager chunk count did not move, the chunk stayed EAGER — and the build exited
+ * 0 with this plugin printing "2 eager, all pinned" and
+ * `scripts/vite-ineffective-dynamic-imports.ts` printing its usual 43, because
+ * neither can see a static edge that lives in another module. That trap is
+ * written up beside the code in `builtinComponents.tsx`.
+ *
+ * Read that 211 as a magnitude, not a direction: an INDEPENDENT rebuild of the
+ * same variant measured the closure delta at -7 bytes. Both figures stand as
+ * what their run measured, and the disagreement is the point — the delta is
+ * small and sensitive to the byte-form of the edit, so its sign is not a
+ * finding. The finding is the pair that reproduced on both rebuilds: the chunk
+ * stayed EAGER and the eager chunk count did not move (45 of 513).
+ *
+ * ⛔ What has NOT changed: the five registrations are still load-bearing and
+ * still unshakeable. {@link declaredSideEffectful} — not
+ * {@link bareSideEffectImport}, which returns `null` for a top-level CALL —
+ * is the guard that refuses to declare their module pure, and it now reads
+ * `register-builtins.ts` out of the package's own array. Moving the bare import
+ * back onto the page barrel would re-arm the whole defect under a new name.
+ *
+ * ## Two entries were REMOVED here BEFORE that, and both removals are wins
*
* `RecordFormPage` and `ReportView` were pinned for a third reason, the one no
* `grep` over the source shows: CHUNK CO-TENANCY (objectui#6680). Rolldown
@@ -246,7 +267,6 @@ export const EAGER_WALK_CONTROL = 'packages/app-shell/src/views/ObjectView.tsx';
*/
export const DECLARED_LAZY_VIEWS_STILL_EAGER: readonly string[] = Object.freeze([
'packages/app-shell/src/views/RecordDetailView.tsx',
- 'packages/app-shell/src/views/metadata-admin/index.ts',
]);
/**