Skip to content

Commit 993f27e

Browse files
objectstack-fleet[bot]claudehuangyiirene
authored
perf(console): the first screen stops loading the spec entries only the metadata designers validate with; the eager-closure ceiling comes down by what that recovered (objectui#11101) (#11342)
Fixes #11101 Clause-②: no. Load timing only; no contract, export or accept set moves, and validation is unchanged once loaded. The console-side payback of the objectui#11088 decision 1 = A loan (the `@objectstack/*` 17.5.0 ceiling raise of PR #11086). Implemented by `os-dev` for the `domain:devx` seat 2 claim `5924908810`, session `https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh`. ## Measured first Every `@objectstack/spec` entry the console bundles sits in one chunk, `vendor-objectstack`, and that chunk is a static import of the entry. Read on a console build of `main` at `5262f7d` with a throwaway module-graph dump (not committed). "gzip alone" is each module's rendered code gzipped by itself: an attribution, not additive, and larger than the module's share of the chunk. | spec entry | rendered | gzip alone | static importers in eager chunks | verdict | |---|---:|---:|---|---| | root `@objectstack/spec` | 2,370,467 | 692,269 | `@object-ui/core`, `@object-ui/auth`, `@objectstack/formula`, metadata-admin `view-item-normalize.ts` | runtime; objectstack#20646's | | `/system` | 1,039,623 | 281,625 | `views/FlowRunner.tsx` (runtime), metadata-admin `metadata-form-i18n.ts`, `previews/doc-draft.ts` | runtime | | `/kernel` | 886,545 | 247,041 | `@object-ui/data-objectstack` (runtime), preview and designer modules | runtime | | `/data` | 614,780 | 172,275 | core, fields, grid, detail, form, client, formula | runtime | | `/ui` | 584,593 | 156,024 | shell, layout, components, grid, list, view | runtime | | `/shared` | 368,108 | 81,754 | `preview/DraftChangesPanel.tsx` (through `ConsoleLayout`'s draft preview bar), `inspectors/ObjectFieldInspector.tsx`, `previews/simulator/flow-sim-validate.ts` | admin/designer code, eager through the layout and the registration leaf | | `/api` | 322,818 | 90,817 | core, data-objectstack, client, formula | runtime | | `/automation` | 127,672 | 37,283 | `utils/approverIdentity.ts` (runtime, `DeclaredActionsBar`) plus metadata-admin inspectors | runtime | | `/ai` | 108,241 | 32,100 | **none**: only `clientValidation.ts`, through `await import()` | lazy in source, eager by the chunk group | | `/security` | 58,779 | 18,500 | formula, plugin-grid | runtime | | `/identity` | 26,836 | 9,348 | `inspectors/FlowReferenceField.tsx`, eager through `register-builtins.ts` | admin-only, eager through the registration leaf | | `/integration` | 23,392 | 6,981 | **none**: only `clientValidation.ts`, through `await import()` | lazy in source, eager by the chunk group | | `/contracts` | 1,467 | 639 | none (lazy console pages, the linter) | lazy in source, eager by the chunk group | | `@objectstack/sdui-parser` (not spec) | 19,801 | 5,694 | none (the linter only) | lazy in source, eager by the chunk group | ⇒ The hypothesis held only in part. `clientValidation.ts` already loads every spec entry through `await import()`. What kept `/ai` and `/integration` on the first screen was the `vendor-objectstack` chunk group: a group claims by module id, not by reachability. That is the objectui#5266 mechanism, one package over; the lookaheads in `VENDOR_OBJECTSTACK_TEST` only ever excluded `@objectstack/lint`. Every other entry the validator loads is also imported statically by something on the first screen, so no lazy import in the validator can move it. ## The change 1. `apps/console/vite.config.ts`: the `vendor-objectstack` group gets `tags: ['$initial']`. That is rolldown's built-in tag for "statically reachable from an entry", so the group now claims only the entry's static closure. A vendor module that sits behind an `import()` follows its importer into a lazy chunk. Nothing the first screen executes moves, because `$initial` IS that closure. The lint lookaheads stay. A group-table pin in the budget's unit test records the tag. 2. `scripts/check-eager-closure-budget.mjs`: the ceilings come down by exactly what was recovered (below). The owed `squashMerge` handle for `048e7f6` is back-filled, and the near-ceiling `framework` fixture pair holds the aggregate at `BASELINE` by construction. 3. A patch changeset for `@object-ui/console`, whose published `dist` changes chunk layout. `check-changeset-presence` owes none, because no published source moved. ## Eager closure, before and after Two builds of one tree, one container, one instrument (`CI=true pnpm exec vite build` in `apps/console`, reading `apps/console/dist/eager-closure.json`), in one `os-verify-lock.sh` hold. The trees differ only in the vite.config line; the before leg put `5262f7d`'s blob on disk and the after leg restored `HEAD`'s, each proven by blob hash. | | `5262f7d` (`main`) | `4acbea07` (this PR) | delta | |---|---:|---:|---:| | aggregate eager closure, gzip | 3,671,724 | 3,638,104 | **−33,620** | | `vendor-objectstack`, gzip | 1,704,699 | 1,670,964 | −33,735 | | the other 329 eager chunks, gzip | 1,967,025 | 1,967,140 | +115 (import bookkeeping, 31 chunks, none over 20) | | aggregate eager closure, raw | 12,366,134 | 12,257,433 | −108,701 | | eager / total chunks | 330 / 2447 | 330 / 2450 | three new lazy chunks: `ai`, `integration`, `contracts` | Noise: the instrument is deterministic per tree. The untagged config read 3,671,724 on three builds (the graph-dump build, the canonical before leg, the ablation's mutated leg), and the tagged config read 3,638,104 on three (the graph-dump build, the canonical after leg, the ablation's restored leg). The recovered 33,620 is 0.37x the 89 KiB regression the gate exists to catch. ## The ceiling: lowered by the recovered amount | constant | before | after | |---|---:|---:| | `MAX_EAGER_CLOSURE_GZIP_BYTES` | 3,694,000 | **3,660,380** (−33,620) | | `BASELINE` | 3,648,737 on `048e7f6` | 3,638,104 on `4acbea07` | | `PER_CHUNK_GZIP_CEILINGS['vendor-objectstack']` | 1,723,000 | **1,689,265** (−33,735) | | `PER_CHUNK_BASELINE['vendor-objectstack']` | 1,703,690 on `048e7f6` | 1,670,964 on `4acbea07` | The ceilings are not re-derived as "reading + half a regression". That would absorb `main`'s own drift since `048e7f6` (+22,987 on `5262f7d`) into the ceiling, which is a raise under another name. The live headroom `main` had before the payback is kept to the byte: 22,276 (0.24x) on the aggregate and 18,301 (0.20x) on `vendor-objectstack`. No build that passed before and measures under 3,660,380 fails after. `REGRESSION_THIS_GATE_MUST_CATCH_BYTES` and the other three per-chunk rows did not move. **`squashMerge`:** `048e7f6` landed on `main` as `81f8498` (#11086, single parent, an ancestor of `origin/main`). That handle is back-filled in the header prose that still cites the `048e7f6` reading. This change re-pins `BASELINE` onto its own reading, so the field is `null` again by its own rule ("the change that re-pins the field above cannot write its own here"). The `4acbea07` → squash back-fill is owed once this merges. ## Ablation: re-eager, and the lowered ceiling goes red `node ../objectstack/scripts/ablation-replace.mjs` dropped `tags: ['$initial']` (anchor 1 → 0, blob `40f6d42c` → `32c403ab`), rebuilt, and ran the gate: - **mutated:** 3,671,724 bytes, `node scripts/check-eager-closure-budget.mjs` **exit 1**. The aggregate line read "3585.7 KB … 11.1 KB over the 3574.6 KB budget", and `vendor-objectstack` read "1664.7 KB / 1649.7 KB ceiling (OVER by 15.1 KB)". - **restored:** blob back to `HEAD` (`40f6d42c`), `git diff HEAD` empty. The rebuild read 3,638,104 and the gate **exit 0**. ## Validation unchanged once a designer opens - **Probe on the BUILT console** (`vite preview`, proxied to a `--fresh` objectstack 17.5.0 showcase backend started by `e2e/live/ci/start-backend.sh`, Playwright + `/opt/pw-browsers/chromium`). The probe ran two builds of the same tree, with and without the tag. - First screen (`/apps/showcase_app`, 331 JS requests): no `ai-*` or `integration-*` chunk is fetched. - `/apps/showcase_app/metadata/agent/new`: opening the designer fetches `ai-rfegJgG7.js`. Typing `Bad Name!` into Name raises "This metadata does not match the spec — 4 validation error(s). Name: Invalid string: must match pattern …". - `/apps/showcase_app/metadata/connector/new`: this designer fetches `integration-DM--okeM.js`, and the 3-error banner shows. - The validator's text is **byte-identical** between the untagged and the tagged build, for both designers. - The only non-2xx was `GET /api/v1/usage/storage` 404 from the backend, present on both builds. No `pageerror`. - **Existing tests, all of `packages/app-shell/src/views/metadata-admin/**` (373 files):** 36 clientValidation / ResourceEditPage / createConformance files plus 1 console file, 398 passed. The other 337 files ran in three chunks: 1066 passed; 1455 passed and 1 skipped; 1462 passed. ## Verification (head `7d24740c`) - Console build + budget, before and after, under the lock: the table above. Checker exit 0 on both. The after build gets exit 0 at the new constants: "3552.8 KB … budget: 3574.6 KB, headroom: 21.8 KB". - `scripts/__tests__`: the 18 files that read `apps/console/vite.config.ts` or the budget script, 1020 passed. That includes `check-eager-closure-budget.test.ts` at 179, plus `vite-objectstack-spec-dist`, `vite-objectstack-client-dist`, `render-budget-comment` and `check-node-esm-load`. - `apps/console` vitest project: 139 files, 1593 passed. - `pnpm --filter @object-ui/console run type-check` exit 0. Reverse check: `tags: ['$bogus']` gives `tsc -b tsconfig.node.json` exit 1 with TS2769 at the group, then restored. - `pnpm --filter @object-ui/app-shell run type-check` exit 0. - Both type-checks ran after `turbo run build --filter='@object-ui/console^...'`. - `pnpm check:control-bytes` OK. - `node scripts/check-changeset-presence.mjs` exit 0. - `pnpm changeset:check` exit 0. - `pnpm check:changeset-claims` exit 0, report-only. It shows two pending bodies naming files touched here, `6681-declared-lazy-marketplace-routes.md` and `7173-ai-pending-actions-inbox-i18n.md`. I read both, and this diff falsifies neither. - `pnpm check:new-line-citations` 0 new. - Narrowed lint: `eslint --no-inline-config --format json` on the 3 changed code files gives 3 files, 0 errors, 0 warnings. - Population: all three are inside `eslint.config.js`'s population; none was reported ignored. - Invariance: the config enables no type-aware linting, so this diff cannot move a verdict on an untouched file. - **NOT MEASURED:** - The full `@object-ui/app-shell` suite outside `metadata-admin/**`, because its ~600 remaining files exceed the foreground window on a box shared by three devs. `packages/app-shell/src` is untouched by this diff, and CI runs the suite. - The repo-wide `pnpm lint`, which CI owns. ## Acceptance notes (observations, not filed) - `/shared` (81,754 gzip alone) and `/identity` (9,348) are reached on the first screen only by designer code. Their routes in are `register-builtins.ts` → `inspectors/index.ts` / `previews/index.ts`, which the package entry bare-imports, and `ConsoleLayout` → `DraftPreviewBar` → `DraftChangesPanel`. Recovering them means lazy-registering the inspector and preview tables, which is an architecture change, not this card. Carrier: none. - `DraftChangesPanel.tsx`'s comment says `@objectstack/core` (inside `@objectstack/client`) has a runtime import of `@objectstack/spec/shared` that keeps that subpath eager. In this 17.5.0 bundle, `/shared`'s importers are the three objectui modules above plus the linter. The comment's premise no longer holds. Carrier: none. - `e2e/live/studio-editor.spec.ts` waits on `getByTestId('metadata-edit-page')`. `git grep` finds that string in no other tracked file, and the agent and connector edit pages rendered without it (this probe's first wait on it timed out). Whether the live suite still runs that spec, and how it passes, is NOT MEASURED. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: 黄怡 <huangyi@hotoa.com>
1 parent db0beb2 commit 993f27e

4 files changed

Lines changed: 227 additions & 59 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/console': patch
3+
---
4+
5+
perf(console): the first screen no longer downloads the spec entries only the metadata designers' validation uses
6+
7+
The metadata designers' client validation loads each metadata type's spec schema through
8+
`import()`, so `@objectstack/spec`'s `/ai` and `/integration` entries were meant to arrive
9+
only when a designer validates an agent, tool, skill or connector draft. They arrived on
10+
every page load instead: the `vendor-objectstack` chunk group claimed them, and that chunk
11+
is a static import of the console entry. The group now claims only the entry's static
12+
closure (`tags: ['$initial']`), so those two entries — and `/contracts` and
13+
`@objectstack/sdui-parser`, which only lazy code reaches — load with the code that needs
14+
them. Nothing the first screen runs moved, and validation answers exactly as before once
15+
loaded. The console performance budget's ceiling comes down by the bytes recovered
16+
(objectui#11101).

‎apps/console/vite.config.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,37 @@ export default defineConfig({
925925
groups: [
926926
{ name: 'vendor-react', test: /[\\/]node_modules[\\/](react|react-dom|react-router|scheduler)[\\/]/, priority: 100 },
927927
{ name: 'vendor-radix', test: /[\\/]node_modules[\\/]@radix-ui[\\/]/, priority: 95 },
928-
{ name: 'vendor-objectstack', test: vendorObjectstackTest, priority: 95 },
928+
//
929+
// ## `tags: ['$initial']` — this group claims only what the first screen runs (objectui#11101)
930+
//
931+
// A group claims by module ID, not by reachability, and this group's
932+
// chunk is a static import of the entry. So every `@objectstack/*`
933+
// module it claims is downloaded and parsed on every page load —
934+
// including one the source reaches only through `import()`. That is
935+
// how `@objectstack/lint` went eager in objectui#5266, and the
936+
// lookaheads in `VENDOR_OBJECTSTACK_TEST` exclude that one package
937+
// and nothing else. The tag closes the class: rolldown's built-in
938+
// `$initial` tag marks a module statically reachable from an entry,
939+
// so a vendor module that sits only behind an `import()` is no
940+
// longer claimed here and follows its importer into a lazy chunk.
941+
//
942+
// What it moved, on objectui#11101's two builds of one tree that
943+
// differ only in this tag: `@objectstack/spec`'s `/ai` and
944+
// `/integration` entries, which the metadata designers' client
945+
// validation (`views/metadata-admin/clientValidation.ts`) reaches
946+
// only through `await import()`; `/contracts`, reached by lazy
947+
// console pages and the linter; and `@objectstack/sdui-parser`,
948+
// reached by the linter alone. The bytes are recorded once, on
949+
// `BASELINE` in `scripts/check-eager-closure-budget.mjs`, ⛔ not here.
950+
//
951+
// ⛔ It moves nothing the first paint needs: `$initial` IS the static
952+
// closure of the entry, so every module the first screen executes is
953+
// still claimed by this group, into the same chunk. The linter's
954+
// lookaheads stay — `assertLazyLinterStaysLazy` names them in its
955+
// diagnostic. Dropping the tag puts the four modules back on every
956+
// page load, and the lowered ceiling in
957+
// `scripts/check-eager-closure-budget.mjs` is what reds on it.
958+
{ name: 'vendor-objectstack', test: vendorObjectstackTest, priority: 95, tags: ['$initial'] },
929959
{ name: 'vendor-icons-core', test: /[\\/]node_modules[\\/]lucide-react[\\/]dist[\\/](lucide-react|esm[\\/](Icon|createLucideIcon|defaultAttributes|shared))/, priority: 90 },
930960
//
931961
// ## ONE CHUNK PER ICON — and ⛔ why this is not the regroup objectui#9251 forbids

‎scripts/__tests__/check-eager-closure-budget.test.ts‎

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,32 @@ describe('chunk attribution (objectui#7399)', () => {
614614
},
615615
);
616616

617+
/**
618+
* objectui#11101 — the same blind spot one option over. Every case above
619+
* asks which group's `test` CLAIMS a module id, and `vendor-objectstack`'s
620+
* test claims `@objectstack/spec`'s `/ai` and `/integration` entries as
621+
* readily as `/ui`. Whether a claimed module is EAGER is not a property of
622+
* the test at all: the group's chunk is a static import of the entry, so it
623+
* is eager whatever the source does — which is how two spec entries that
624+
* only the designers' client validation reaches, through `import()` in
625+
* `clientValidation.ts`, were on every page load. `tags: ['$initial']`
626+
* restricts the group to the entry's static closure.
627+
*
628+
* ⚠️ This pins the group TABLE. What the tag bought is weighed on a real
629+
* console build by `scripts/check-eager-closure-budget.mjs`, whose lowered
630+
* ceiling reds when the tag goes; neither substitutes for the other.
631+
*/
632+
it('narrows `vendor-objectstack` to the entry`s static closure, so an `import()` stays lazy', () => {
633+
const group = groups.find((g) => g.name === 'vendor-objectstack');
634+
expect(group).toBeDefined();
635+
expect(group!.options).toContain(`tags: ['$initial']`);
636+
// The control: the parse reads the tag off THIS group, not off every
637+
// group — a tail-blind or table-wide match would satisfy the line above.
638+
expect(groups.filter((g) => g.options.includes('$initial')).map((g) => g.name)).toEqual([
639+
'vendor-objectstack',
640+
]);
641+
});
642+
617643
it('leaves no second claimant at the winner`s priority', () => {
618644
for (const id of [LOCALE_MODULE, RESIDENT_LOCALE_MODULE, DATA_MODULE, ZOD_MODULE]) {
619645
const claiming = claimants(id);
@@ -780,9 +806,10 @@ describe('ceiling sensitivity, judged live (objectui#5924)', () => {
780806
// down to 3060.0 when lucide's 1,781-icon record left it, objectui#10996 UP
781807
// to 3104.5 on the authorised re-pin over `main`'s own drift, objectui#11073
782808
// UP to 3563.2 on the ruled raise for `@objectstack/*` 17.5.0,
783-
// objectui#11088 decision 1 = A) — a rendering derived in the test would
784-
// agree with the renderer by construction and pin nothing.
785-
expect(result.message).toContain('3563.2');
809+
// objectui#11088 decision 1 = A, objectui#11101 down to 3552.8 on the first
810+
// payback of that raise) — a rendering derived in the test would agree with
811+
// the renderer by construction and pin nothing.
812+
expect(result.message).toContain('3552.8');
786813
});
787814

788815
it('is exactly one regression wide, from either side of the line', () => {
@@ -1612,7 +1639,7 @@ describe('main', () => {
16121639
// about the FIXTURE while the gate under test behaved correctly. The number
16131640
// this case is actually about is "the report's chunk count, echoed".
16141641
expect(outputs.closure_chunks).toBe(String(fixture.files.length));
1615-
expect(outputs.closure_gzip_kb).toBe('3563.2');
1642+
expect(outputs.closure_gzip_kb).toBe('3552.8');
16161643
});
16171644

16181645
it('exits 1 — a verdict about the BUNDLE — when over budget', () => {
@@ -1759,9 +1786,22 @@ describe('main', () => {
17591786
* maintainer retired that leg (the ruling is quoted in this file's header
17601787
* and in the checker's), so the same fixture is now a pass — and the
17611788
* over-by-one fixture below is what proves the budget did not leave with it.
1789+
*
1790+
* ⚠️ Both fixtures hold the AGGREGATE at `BASELINE`: the bytes `framework`
1791+
* gains come out of the rest of the closure. "Every size line in the file
1792+
* was satisfied" has to hold by construction, not by which headroom happens
1793+
* to be wider. It used to hold by the latter, and objectui#11101 ended that:
1794+
* the first payback of the 17.5.0 raise lowered the aggregate line by
1795+
* exactly what it recovered, which left the aggregate's constant headroom
1796+
* narrower than `framework`'s, so growing `framework` to its line on top of
1797+
* an unchanged rest crossed the AGGREGATE line first, and this pair is about
1798+
* the per-chunk one.
17621799
*/
1800+
const frameworkAt = (bytes: number) =>
1801+
budgeted({ framework: bytes }, PER_CHUNK_BASELINE.framework - bytes);
1802+
17631803
it('exits 0 when a chunk sits just UNDER its ceiling — merely close is not a verdict', () => {
1764-
const { code, outputs } = run(budgeted({ framework: PER_CHUNK_GZIP_CEILINGS.framework - 1 }));
1804+
const { code, outputs } = run(frameworkAt(PER_CHUNK_GZIP_CEILINGS.framework - 1));
17651805
expect(code).toBe(0);
17661806
expect(outputs.closure_status).toBe('pass');
17671807
expect(outputs.closure_chunk_status).toBe('pass');
@@ -1781,7 +1821,7 @@ describe('main', () => {
17811821
* removed".
17821822
*/
17831823
it('still exits 1 when that same chunk goes OVER the same ceiling — the budget stays', () => {
1784-
const { code, outputs } = run(budgeted({ framework: PER_CHUNK_GZIP_CEILINGS.framework + 1 }));
1824+
const { code, outputs } = run(frameworkAt(PER_CHUNK_GZIP_CEILINGS.framework + 1));
17851825
expect(code).toBe(1);
17861826
expect(outputs.closure_status).toBe('pass');
17871827
expect(outputs.closure_chunk_status).toBe('fail');
@@ -2576,9 +2616,10 @@ describe('the prose attached to the baselines (objectui#7046)', () => {
25762616
* enough to accept both shapes would stop recording anything.
25772617
*
25782618
* objectui#10996 is the first re-baseline to meet it, and re-pinned it to the
2579-
* shape it left: `squashMerge` null, one commit carried. ⛔ Still exact and
2580-
* positional — a back-fill of the squash reds here again, and is re-pinned to
2581-
* two strings the same way.
2619+
* shape it left: `squashMerge` null, one commit carried. objectui#11073 and
2620+
* objectui#11101 each re-pinned the constant onto a branch tip of their own
2621+
* and left the same shape. ⛔ Still exact and positional — a back-fill of the
2622+
* squash reds here again, and is re-pinned to two strings the same way.
25822623
*/
25832624
it('records what each baseline carries as data, so the pin cannot go vacuous', () => {
25842625
expect(commitsCarriedBy(BASELINE)).toEqual([BASELINE.commit]);

0 commit comments

Comments
 (0)